@@ -8,109 +8,78 @@ import { pipeline } from 'node:stream/promises';
88import { fileURLToPath } from 'node:url';
99import { parseArgs } from 'node:util';
1010
11- // Constants for NSS release metadata.
12- const kNSSVersion = 'version';
13- const kNSSDate = 'date';
14- const kFirefoxVersion = 'firefoxVersion';
15- const kFirefoxDate = 'firefoxDate';
16-
1711const __filename = fileURLToPath(import.meta.url);
18- const now = new Date();
19-
20- const formatDate = (d) => {
21- const iso = d.toISOString();
22- return iso.substring(0, iso.indexOf('T'));
23- };
2412
2513const getCertdataURL = (version) => {
2614 const tag = `NSS_${version.replaceAll('.', '_')}_RTM`;
27- const certdataURL = `https://hg.mozilla.org/projects /nss/raw-file /${tag}/lib/ckfw/builtins/certdata.txt`;
15+ const certdataURL = `https://raw.githubusercontent.com/nss-dev /nss/refs/tags /${tag}/lib/ckfw/builtins/certdata.txt`;
2816 return certdataURL;
2917};
3018
31- const normalizeTD = (text = '') => {
32- // Remove whitespace and any HTML tags.
33- return text?.trim().replace(/<.*?>/g, '');
34- };
35- const getReleases = (text) => {
36- const releases = [];
37- const tableRE = /<table [^>]+>([\S\s]*?)<\/table>/g;
38- const tableRowRE = /<tr ?[^>]*>([\S\s]*?)<\/tr>/g;
39- const tableHeaderRE = /<th ?[^>]*>([\S\s]*?)<\/th>/g;
40- const tableDataRE = /<td ?[^>]*>([\S\s]*?)<\/td>/g;
41- for (const table of text.matchAll(tableRE)) {
42- const columns = {};
43- const matches = table[1].matchAll(tableRowRE);
44- // First row has the table header.
45- let row = matches.next();
46- if (row.done) {
47- continue;
48- }
49- const headers = Array.from(row.value[1].matchAll(tableHeaderRE), (m) => m[1]);
50- if (headers.length > 0) {
51- for (let i = 0; i < headers.length; i++) {
52- if (/NSS version/i.test(headers[i])) {
53- columns[kNSSVersion] = i;
54- } else if (/Release.*from branch/i.test(headers[i])) {
55- columns[kNSSDate] = i;
56- } else if (/Firefox version/i.test(headers[i])) {
57- columns[kFirefoxVersion] = i;
58- } else if (/Firefox release date/i.test(headers[i])) {
59- columns[kFirefoxDate] = i;
60- }
61- }
62- }
63- // Filter out "NSS Certificate bugs" table.
64- if (columns[kNSSDate] === undefined) {
65- continue;
66- }
67- // Scrape releases.
68- row = matches.next();
69- while (!row.done) {
70- const cells = Array.from(row.value[1].matchAll(tableDataRE), (m) => m[1]);
71- const release = {};
72- release[kNSSVersion] = normalizeTD(cells[columns[kNSSVersion]]);
73- release[kNSSDate] = new Date(normalizeTD(cells[columns[kNSSDate]]));
74- release[kFirefoxVersion] = normalizeTD(cells[columns[kFirefoxVersion]]);
75- release[kFirefoxDate] = new Date(normalizeTD(cells[columns[kFirefoxDate]]));
76- releases.push(release);
77- row = matches.next();
78- }
19+ const getFirefoxReleases = async (everything = false) => {
20+ const releaseDataURL = `https://nucleus.mozilla.org/rna/all-releases.json${everything ? '?all=true' : ''}`;
21+ if (values.verbose) {
22+ console.log(`Fetching Firefox release data from ${releaseDataURL}.`);
23+ }
24+ const releaseData = await fetch(releaseDataURL);
25+ if (!releaseData.ok) {
26+ console.error(`Failed to fetch ${releaseDataURL}: ${releaseData.status}: ${releaseData.statusText}.`);
27+ process.exit(-1);
7928 }
80- return releases;
29+ return (await releaseData.json()).filter((release) => {
30+ // We're only interested in public releases of Firefox.
31+ return (release.product === 'Firefox' && release.channel === 'Release' && release.is_public === true);
32+ }).sort((a, b) => {
33+ // Sort results by release date.
34+ return new Date(b.release_date) - new Date(a.release_date);
35+ });
8136};
8237
83- const getLatestVersion = async (releases) => {
84- const arrayNumberSortDescending = (x, y, i) => {
85- if (x[i] === undefined && y[i] === undefined) {
86- return 0;
87- } else if (x[i] === y[i]) {
88- return arrayNumberSortDescending(x, y, i + 1);
89- }
90- return (y[i] ?? 0) - (x[i] ?? 0);
91- };
92- const extractVersion = (t) => {
93- return t[kNSSVersion].split('.').map((n) => parseInt(n));
94- };
95- const releaseSorter = (x, y) => {
96- return arrayNumberSortDescending(extractVersion(x), extractVersion(y), 0);
97- };
98- // Return the most recent certadata.txt that exists on the server.
99- const sortedReleases = releases.sort(releaseSorter).filter(pastRelease);
100- for (const candidate of sortedReleases) {
101- const candidateURL = getCertdataURL(candidate[kNSSVersion]);
102- if (values.verbose) {
103- console.log(`Trying ${candidateURL}`);
38+ const getFirefoxRelease = async (version) => {
39+ let releases = await getFirefoxReleases();
40+ let found;
41+ if (version === undefined) {
42+ // No version specified. Find the most recent.
43+ if (releases.length > 0) {
44+ found = releases[0];
45+ } else {
46+ if (values.verbose) {
47+ console.log('Unable to find release data for Firefox. Searching full release data.');
48+ }
49+ releases = await getFirefoxReleases(true);
50+ found = releases[0];
10451 }
105- const response = await fetch(candidateURL, { method: 'HEAD' });
106- if (response.ok) {
107- return candidate[kNSSVersion];
52+ } else {
53+ // Search for the specified release.
54+ found = releases.find((release) => release.version === version);
55+ if (found === undefined) {
56+ if (values.verbose) {
57+ console.log(`Unable to find release data for Firefox ${version}. Searching full release data.`);
58+ }
59+ releases = await getFirefoxReleases(true);
60+ found = releases.find((release) => release.version === version);
10861 }
10962 }
63+ return found;
11064};
11165
112- const pastRelease = (r) => {
113- return r[kNSSDate] < now;
66+ const getNSSVersion = async (release) => {
67+ const latestFirefox = release.version;
68+ const firefoxTag = `FIREFOX_${latestFirefox.replace('.', '_')}_RELEASE`;
69+ const tagInfoURL = `https://hg.mozilla.org/releases/mozilla-release/raw-file/${firefoxTag}/security/nss/TAG-INFO`;
70+ if (values.verbose) {
71+ console.log(`Fetching NSS tag from ${tagInfoURL}.`);
72+ }
73+ const tagInfo = await fetch(tagInfoURL);
74+ if (!tagInfo.ok) {
75+ console.error(`Failed to fetch ${tagInfoURL}: ${tagInfo.status}: ${tagInfo.statusText}`);
76+ }
77+ const tag = await tagInfo.text();
78+ if (values.verbose) {
79+ console.log(`Found tag ${tag}.`);
80+ }
81+ // Tag will be of form `NSS_x_y_RTM`. Convert to `x.y`.
82+ return tag.split('_').slice(1, -1).join('.');
11483};
11584
11685const options = {
@@ -135,9 +104,9 @@ const {
135104});
136105
137106if (values.help) {
138- console.log(`Usage: ${basename(__filename)} [OPTION]... [VERSION ]...`);
107+ console.log(`Usage: ${basename(__filename)} [OPTION]... [RELEASE ]...`);
139108 console.log();
140- console.log('Updates certdata.txt to NSS VERSION ( most recent release by default ).');
109+ console.log('Updates certdata.txt to NSS version contained in Firefox RELEASE (default: most recent release).');
141110 console.log('');
142111 console.log(' -f, --file=FILE writes a commit message reflecting the change to the');
143112 console.log(' specified FILE');
@@ -146,29 +115,11 @@ if (values.help) {
146115 process.exit(0);
147116}
148117
149- const scheduleURL = 'https://wiki.mozilla.org/NSS:Release_Versions';
150- if (values.verbose) {
151- console.log(`Fetching NSS release schedule from ${scheduleURL}`);
152- }
153- const schedule = await fetch(scheduleURL);
154- if (!schedule.ok) {
155- console.error(`Failed to fetch ${scheduleURL}: ${schedule.status}: ${schedule.statusText}`);
156- process.exit(-1);
157- }
158- const scheduleText = await schedule.text();
159- const nssReleases = getReleases(scheduleText);
160-
118+ const firefoxRelease = await getFirefoxRelease(positionals[0]);
161119// Retrieve metadata for the NSS release being updated to.
162- const version = positionals[0] ?? await getLatestVersion(nssReleases);
163- const release = nssReleases.find((r) => {
164- return new RegExp(`^${version.replace('.', '\\.')}\\b`).test(r[kNSSVersion]);
165- });
166- if (!pastRelease(release)) {
167- console.warn(`Warning: NSS ${version} is not due to be released until ${formatDate(release[kNSSDate])}`);
168- }
120+ const version = await getNSSVersion(firefoxRelease);
169121if (values.verbose) {
170- console.log('Found NSS version:');
171- console.log(release);
122+ console.log(`Updating to NSS version ${version}`);
172123}
173124
174125// Fetch certdata.txt and overwrite the local copy.
@@ -213,14 +164,15 @@ const added = [ ...diff.matchAll(certsAddedRE) ].map((m) => m[1]);
213164const removed = [ ...diff.matchAll(certsRemovedRE) ].map((m) => m[1]);
214165
215166const commitMsg = [
216- `crypto: update root certificates to NSS ${release[kNSSVersion] }`,
167+ `crypto: update root certificates to NSS ${version }`,
217168 '',
218- `This is the certdata.txt[0] from NSS ${release[kNSSVersion]}, released on ${formatDate(release[kNSSDate])}.`,
219- '',
220- `This is the version of NSS that ${release[kFirefoxDate] < now ? 'shipped' : 'will ship'} in Firefox ${release[kFirefoxVersion]} on`,
221- `${formatDate(release[kFirefoxDate])}.`,
169+ `This is the certdata.txt[0] from NSS ${version}.`,
222170 '',
223171];
172+ if (firefoxRelease) {
173+ commitMsg.push(`This is the version of NSS that shipped in Firefox ${firefoxRelease.version} on ${firefoxRelease.release_date}.`);
174+ commitMsg.push('');
175+ }
224176if (added.length > 0) {
225177 commitMsg.push('Certificates added:');
226178 commitMsg.push(...added.map((cert) => `- ${cert}`));
@@ -234,7 +186,7 @@ if (removed.length > 0) {
234186commitMsg.push(`[0] ${certdataURL}`);
235187const delimiter = randomUUID();
236188const properties = [
237- `NEW_VERSION=${release[kNSSVersion] }`,
189+ `NEW_VERSION=${version }`,
238190 `COMMIT_MSG<<${delimiter}`,
239191 ...commitMsg,
240192 delimiter,
0 commit comments