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 CHECK_INFO_RETURN_RET_LOG(!foundExifKeyword, false, "The text chunk is without exif keyword");
265 return true;
266 }
267
VerifyExifIdCode(DataBuf & exifInfo,size_t exifInfoLength)268 size_t PngImageChunkUtils::VerifyExifIdCode(DataBuf &exifInfo, size_t exifInfoLength)
269 {
270 static const std::array<byte, EXIF_HEADER_SIZE> exifIdCode { 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 };
271 size_t exifIdPos = std::numeric_limits<size_t>::max();
272
273 for (size_t i = 0; i < exifInfoLength - exifIdCode.size(); i++) {
274 if (exifInfo.CmpBytes(i, exifIdCode.data(), exifIdCode.size()) == 0) {
275 exifIdPos = i;
276 break;
277 }
278 }
279 return exifIdPos;
280 }
281
GetTiffDataFromRawText(const DataBuf & rawText,DataBuf & tiffData)282 int PngImageChunkUtils::GetTiffDataFromRawText(const DataBuf &rawText, DataBuf &tiffData)
283 {
284 DataBuf exifInfo = ConvertRawTextToExifInfo(rawText);
285 if (exifInfo.Empty()) {
286 IMAGE_LOGE("Unable to parse Exif metadata: conversion from text to hex failed");
287 return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
288 }
289
290 size_t exifInfoLength = exifInfo.Size();
291 if (exifInfoLength < EXIF_HEADER_SIZE) {
292 IMAGE_LOGE("Unable to parse Exif metadata: data length insufficient. "
293 "Actual: %{public}zu, Expected: %{public}d",
294 exifInfoLength, EXIF_HEADER_SIZE);
295 return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
296 }
297
298 size_t exifHeadPos = VerifyExifIdCode(exifInfo, exifInfoLength);
299 if (exifHeadPos == std::numeric_limits<size_t>::max()) {
300 IMAGE_LOGE("Unable to parse metadata: Exif header not found");
301 return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
302 }
303
304 size_t tiffOffset = EXIF_HEADER_SIZE;
305 tiffData = DataBuf(exifInfo.CData(tiffOffset), exifInfoLength - tiffOffset);
306 if (tiffData.Empty()) {
307 IMAGE_LOGE("Unable to extract Tiff data: data length insufficient. "
308 "Actual: %{public}zu, Expected: %{public}zu",
309 tiffData.Size(), exifInfoLength - tiffOffset);
310 return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
311 }
312 return SUCCESS;
313 }
314
DecompressText(const byte * sourceData,unsigned int sourceDataLen,DataBuf & textOut)315 int PngImageChunkUtils::DecompressText(const byte *sourceData, unsigned int sourceDataLen, DataBuf &textOut)
316 {
317 if (sourceDataLen > IMAGE_SEG_MAX_SIZE) {
318 IMAGE_LOGE("Decompression failed: data size exceeds limit. "
319 "Data size: %{public}u, Limit: %{public}d",
320 sourceDataLen, IMAGE_SEG_MAX_SIZE);
321 return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
322 }
323 uLongf destDataLen = IMAGE_SEG_MAX_SIZE;
324
325 textOut.Resize(destDataLen);
326 int result = uncompress(textOut.Data(), &destDataLen, sourceData, sourceDataLen);
327 if (result != Z_OK) {
328 IMAGE_LOGE("Decompression failed: job aborted");
329 return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
330 }
331 textOut.Resize(destDataLen);
332 return SUCCESS;
333 }
334
StepOverNewLine(const char * sourcePtr,const char * endPtr)335 const char *PngImageChunkUtils::StepOverNewLine(const char *sourcePtr, const char *endPtr)
336 {
337 while (*sourcePtr != '\n') {
338 sourcePtr++;
339 if (sourcePtr == endPtr) {
340 return NULL;
341 }
342 }
343 sourcePtr++;
344 if (sourcePtr == endPtr) {
345 return NULL;
346 }
347 return sourcePtr;
348 }
349
GetExifInfoLen(const char * sourcePtr,size_t * lengthOut,const char * endPtr)350 const char *PngImageChunkUtils::GetExifInfoLen(const char *sourcePtr, size_t *lengthOut, const char *endPtr)
351 {
352 while ((*sourcePtr == '\0') || (*sourcePtr == ' ') || (*sourcePtr == '\n')) {
353 sourcePtr++;
354 if (sourcePtr == endPtr) {
355 IMAGE_LOGE("Unable to get Exif length: content is blank");
356 return NULL;
357 }
358 }
359
360 size_t exifLength = 0;
361 while (('0' <= *sourcePtr) && (*sourcePtr <= '9')) {
362 const size_t newlength = (DECIMAL_BASE * exifLength) + (*sourcePtr - '0');
363 exifLength = newlength;
364 sourcePtr++;
365 if (sourcePtr == endPtr) {
366 IMAGE_LOGE("Unable to get Exif length: no digit content found");
367 return NULL;
368 }
369 }
370 sourcePtr++; // ignore the '\n' character
371 if (sourcePtr == endPtr) {
372 IMAGE_LOGE("Unable to get Exif length: Exif info not found");
373 return NULL;
374 }
375 *lengthOut = exifLength;
376 return sourcePtr;
377 }
378
ConvertAsciiToInt(const char * sourcePtr,size_t exifInfoLength,unsigned char * destPtr)379 int PngImageChunkUtils::ConvertAsciiToInt(const char *sourcePtr, size_t exifInfoLength, unsigned char *destPtr)
380 {
381 if (sourcePtr == nullptr || destPtr == nullptr) {
382 IMAGE_LOGE("The scrPointer or destPointer is not valid.");
383 return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
384 }
385
386 static const unsigned char hexAsciiToInt[ASCII_TO_HEX_MAP_SIZE] = {
387 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0,
390 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
391 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12,
392 13, 14, 15,
393 };
394
395 size_t sourceLength = exifInfoLength * 2;
396 size_t sourcePtrCount = 0;
397 for (size_t i = 0; i < sourceLength && sourcePtrCount < sourceLength; i++) {
398 while (sourcePtrCount < sourceLength &&
399 ((*sourcePtr < '0') || ((*sourcePtr > '9') && (*sourcePtr < 'a')) || (*sourcePtr > 'f'))) {
400 if (*sourcePtr == '\0') {
401 IMAGE_LOGE("Unexpected null character encountered while converting Exif ASCII string. "
402 "Position: %{public}zu, Expected length: %{public}zu",
403 i, sourceLength);
404 return ERR_IMAGE_SOURCE_DATA_INCOMPLETE;
405 }
406 sourcePtr++;
407 sourcePtrCount++;
408 }
409
410 if ((i % HEX_STRING_UNIT_SIZE) == 0) {
411 *destPtr = static_cast<unsigned char>(HEX_BASE * hexAsciiToInt[static_cast<size_t>(*sourcePtr++)]);
412 } else {
413 (*destPtr++) += hexAsciiToInt[static_cast<size_t>(*sourcePtr++)];
414 }
415 sourcePtrCount++;
416 }
417 return SUCCESS;
418 }
419
ConvertRawTextToExifInfo(const DataBuf & rawText)420 DataBuf PngImageChunkUtils::ConvertRawTextToExifInfo(const DataBuf &rawText)
421 {
422 if (rawText.Size() <= 1) {
423 IMAGE_LOGE("The size of the raw profile text is too small.");
424 return {};
425 }
426 const char *sourcePtr = reinterpret_cast<const char *>(rawText.CData(1));
427 const char *endPtr = reinterpret_cast<const char *>(rawText.CData(rawText.Size() - 1));
428
429 if (sourcePtr >= endPtr) {
430 IMAGE_LOGE("The source pointer is not valid.");
431 return {};
432 }
433 sourcePtr = StepOverNewLine(sourcePtr, endPtr);
434 if (sourcePtr == NULL) {
435 IMAGE_LOGE("Error encountered when stepping over new line in raw profile text.");
436 return {};
437 }
438
439 size_t exifInfoLength = 0;
440 sourcePtr = GetExifInfoLen(sourcePtr, &exifInfoLength, endPtr);
441 if (sourcePtr == NULL) {
442 IMAGE_LOGE("Error encountered when getting the length of the string in raw profile text.");
443 return {};
444 }
445
446 if ((exifInfoLength == 0) || (exifInfoLength > rawText.Size())) {
447 IMAGE_LOGE("Invalid text length in raw profile text.");
448 return {};
449 }
450
451 DataBuf exifInfo;
452 exifInfo.Resize(exifInfoLength);
453 if (exifInfo.Size() != exifInfoLength) {
454 IMAGE_LOGE("Unable to allocate memory for Exif information.");
455 return {};
456 }
457 if (exifInfo.Empty()) {
458 return exifInfo;
459 }
460 unsigned char *destPtr = exifInfo.Data();
461 if (sourcePtr + EXIF_INFO_LENGTH_TWO * exifInfoLength > endPtr) {
462 IMAGE_LOGE("Invalid text length in raw profile text, it will result in OOB.");
463 return {};
464 }
465 int ret = ConvertAsciiToInt(sourcePtr, exifInfoLength, destPtr);
466 if (ret != 0) {
467 IMAGE_LOGE("Error encountered when converting Exif string ASCII to integer.");
468 return {};
469 }
470
471 return exifInfo;
472 }
473 } // namespace Media
474 } // namespace OHOS
475