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