Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions knife
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ function cmd_update_node_archives () {
| sort_by(.date) | reverse | .[0].version
')
latest_nov=$(echo "$latest_version" | sed 's/^v//')
if [[ ! "$latest_nov" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid Node.js version from release index: $latest_version" >&2
exit 1
fi
versions+=("$latest_nov")
done

Expand Down
93 changes: 78 additions & 15 deletions knife.d/update_node_archives.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,52 @@ const crypto = require("crypto");
const https = require("https");
const fs = require("fs");

if (process.argv.length < 3) {
console.error("Usage: node nodeChecksum.js <nodejs_version>");
process.exit(1);
}

const versions = process.argv[2].split(",");
let versions = [];
const architectures = ["amd64", "arm64", "arm", "ppc64le", "s390x"];
const NODE_VERSION_RE = /^[0-9]+\.[0-9]+\.[0-9]+$/;
const ARCHIVE_SUFFIX_RE = /^(x64|arm64|armv7l|ppc64le|s390x)$/;
const DISTROLESS_ARCH_RE = /^(amd64|arm64|arm|ppc64le|s390x)$/;
const SHA256_RE = /^[0-9a-f]{64}$/;

const nodeVersions = {};

const starlarkString = (value) => {
return `"${String(value)
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/\t/g, "\\t")}"`;
};

const validateNodeVersion = (version) => {
if (!NODE_VERSION_RE.test(version)) {
throw new Error(`Invalid Node.js version: ${version}`);
}
return version;
};

const validateArchiveSuffix = (suffix) => {
if (!ARCHIVE_SUFFIX_RE.test(suffix)) {
throw new Error(`Invalid Node.js archive suffix: ${suffix}`);
}
return suffix;
};

const validateDistrolessArch = (arch) => {
if (!DISTROLESS_ARCH_RE.test(arch)) {
throw new Error(`Invalid distroless architecture: ${arch}`);
}
return arch;
};

const validateSha256 = (sha256) => {
if (!SHA256_RE.test(sha256)) {
throw new Error(`Invalid SHA256 checksum: ${sha256}`);
}
return sha256;
};

const calculateChecksum = (url) => {
return new Promise((resolve, reject) => {
https
Expand All @@ -33,6 +69,7 @@ const calculateChecksum = (url) => {

const fetchChecksums = async () => {
for (const nodeVersion of versions) {
validateNodeVersion(nodeVersion);
const major = parseInt(nodeVersion.split(".")[0]);
nodeVersions[nodeVersion] = {};
await Promise.all(
Expand All @@ -46,6 +83,7 @@ const fetchChecksums = async () => {
} else if (key === "arm") {
arch = "armv7l";
}
validateArchiveSuffix(arch);
const url = `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-${arch}.tar.gz`;
try {
const checksum = await calculateChecksum(url);
Expand All @@ -61,7 +99,14 @@ const fetchChecksums = async () => {
}
};

fetchChecksums().then(() => {
const main = async () => {
if (process.argv.length < 3) {
console.error("Usage: node nodeChecksum.js <nodejs_version>");
process.exit(1);
}
versions = process.argv[2].split(",");
await fetchChecksums();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If fetchChecksums fails to retrieve the checksum for any architecture (e.g., due to a network error or a 404 response), it logs the error but does not throw. This leaves nodeVersions[nodeVersion][key] undefined, which subsequently causes a silent TypeError when trying to access arch.suffix on line 219.

To make the script robust and fail-fast with a clear error message, we should validate that all expected version/architecture combinations were successfully fetched immediately after calling fetchChecksums().

  await fetchChecksums();
  for (const version of versions) {
    for (const arch of architectures) {
      const major = parseInt(version.split(".")[0]);
      if (major > 22 && arch === "arm") {
        continue;
      }
      if (!nodeVersions[version] || !nodeVersions[version][arch]) {
        throw new Error("Failed to fetch checksum for Node.js version " + version + " on " + arch);
      }
    }
  }


let nodeArchives = `"node"

BUILD_TMPL = """\\
Expand Down Expand Up @@ -172,16 +217,19 @@ def _node_impl(module_ctx):
}
const arch = nodeVersions[nodeVersion][key];
const url = `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-${arch.suffix}.tar.gz`;
validateDistrolessArch(key);
validateArchiveSuffix(arch.suffix);
validateSha256(arch.checksum);

nodeArchives += "\n";
nodeArchives += `
node_archive(
name = "nodejs${major}_${key}",
sha256 = "${arch.checksum}",
strip_prefix = "node-v${nodeVersion}-linux-${arch.suffix}/",
urls = ["${url}"],
version = "${nodeVersion}",
architecture = "${key}",
name = ${starlarkString(`nodejs${major}_${key}`)},
sha256 = ${starlarkString(arch.checksum)},
strip_prefix = ${starlarkString(`node-v${nodeVersion}-linux-${arch.suffix}/`)},
urls = [${starlarkString(url)}],
version = ${starlarkString(nodeVersion)},
architecture = ${starlarkString(key)},
control = "//nodejs:control",
)`;
}
Expand Down Expand Up @@ -212,7 +260,7 @@ commandTests:
continue;
}
nodeArchives += `
"nodejs${major}_${arch}",`;
${starlarkString(`nodejs${major}_${arch}`)},`;
}
}

Expand All @@ -237,4 +285,19 @@ node = module_extension(
console.error(err);
}
});
Comment on lines 285 to 287

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The file writing operation fs.writeFile is asynchronous and its callback only logs the error without propagating it or exiting with a non-zero code. Furthermore, because main is an async function but does not await the file writing, the function resolves immediately, and the process could exit before the file is fully written or fail silently on write errors.

Since this is a build/update script, using synchronous file writing (fs.writeFileSync) is much simpler and safer as it will naturally throw errors and cause the process to exit with a non-zero code.

Consider refactoring the file writing to use fs.writeFileSync:

  fs.writeFileSync("private/extensions/node.bzl", nodeArchives);

Similarly, the test data file writing on line 245 should also be refactored to fs.writeFileSync for the same reasons.

});
};

if (require.main === module) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}

module.exports = {
starlarkString,
validateArchiveSuffix,
validateDistrolessArch,
validateNodeVersion,
validateSha256,
};
38 changes: 38 additions & 0 deletions knife.d/update_node_archives_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env node
const assert = require("node:assert/strict");

const {
starlarkString,
validateArchiveSuffix,
validateDistrolessArch,
validateNodeVersion,
validateSha256,
} = require("./update_node_archives.js");

assert.equal(validateNodeVersion("26.2.0"), "26.2.0");
assert.throws(
() => validateNodeVersion('26.2.0" + "bad'),
/Invalid Node.js version/
);
assert.throws(() => validateNodeVersion("v26.2.0"), /Invalid Node.js version/);
assert.throws(() => validateNodeVersion("26.2.0\n26.2.1"), /Invalid Node.js version/);

assert.equal(validateArchiveSuffix("x64"), "x64");
assert.throws(() => validateArchiveSuffix('x64" + fail("bad") + "'), /Invalid Node.js archive suffix/);

assert.equal(validateDistrolessArch("amd64"), "amd64");
assert.throws(() => validateDistrolessArch("amd64\nbad"), /Invalid distroless architecture/);

assert.equal(
validateSha256("a".repeat(64)),
"a".repeat(64)
);
assert.throws(() => validateSha256("not-a-sha"), /Invalid SHA256 checksum/);

assert.equal(
starlarkString('26.2.0" + "bad'),
'"26.2.0\\" + \\"bad"'
);
assert.equal(starlarkString("line1\nline2"), '"line1\\nline2"');

console.log("update_node_archives validation tests passed");