Skip to content

Commit

Permalink
(fix) Search acTL only among chunk types
Browse files Browse the repository at this point in the history
Byte sequence `acTL` can be encountered in other parts of PNG,
that are not chunk type (e.g. in chunk data or crc),
so we should check only chunk types to avoid false positive detection.
  • Loading branch information
madtisa committed Aug 12, 2024
1 parent befa2f2 commit 55bdfa7
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 35 deletions.
Binary file added src/images/staticWithMetadata.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 32 additions & 12 deletions src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,43 +23,63 @@ test('returns false on JPG', (t) => {
t.false(check(dir + '/images/static.jpg'))
})

test('returns false on static PNG with `acTL` text in metadata', (t) => {
t.false(check(dir + '/images/staticWithMetadata.png'))
})

test('returns true when IDAT follows acTL', (t) => {
t.true(
isApng(
new Uint8Array([
// PNG header
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
// acTL
// Chunk length: 0
0x00, 0x00, 0x00, 0x00,
// Chunk type: acTL
0x61, 0x63, 0x54, 0x4c,
// IDAT
// Chunk CRC
0x00, 0x00, 0x00, 0x00,
// Chunk length: 0
0x00, 0x00, 0x00, 0x00,
// Chunk type: IDAT
0x49, 0x44, 0x41, 0x54,
]),
),
)
})

test('returns false when IDAT precedes acTL', (t) => {
test('returns false when acTL is not a chunk type', (t) => {
t.false(
isApng(
new Uint8Array([
// PNG header
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
// IDAT
0x49, 0x44, 0x41, 0x54,
// acTL
// Chunk length: 4
0x00, 0x00, 0x00, 0x04,
// Chunk type: any
0x00, 0x00, 0x00, 0x01,
// Chunk data: acTL
0x61, 0x63, 0x54, 0x4c,
]),
),
)
})

test('returns false when missing IDAT', (t) => {
test('returns false when IDAT precedes acTL', (t) => {
t.false(
isApng(
new Uint8Array([
// PNG header
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
// acTL
// Chunk length: 0
0x00, 0x00, 0x00, 0x00,
// Chunk type: IDAT
0x49, 0x44, 0x41, 0x54,
// Chunk CRC
0x00, 0x00, 0x00, 0x00,
// Chunk length: 0
0x00, 0x00, 0x00, 0x00,
// Chunk type: acTL
0x61, 0x63, 0x54, 0x4c,
]),
),
Expand All @@ -72,10 +92,10 @@ test('chunks should be found when preceded by a partial of themselves', (t) => {
new Uint8Array([
// PNG header
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
// a acTL
0x61, 0x61, 0x63, 0x54, 0x4c,
// I IDAT
0x49, 0x49, 0x44, 0x41, 0x54,
// Chunk length: a
0x00, 0x00, 0x00, 0x61,
// Chunk type: acTL
0x61, 0x63, 0x54, 0x4c,
]),
),
)
Expand Down
69 changes: 46 additions & 23 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
function convertToInt(bytes: Uint8Array) {
return bytes.reduce((value, byte) => (value << 8) + byte)
}

function isEqual(first, second) {
return (
first[0] === second[0] &&
first[1] === second[1] &&
first[2] === second[2] &&
first[3] === second[3]
)
}

const encoder = new TextEncoder()
const sequences = {
animationControlChunk: encoder.encode('acTL'),
imageDataChunk: encoder.encode('IDAT'),
const chunkTypes = {
animationControl: encoder.encode('acTL'),
imageData: encoder.encode('IDAT'),
}

/**
* @see http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html
*/
const headerSizes = {
/** Number of bytes reserved for PNG signature */
SIGNATURE: 8,
/** Number of bytes reserved for chunk type */
LENGTH: 4,
/** Number of bytes reserved for chunk type */
TYPE: 4,
/** Number of bytes reserved for chunk CRC */
CRC: 4,
}

export default function isApng(buffer: Buffer | Uint8Array): boolean {
Expand Down Expand Up @@ -33,32 +60,28 @@ export default function isApng(buffer: Buffer | Uint8Array): boolean {
// APNGs have an animation control chunk (acTL) preceding any IDAT(s).
// See: https://en.wikipedia.org/wiki/APNG#File_format

buffer = buffer.subarray(8)
buffer = buffer.subarray(headerSizes.SIGNATURE)

let firstIndex = 0
let secondIndex = 0
for (let i = 0; i < buffer.length; i++) {
if (buffer[i] !== sequences.animationControlChunk[firstIndex]) {
firstIndex = 0
}
const minBufferSize = headerSizes.LENGTH + headerSizes.TYPE
while (buffer.length >= minBufferSize) {
const chunkLength = convertToInt(buffer.subarray(0, headerSizes.LENGTH))
const chunkType = buffer.subarray(
headerSizes.LENGTH,
headerSizes.LENGTH + headerSizes.TYPE,
)

if (buffer[i] === sequences.animationControlChunk[firstIndex]) {
firstIndex++
if (firstIndex === sequences.animationControlChunk.length) {
return true
}
if (isEqual(chunkType, chunkTypes.animationControl)) {
return true
}

if (buffer[i] !== sequences.imageDataChunk[secondIndex]) {
secondIndex = 0
if (isEqual(chunkType, chunkTypes.imageData)) {
return false
}

if (buffer[i] === sequences.imageDataChunk[secondIndex]) {
secondIndex++
if (secondIndex === sequences.imageDataChunk.length) {
return false
}
}
const nextChunkPosition =
headerSizes.LENGTH + headerSizes.TYPE + chunkLength + headerSizes.CRC

buffer = buffer.subarray(nextChunkPosition)
}

return false
Expand Down

0 comments on commit 55bdfa7

Please sign in to comment.