Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Four pre-existing bugs (with fixes): typeof check, dead test loop, unchecked ua.match destructures, rollup writeBundle race #139

@isamu

Description

@isamu

Four pre-existing bugs surfaced while modernizing the codebase

While doing a TypeScript / ESLint / strict-mode tightening pass on a fork of exifr (modernized-js/exifr) I bumped into four bugs that all exist in master (6cbf6e9) of this repo. They aren't regressions — they've been there for a while. Three of them are real runtime / correctness issues, one is a build race. Filing them as a single issue with proposed fixes, since each is a one-or-two-line change.

I am happy to send PRs against this repo if any of them look helpful — let me know.


1. typeof BigInt !== undefined is always true — src/util/BufferView-get64.mjs

Line 16:

} else if (typeof BigInt !== undefined) {

typeof X always returns a string ("undefined", "function", …) and is never the undefined value. The condition is always true. The intended check is comparing against the string 'undefined'.

The else { throwError(...) } branch on line 22 is therefore unreachable in practice. In any environment without BigInt (none of which this lib targets at runtime today, but the intent of the branch is clearly to fail fast), BigInt(part1) would throw a generic ReferenceError instead of the descriptive throwError(...) below.

Fix:

-	} else if (typeof BigInt !== undefined) {
+	} else if (typeof BigInt !== 'undefined') {

2. Test loop never executes — test/xmp-segment.spec.mjs

Line 282:

assert.lengthOf(actualSegStarts, 45)
for (let i = 0; i > actualSegStarts; i++) {
    const expected = expectedSegStarts[i]
    const actual   = actualSegStarts[i]
    assert.equal(actual, expected)
}

i > actualSegStarts is comparing a number to an array. JavaScript coerces the array to NaN (via [obj] -> string -> NaN for non-numeric arrays), so 0 > NaN is false on the first iteration and the loop body never runs.

The intent is i < actualSegStarts.length. Right now this it(...) block silently asserts nothing about the actual / expected segment-start contents — only the preceding assert.lengthOf(...) runs.

Fix:

-	for (let i = 0; i > actualSegStarts; i++) {
+	for (let i = 0; i < actualSegStarts.length; i++) {

3. ua.match(...) result destructured without null check — src/highlevel/orientation.mjs

Three sites: lines 48, 52, 55.

} else if (ua.includes('OS X 10')) {
    let [, version] = ua.match(/OS X 10[_.](\d+)/)
    rotateCanvas = rotateCss = Number(version) < 15
}
// …
if (ua.includes('Chrome/')) {
    let [, version] = ua.match(/Chrome\/(\d+)/)
    rotateCanvas = rotateCss = Number(version) < 81
} else if (ua.includes('Firefox/')) {
    let [, version] = ua.match(/Firefox\/(\d+)/)
    rotateCanvas = rotateCss = Number(version) < 77
}

ua.includes('Chrome/') only confirms the substring is present. The matching regex (/Chrome\/(\d+)/) requires at least one digit after Chrome/. UA strings like …Chrome/<flag> (with no digits or with letter-prefix flags) would pass includes but fail match, returning null. Then the destructure let [, version] = null throws TypeError: Cannot destructure property '1' of 'null' as it is null.

In practice this can't be triggered by mainstream browsers, but it's brittle defensive code, and the OS X 10 case in particular can vary on iOS web-views.

Fix (apply to all three branches):

 if (ua.includes('Chrome/')) {
-    let [, version] = ua.match(/Chrome\/(\d+)/)
-    rotateCanvas = rotateCss = Number(version) < 81
+    const match = ua.match(/Chrome\/(\d+)/)
+    if (match) rotateCanvas = rotateCss = Number(match[1]) < 81
 } else if (ua.includes('Firefox/')) {
-    let [, version] = ua.match(/Firefox\/(\d+)/)
-    rotateCanvas = rotateCss = Number(version) < 77
+    const match = ua.match(/Firefox\/(\d+)/)
+    if (match) rotateCanvas = rotateCss = Number(match[1]) < 77
 }

4. rollup cloneCjsAndMjsToJs writeBundle hook doesn't await fs.copyFilerollup.config.js

function cloneCjsAndMjsToJs() {
    return {
        writeBundle(bundle) {
            let newFileName = bundle.file.replace('.cjs', '.js').replace('.mjs', '.js')
            fs.copyFile(bundle.file, newFileName)
        }
    }
}

fs.copyFile (from fs.promises) returns a Promise that's never returned to rollup, so the build can finish (and the rollup process can exit) while the .js mirror copies are still being written. In practice this hasn't been observed because the disk write completes before Node exits, but it's a race — and it's the entrypoint that package.json#main points to (./dist/full.umd.js is the mirror), so a flaky build can publish a tarball without the mirror.

Fix:

 function cloneCjsAndMjsToJs() {
     return {
         writeBundle(bundle) {
             let newFileName = bundle.file.replace('.cjs', '.js').replace('.mjs', '.js')
-            fs.copyFile(bundle.file, newFileName)
+            return fs.copyFile(bundle.file, newFileName)
         }
     }
 }

These all came up while doing a strict-mode TypeScript / ESLint pass — they're good demonstrations of why those tools earn their keep, even on a codebase that's been working in production for years. Thanks for the original library; it's lovely to work with.


About the fork that surfaced these

@modernized/exifr (modernized-js/exifr) is a modernization-only fork of [email protected]. Public API is byte-identical (the published index.d.ts is unchanged from upstream); the scope is purely toolchain and runtime envelope:

  • TypeScript sources, Node 22.18+ runtime, ES2020+ browser targets
  • Drops IE11 / pre-Chromium-Edge support and the Babel / IE-polyfill pipeline (~50 % smaller bundles, ~60 % faster build)
  • mocha + chainode:test + node:assert
  • Travis → GitHub Actions (Linux + macOS + Windows × Node 22)
  • ESLint 10 (flat config) + Prettier + @arethetypeswrong/cli package-types check in CI
  • Bundle outputs preserved: mini / lite / full × esm.mjs / esm.js / umd.cjs / umd.js (legacy UMD dropped)

Not a competitor — the README is explicit that all credit goes to you and the existing contributors, and users who need IE / older Node should keep using the upstream exifr v7.x line. Just a place to land the modernization without bothering you.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions