From aacb09a0e6fb9aa96436db4f36169c1366c0891d Mon Sep 17 00:00:00 2001 From: Philip <17368112+vHeemstra@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:56:27 +0200 Subject: [PATCH] (fix/perf) Improvements * Fix: Catch false-positive where `acTL` is present, but no following `IDAT` * Perf: Refactored and simplified code --- src/index.ts | 111 +++++++++++++-------------------------------------- 1 file changed, 27 insertions(+), 84 deletions(-) diff --git a/src/index.ts b/src/index.ts index 430361a..2d94702 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,76 +1,3 @@ -// import { Buffer } from 'node:buffer' - -/** - * Returns the index of the first occurrence of a sequence in an typed array, or -1 if it is not present. - * - * Works similar to `Array.prototype.indexOf()`, but it searches for a sequence of array values (bytes). - * The bytes in the `haystack` array are decoded (UTF-8) and then used to search for `needle`. - * - * @param buffer - * Array to search in. - * - * @param searchSequence - * The value to locate in the array. - * - * @param fromIndex - * The array index at which to begin the search. - * - * @param stopSequence - * Byte sequence to stop search - * - * @returns boolean - * Whether the array holds Animated PNG data. - */ -function hasSequence( - buffer: Buffer | Uint8Array, - searchSequence: Uint8Array, - fromIndex: number, - stopSequence: Uint8Array, -): boolean { - function validateSequence(sequence: Uint8Array): void { - if (!sequence.length) { - throw new Error('Sequence is empty') - } - - // Search only unique symbols to simplify the algorithm - if (new Set(sequence).size !== sequence.length) { - throw new Error('Sequence must consist of unique symbols') - } - } - - validateSequence(searchSequence) - validateSequence(stopSequence) - - if (fromIndex >= buffer.length) { - return false - } - buffer = buffer.subarray(fromIndex) - - let matchSearchIndex = 0 - let matchStopIndex = 0 - for (let i = 0; i < buffer.length; i++) { - if (buffer[i] === searchSequence[matchSearchIndex]) { - matchSearchIndex++ - if (matchSearchIndex === searchSequence.length) { - return true - } - } else { - matchSearchIndex = 0 - } - - if (buffer[i] === stopSequence[matchStopIndex]) { - matchStopIndex++ - if (matchStopIndex === stopSequence.length) { - return false - } - } else { - matchStopIndex = 0 - } - } - - return false -} - const encoder = new TextEncoder() const sequences = { animationControlChunk: encoder.encode('acTL'), @@ -103,17 +30,33 @@ export default function isApng(buffer: Buffer | Uint8Array): boolean { return false } - // APNGs have an animation control chunk ('acTL') preceding the IDATs. + // APNGs have an animation control chunk ('acTL') preceding any IDAT(s). // See: https://en.wikipedia.org/wiki/APNG#File_format - return hasSequence( - buffer, - sequences.animationControlChunk, - 8, - sequences.imageDataChunk, - ) -} -// globalThis.isApng = isApng + buffer = buffer.subarray(8) + + let foundFirst = false + let firstIndex = 0 + let secondIndex = 0 + for (let i = 0; i < buffer.length; i++) { + if (buffer[i] === sequences.animationControlChunk[firstIndex]) { + firstIndex++ + if (firstIndex === sequences.animationControlChunk.length) { + foundFirst = true + } + } else { + firstIndex = 0 + } + + if (buffer[i] === sequences.imageDataChunk[secondIndex]) { + secondIndex++ + if (secondIndex === sequences.imageDataChunk.length) { + return foundFirst + } + } else { + secondIndex = 0 + } + } -// const idatIdx = buffer.indexOf('IDAT') -// const actlIdx = buffer.indexOf('acTL') + return false +}