1 /*
2  * Copyright (C) 2024 Huawei Device Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 #include <libexif/exif-data.h>
17 #include <zlib.h>
18 #include <array>
19 
20 #include "data_buf.h"
21 #include "exif_metadata.h"
22 #include "image_log.h"
23 #include "media_errors.h"
24 #include "metadata_stream.h"
25 #include "png_exif_metadata_accessor.h"
26 #include "png_image_chunk_utils.h"
27 #include "tiff_parser.h"
28 
29 #undef LOG_DOMAIN
30 #define LOG_DOMAIN LOG_TAG_DOMAIN_ID_IMAGE
31 
32 #undef LOG_TAG
33 #define LOG_TAG "PngImageChunkUtils"
34 
35 namespace OHOS {
36 namespace Media {
37 namespace {
38 constexpr auto ASCII_TO_HEX_MAP_SIZE = 103;
39 constexpr auto IMAGE_SEG_MAX_SIZE = 65536;
40 constexpr auto EXIF_HEADER_SIZE = 6;
41 constexpr auto PNG_CHUNK_KEYWORD_EXIF_APP1_SIZE = 21;
42 constexpr auto HEX_BASE = 16;
43 constexpr auto DECIMAL_BASE = 10;
44 constexpr auto PNG_PROFILE_EXIF = "Raw profile type exif";
45 constexpr auto PNG_PROFILE_APP1 = "Raw profile type APP1";
46 constexpr auto CHUNK_COMPRESS_METHOD_VALID = 0;
47 constexpr auto CHUNK_FLAG_COMPRESS_NO = 0;
48 constexpr auto CHUNK_FLAG_COMPRESS_YES = 1;
49 constexpr auto NULL_CHAR_AMOUNT = 2;
50 constexpr auto HEX_STRING_UNIT_SIZE = 2;
51 constexpr auto EXIF_INFO_LENGTH_TWO = 2;
52 constexpr auto CHUNKDATA_KEYSIZE_OFFSET_ONE = 1;
53 constexpr auto CHUNKDATA_KEYSIZE_OFFSET_THREE = 3;
54 }
55 
ParseTextChunk(const DataBuf & chunkData,TextChunkType chunkType,DataBuf & tiffData,bool & isCompressed)56 int PngImageChunkUtils::ParseTextChunk(const DataBuf &chunkData, TextChunkType chunkType,
57     DataBuf &tiffData, bool &isCompressed)
58 {
59     DataBuf keyword = GetKeywordFromChunk(chunkData);
60     if (keyword.Empty()) {
61         IMAGE_LOGE("Failed to read the keyword from the chunk data. Chunk data size: %{public}zu", chunkData.Size());
62         return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
63     }
64 
65     bool foundExifKeyword = FindExifKeyword(keyword.CData(), keyword.Size());
66     if (!foundExifKeyword) {
67         return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
68     }
69 
70     DataBuf rawText = GetRawTextFromChunk(chunkData, keyword.Size(), chunkType, isCompressed);
71     if (rawText.Empty()) {
72         IMAGE_LOGE("Failed to read the raw text from the chunk data. Chunk data size: %{public}zu", chunkData.Size());
73         return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
74     }
75 
76     return GetTiffDataFromRawText(rawText, tiffData);
77 }
78 
GetKeywordFromChunk(const DataBuf & chunkData)79 DataBuf PngImageChunkUtils::GetKeywordFromChunk(const DataBuf &chunkData)
80 {
81     if (chunkData.Size() <= 0) {
82         IMAGE_LOGE("Data size check failed: offset is larger than data size. "
83             "Data size: %{public}zu",
84             chunkData.Size());
85         return {};
86     }
87 
88     auto keyword = std::find(chunkData.CBegin(), chunkData.CEnd(), 0);
89     if (keyword == chunkData.CEnd()) {
90         IMAGE_LOGE("Keyword lookup failed: keyword not found in chunk data. "
91             "Chunk data size: %{public}zu",
92             chunkData.Size());
93         return {};
94     }
95     const size_t keywordLength = static_cast<size_t>(std::distance(chunkData.CBegin(), keyword));
96     return { chunkData.CData(), keywordLength };
97 }
98 
GetRawTextFromZtxtChunk(const DataBuf & chunkData,size_t keySize,DataBuf & rawText,bool & isCompressed)99 DataBuf PngImageChunkUtils::GetRawTextFromZtxtChunk(const DataBuf &chunkData, size_t keySize,
100     DataBuf &rawText, bool &isCompressed)
101 {
102     if (chunkData.Empty() || chunkData.CData() == nullptr) {
103         IMAGE_LOGE("Failed to get raw text from ztxt: chunkData is null. ");
104         return {};
105     }
106     if (chunkData.CData(keySize + CHUNKDATA_KEYSIZE_OFFSET_ONE) == nullptr) {
107         IMAGE_LOGE("Failed to get raw text from itxt:  keySize + 1 greater than chunklength.");
108         return {};
109     }
110     if (*(chunkData.CData(keySize + 1)) != CHUNK_COMPRESS_METHOD_VALID) {
111         IMAGE_LOGE("Metadata corruption detected: Invalid compression method. "
112             "Expected: %{public}d, Found: %{public}d",
113             CHUNK_COMPRESS_METHOD_VALID, *(chunkData.CData(keySize + 1)));
114         return {};
115     }
116 
117     size_t compressedTextSize = chunkData.Size() - keySize - NULL_CHAR_AMOUNT;
118     if (compressedTextSize > 0) {
119         const byte *compressedText = chunkData.CData(keySize + NULL_CHAR_AMOUNT);
120         int ret = DecompressText(compressedText, static_cast<uint32_t>(compressedTextSize), rawText);
121         if (ret != 0) {
122             IMAGE_LOGE("Failed to decompress text. Return code: %{public}d", ret);
123             return {};
124         }
125         isCompressed = true;
126     }
127     return rawText;
128 }
129 
GetRawTextFromTextChunk(const DataBuf & chunkData,size_t keySize,DataBuf & rawText)130 DataBuf PngImageChunkUtils::GetRawTextFromTextChunk(const DataBuf &chunkData, size_t keySize, DataBuf &rawText)
131 {
132     size_t rawTextsize = chunkData.Size() - keySize - 1;
133     if (rawTextsize) {
134         const byte *textPosition = chunkData.CData(keySize + 1);
135         rawText = DataBuf(textPosition, rawTextsize);
136     }
137     return rawText;
138 }
139 
FetchString(const char * chunkData,size_t dataLength)140 std::string FetchString(const char *chunkData, size_t dataLength)
141 {
142     if (chunkData == nullptr) {
143         IMAGE_LOGE("ChunkData is null. Cannot fetch string.");
144         return {};
145     }
146     if (dataLength == 0) {
147         IMAGE_LOGE("Data length is zero. Cannot fetch string.");
148         return {};
149     }
150     const size_t stringLength = strnlen(chunkData, dataLength);
151     return { chunkData, stringLength };
152 }
153 
CheckChunkData(const DataBuf & chunkData,size_t keySize)154 bool CheckChunkData(const DataBuf &chunkData, size_t keySize)
155 {
156     const byte compressionFlag = chunkData.ReadUInt8(keySize + 1);
157     const byte compressionMethod = chunkData.ReadUInt8(keySize + 2);
158     if ((compressionFlag != CHUNK_FLAG_COMPRESS_NO) && (compressionFlag != CHUNK_FLAG_COMPRESS_YES)) {
159         IMAGE_LOGE("Metadata corruption detected: Invalid compression flag. "
160             "Expected: %{public}d or %{public}d, Found: %{public}d",
161             CHUNK_FLAG_COMPRESS_NO, CHUNK_FLAG_COMPRESS_YES, compressionFlag);
162         return false;
163     }
164 
165     if ((compressionFlag == CHUNK_FLAG_COMPRESS_YES) && (compressionMethod != CHUNK_COMPRESS_METHOD_VALID)) {
166         IMAGE_LOGE("Metadata corruption detected: Invalid compression method. "
167             "Expected: %{public}d, Found: %{public}d",
168             CHUNK_COMPRESS_METHOD_VALID, compressionMethod);
169         return false;
170     }
171     return true;
172 }
173 
GetRawTextFromItxtChunk(const DataBuf & chunkData,size_t keySize,DataBuf & rawText,bool & isCompressed)174 DataBuf PngImageChunkUtils::GetRawTextFromItxtChunk(const DataBuf &chunkData, size_t keySize,
175     DataBuf &rawText, bool &isCompressed)
176 {
177     if (chunkData.CData(keySize + CHUNKDATA_KEYSIZE_OFFSET_THREE) == nullptr) {
178         IMAGE_LOGE("Failed to get raw text from itxt:  keySize + 3 greater than chunklength.");
179         return {};
180     }
181     const size_t nullCount = static_cast<size_t>(std::count(chunkData.CData(keySize + 3),
182                                                             chunkData.CData(chunkData.Size() - 1), '\0'));
183     if (nullCount < NULL_CHAR_AMOUNT) {
184         IMAGE_LOGE("Metadata corruption detected: Null character count after "
185             "Language tag is less than 2. Found: %{public}zu",
186             nullCount);
187         return {};
188     }
189     if (!CheckChunkData(chunkData, keySize)) {
190         return {};
191     }
192     const byte compressionFlag = chunkData.ReadUInt8(keySize + 1);
193 
194     const size_t languageTextPos = keySize + 3;
195     const size_t languageTextMaxLen = chunkData.Size() - keySize - 3;
196     std::string languageText =
197         FetchString(reinterpret_cast<const char *>(chunkData.CData(languageTextPos)), languageTextMaxLen);
198     const size_t languageTextLen = languageText.size();
199 
200     const size_t translatedKeyPos = languageTextPos + languageTextLen + 1;
201     std::string translatedKeyText = FetchString(reinterpret_cast<const char *>(chunkData.CData(translatedKeyPos)),
202         chunkData.Size() - translatedKeyPos);
203     const size_t translatedKeyTextLen = translatedKeyText.size();
204 
205     const size_t textLen = chunkData.Size() - (keySize + 3 + languageTextLen + 1 + translatedKeyTextLen + 1);
206     if (textLen == 0) {
207         return {};
208     }
209 
210     const size_t textPosition = translatedKeyPos + translatedKeyTextLen + 1;
211     const byte *textPtr = chunkData.CData(textPosition);
212     if (compressionFlag == CHUNK_FLAG_COMPRESS_NO) {
213         rawText = DataBuf(textPtr, textLen);
214     } else {
215         int ret = DecompressText(textPtr, textLen, rawText);
216         if (ret != 0) {
217             IMAGE_LOGE("Decompress text failed.");
218             return {};
219         }
220         isCompressed = true;
221     }
222     return rawText;
223 }
224 
GetRawTextFromChunk(const DataBuf & chunkData,size_t keySize,TextChunkType chunkType,bool & isCompressed)225 DataBuf PngImageChunkUtils::GetRawTextFromChunk(const DataBuf &chunkData, size_t keySize,
226     TextChunkType chunkType, bool &isCompressed)
227 {
228     DataBuf rawText;
229     isCompressed = false;
230     if (chunkType == zTXtChunk) {
231         GetRawTextFromZtxtChunk(chunkData, keySize, rawText, isCompressed);
232     } else if (chunkType == tEXtChunk) {
233         GetRawTextFromTextChunk(chunkData, keySize, rawText);
234     } else if (chunkType == iTXtChunk) {
235         GetRawTextFromItxtChunk(chunkData, keySize, rawText, isCompressed);
236     } else {
237         IMAGE_LOGE("Unexpected chunk type encountered: %{public}d", chunkType);
238         return {};
239     }
240     return rawText;
241 }
242 
FindExifKeyword(const byte * keyword,size_t size)243 bool PngImageChunkUtils::FindExifKeyword(const byte *keyword, size_t size)
244 {
245     if ((keyword == nullptr) || (size < PNG_CHUNK_KEYWORD_EXIF_APP1_SIZE)) {
246         return false;
247     }
248     if ((memcmp(PNG_PROFILE_EXIF, keyword, PNG_CHUNK_KEYWORD_EXIF_APP1_SIZE) == 0) ||
249         (memcmp(PNG_PROFILE_APP1, keyword, PNG_CHUNK_KEYWORD_EXIF_APP1_SIZE) == 0)) {
250         return true;
251     }
252     return false;
253 }
254 
FindExifFromTxt(DataBuf & chunkData)255 bool PngImageChunkUtils::FindExifFromTxt(DataBuf &chunkData)
256 {
257     DataBuf keyword = GetKeywordFromChunk(chunkData);
258     if (keyword.Empty()) {
259         IMAGE_LOGE("Failed to read the keyword from chunk.");
260         return false;
261     }
262 
263     bool foundExifKeyword = FindExifKeyword(keyword.CData(), keyword.Size());
264     if (!foundExifKeyword) {
265         IMAGE_LOGI("The text chunk is without exif keyword");
266         return false;
267     }
268     return true;
269 }
270 
VerifyExifIdCode(DataBuf & exifInfo,size_t exifInfoLength)271 size_t PngImageChunkUtils::VerifyExifIdCode(DataBuf &exifInfo, size_t exifInfoLength)
272 {
273     static const std::array<byte, EXIF_HEADER_SIZE> exifIdCode { 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 };
274     size_t exifIdPos = std::numeric_limits<size_t>::max();
275 
276     for (size_t i = 0; i < exifInfoLength - exifIdCode.size(); i++) {
277         if (exifInfo.CmpBytes(i, exifIdCode.data(), exifIdCode.size()) == 0) {
278             exifIdPos = i;
279             break;
280         }
281     }
282     return exifIdPos;
283 }
284 
GetTiffDataFromRawText(const DataBuf & rawText,DataBuf & tiffData)285 int PngImageChunkUtils::GetTiffDataFromRawText(const DataBuf &rawText, DataBuf &tiffData)
286 {
287     DataBuf exifInfo = ConvertRawTextToExifInfo(rawText);
288     if (exifInfo.Empty()) {
289         IMAGE_LOGE("Unable to parse Exif metadata: conversion from text to hex failed");
290         return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
291     }
292 
293     size_t exifInfoLength = exifInfo.Size();
294     if (exifInfoLength < EXIF_HEADER_SIZE) {
295         IMAGE_LOGE("Unable to parse Exif metadata: data length insufficient. "
296             "Actual: %{public}zu, Expected: %{public}d",
297             exifInfoLength, EXIF_HEADER_SIZE);
298         return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
299     }
300 
301     size_t exifHeadPos = VerifyExifIdCode(exifInfo, exifInfoLength);
302     if (exifHeadPos == std::numeric_limits<size_t>::max()) {
303         IMAGE_LOGE("Unable to parse metadata: Exif header not found");
304         return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
305     }
306 
307     size_t tiffOffset = EXIF_HEADER_SIZE;
308     tiffData = DataBuf(exifInfo.CData(tiffOffset), exifInfoLength - tiffOffset);
309     if (tiffData.Empty()) {
310         IMAGE_LOGE("Unable to extract Tiff data: data length insufficient. "
311             "Actual: %{public}zu, Expected: %{public}zu",
312             tiffData.Size(), exifInfoLength - tiffOffset);
313         return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
314     }
315     return SUCCESS;
316 }
317 
DecompressText(const byte * sourceData,unsigned int sourceDataLen,DataBuf & textOut)318 int PngImageChunkUtils::DecompressText(const byte *sourceData, unsigned int sourceDataLen, DataBuf &textOut)
319 {
320     if (sourceDataLen > IMAGE_SEG_MAX_SIZE) {
321         IMAGE_LOGE("Decompression failed: data size exceeds limit. "
322             "Data size: %{public}u, Limit: %{public}d",
323             sourceDataLen, IMAGE_SEG_MAX_SIZE);
324         return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
325     }
326     uLongf destDataLen = IMAGE_SEG_MAX_SIZE;
327 
328     textOut.Resize(destDataLen);
329     int result = uncompress(textOut.Data(), &destDataLen, sourceData, sourceDataLen);
330     if (result != Z_OK) {
331         IMAGE_LOGE("Decompression failed: job aborted");
332         return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
333     }
334     textOut.Resize(destDataLen);
335     return SUCCESS;
336 }
337 
StepOverNewLine(const char * sourcePtr,const char * endPtr)338 const char *PngImageChunkUtils::StepOverNewLine(const char *sourcePtr, const char *endPtr)
339 {
340     while (*sourcePtr != '\n') {
341         sourcePtr++;
342         if (sourcePtr == endPtr) {
343             return NULL;
344         }
345     }
346     sourcePtr++;
347     if (sourcePtr == endPtr) {
348         return NULL;
349     }
350     return sourcePtr;
351 }
352 
GetExifInfoLen(const char * sourcePtr,size_t * lengthOut,const char * endPtr)353 const char *PngImageChunkUtils::GetExifInfoLen(const char *sourcePtr, size_t *lengthOut, const char *endPtr)
354 {
355     while ((*sourcePtr == '\0') || (*sourcePtr == ' ') || (*sourcePtr == '\n')) {
356         sourcePtr++;
357         if (sourcePtr == endPtr) {
358             IMAGE_LOGE("Unable to get Exif length: content is blank");
359             return NULL;
360         }
361     }
362 
363     size_t exifLength = 0;
364     while (('0' <= *sourcePtr) && (*sourcePtr <= '9')) {
365         const size_t newlength = (DECIMAL_BASE * exifLength) + (*sourcePtr - '0');
366         exifLength = newlength;
367         sourcePtr++;
368         if (sourcePtr == endPtr) {
369             IMAGE_LOGE("Unable to get Exif length: no digit content found");
370             return NULL;
371         }
372     }
373     sourcePtr++; // ignore the '\n' character
374     if (sourcePtr == endPtr) {
375         IMAGE_LOGE("Unable to get Exif length: Exif info not found");
376         return NULL;
377     }
378     *lengthOut = exifLength;
379     return sourcePtr;
380 }
381 
ConvertAsciiToInt(const char * sourcePtr,size_t exifInfoLength,unsigned char * destPtr)382 int PngImageChunkUtils::ConvertAsciiToInt(const char *sourcePtr, size_t exifInfoLength, unsigned char *destPtr)
383 {
384     static const unsigned char hexAsciiToInt[ASCII_TO_HEX_MAP_SIZE] = {
385         0, 0, 0, 0, 0,    0, 0, 0, 0, 0,    0, 0, 0, 0, 0,    0, 0, 0, 0, 0,
386         0, 0, 0, 0, 0,    0, 0, 0, 0, 0,    0, 0, 0, 0, 0,    0, 0, 0, 0, 0,
387         0, 0, 0, 0, 0,    0, 0, 0, 0, 1,    2, 3, 4, 5, 6,    7, 8, 9, 0, 0,
388         0, 0, 0, 0, 0,    0, 0, 0, 0, 0,    0, 0, 0, 0, 0,    0, 0, 0, 0, 0,
389         0, 0, 0, 0, 0,    0, 0, 0, 0, 0,    0, 0, 0, 0, 0,    0, 0, 10, 11, 12,
390         13, 14, 15,
391     };
392 
393     size_t sourceLength = exifInfoLength * 2;
394     for (size_t i = 0; i < sourceLength; i++) {
395         while ((*sourcePtr < '0') || ((*sourcePtr > '9') && (*sourcePtr < 'a')) || (*sourcePtr > 'f')) {
396             if (*sourcePtr == '\0') {
397                 IMAGE_LOGE("Unexpected null character encountered while converting Exif ASCII string. "
398                     "Position: %{public}zu, Expected length: %{public}zu",
399                     i, sourceLength);
400                 return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
401             }
402             sourcePtr++;
403         }
404 
405         if ((i % HEX_STRING_UNIT_SIZE) == 0) {
406             *destPtr = static_cast<unsigned char>(HEX_BASE * hexAsciiToInt[static_cast<size_t>(*sourcePtr++)]);
407         } else {
408             (*destPtr++) += hexAsciiToInt[static_cast<size_t>(*sourcePtr++)];
409         }
410     }
411     return SUCCESS;
412 }
413 
ConvertRawTextToExifInfo(const DataBuf & rawText)414 DataBuf PngImageChunkUtils::ConvertRawTextToExifInfo(const DataBuf &rawText)
415 {
416     if (rawText.Size() <= 1) {
417         IMAGE_LOGE("The size of the raw profile text is too small.");
418         return {};
419     }
420     const char *sourcePtr = reinterpret_cast<const char *>(rawText.CData(1));
421     const char *endPtr = reinterpret_cast<const char *>(rawText.CData(rawText.Size() - 1));
422 
423     if (sourcePtr >= endPtr) {
424         IMAGE_LOGE("The source pointer is not valid.");
425         return {};
426     }
427     sourcePtr = StepOverNewLine(sourcePtr, endPtr);
428     if (sourcePtr == NULL) {
429         IMAGE_LOGE("Error encountered when stepping over new line in raw profile text.");
430         return {};
431     }
432 
433     size_t exifInfoLength = 0;
434     sourcePtr = GetExifInfoLen(sourcePtr, &exifInfoLength, endPtr);
435     if (sourcePtr == NULL) {
436         IMAGE_LOGE("Error encountered when getting the length of the string in raw profile text.");
437         return {};
438     }
439 
440     if ((exifInfoLength == 0) || (exifInfoLength > rawText.Size())) {
441         IMAGE_LOGE("Invalid text length in raw profile text.");
442         return {};
443     }
444 
445     DataBuf exifInfo;
446     exifInfo.Resize(exifInfoLength);
447     if (exifInfo.Size() != exifInfoLength) {
448         IMAGE_LOGE("Unable to allocate memory for Exif information.");
449         return {};
450     }
451     if (exifInfo.Empty()) {
452         return exifInfo;
453     }
454     unsigned char *destPtr = exifInfo.Data();
455     if (sourcePtr + EXIF_INFO_LENGTH_TWO * exifInfoLength > endPtr) {
456         IMAGE_LOGE("Invalid text length in raw profile text, it will result in OOB.");
457         return {};
458     }
459     int ret = ConvertAsciiToInt(sourcePtr, exifInfoLength, destPtr);
460     if (ret != 0) {
461         IMAGE_LOGE("Error encountered when converting Exif string ASCII to integer.");
462         return {};
463     }
464 
465     return exifInfo;
466 }
467 } // namespace Media
468 } // namespace OHOS
469