Skip to content

deno cache --frozen rejects its own lockfile for an unchanged Latin-1 remote module #37003

Description

@YieldRay

Summary

Deno 2.9.7 successfully generates a lockfile for a remote JavaScript module served with Content-Type: application/javascript; charset=iso-8859-1, but subsequently rejects that same module with Integrity check failed when running deno cache --frozen.

The URL, response body, response headers, and lockfile are unchanged between runs.

In this reproduction, the checksum written to the lockfile matches the SHA-256 of the decoded source encoded as UTF-8. However, the “Actual” checksum reported during verification matches the SHA-256 of the original Latin-1 response bytes.

This indicates an inconsistency in the byte representation used for lockfile generation and verification.

Environment

  • Deno: 2.9.7
  • OS: macOS
  • Lockfile format: v5

Reproduction

Run the following command with Deno 2.9.7.

deno eval --no-config --no-lock '
const source = "export default \"café\";\n";
const bytes = Uint8Array.from(source, c => c.charCodeAt(0));
const dir = await Deno.makeTempDir({ prefix: "deno-lock-repro-" });

const server = Deno.serve(
  { hostname: "127.0.0.1", port: 0, onListen() {} },
  () => new Response(bytes, {
    headers: {
      "Content-Type": "application/javascript; charset=iso-8859-1",
    },
  }),
);

const url = `http://127.0.0.1:${server.addr.port}/mod.js`;
const lockPath = `${dir}/deno.lock`;

const sha256 = async data => Array.from(
  new Uint8Array(await crypto.subtle.digest("SHA-256", data)),
  b => b.toString(16).padStart(2, "0"),
).join("");

try {
  console.log(`Deno ${Deno.version.deno}`);

  for (const frozen of [false, true]) {
    const result = await new Deno.Command(Deno.execPath(), {
      args: [
        "cache",
        "--no-config",
        `--lock=${lockPath}`,
        "--allow-import=127.0.0.1",
        ...(frozen ? ["--frozen"] : []),
        url,
      ],
      cwd: dir,
      env: {
        DENO_DIR: `${dir}/cache`,
        NO_COLOR: "1",
      },
    }).output();

    console.log(
      `${frozen ? "Second run (--frozen)" : "First run"}: exit ${result.code}`,
    );
    console.log(new TextDecoder().decode(result.stderr));
  }

  const lock = JSON.parse(await Deno.readTextFile(lockPath));
  console.log("Lock hash:         ", lock.remote[url]);
  console.log("Raw Latin-1 hash:  ", await sha256(bytes));
  console.log(
    "Decoded UTF-8 hash:",
    await sha256(new TextEncoder().encode(source)),
  );
} finally {
  await server.shutdown();
  await Deno.remove(dir, { recursive: true });
}
'

Expected behavior

Both runs should succeed. The second run should validate the unchanged module against the lockfile generated by the first run, without modifying the lockfile.

Lockfile generation and verification should hash the same byte representation.

Actual behavior

  • The first deno cache run exits with code 0 and generates the lockfile.
  • The second deno cache --frozen run exits with code 10 and reports Integrity check failed.
  • The checksum in lock.remote[url] equals the printed “Decoded UTF-8 hash”.
  • The error’s “Actual” checksum equals the printed “Raw Latin-1 hash”.
  • Those two hashes differ.

Analysis

The observed hashes indicate that these paths use different inputs:

  • Lockfile generation: original response bytes → charset decoding → UTF-8 encoding of the decoded source → SHA-256.
  • Subsequent verification: original response bytes → SHA-256 → comparison with the stored checksum.

So i think either deno should just use raw module bytes to calculate hash or always use the decoded text.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions