@@ -8,109 +8,78 @@ import { pipeline } from 'node:stream/promises'; |
8 | 8 | import { fileURLToPath } from 'node:url'; |
9 | 9 | import { parseArgs } from 'node:util'; |
10 | 10 |
|
11 | | -// Constants for NSS release metadata. |
12 | | -const kNSSVersion = 'version'; |
13 | | -const kNSSDate = 'date'; |
14 | | -const kFirefoxVersion = 'firefoxVersion'; |
15 | | -const kFirefoxDate = 'firefoxDate'; |
16 | | - |
17 | 11 | const __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 | | -}; |
24 | 12 |
|
25 | 13 | const getCertdataURL = (version) => { |
26 | 14 | 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`; |
28 | 16 | return certdataURL; |
29 | 17 | }; |
30 | 18 |
|
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); |
79 | 28 | } |
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 | + }); |
81 | 36 | }; |
82 | 37 |
|
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]; |
104 | 51 | } |
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); |
108 | 61 | } |
109 | 62 | } |
| 63 | + return found; |
110 | 64 | }; |
111 | 65 |
|
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('.'); |
114 | 83 | }; |
115 | 84 |
|
116 | 85 | const options = { |
@@ -135,9 +104,9 @@ const { |
135 | 104 | }); |
136 | 105 |
|
137 | 106 | if (values.help) { |
138 | | - console.log(`Usage: ${basename(__filename)} [OPTION]... [VERSION]...`); |
| 107 | + console.log(`Usage: ${basename(__filename)} [OPTION]... [RELEASE]...`); |
139 | 108 | 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).'); |
141 | 110 | console.log(''); |
142 | 111 | console.log(' -f, --file=FILE writes a commit message reflecting the change to the'); |
143 | 112 | console.log(' specified FILE'); |
@@ -146,29 +115,11 @@ if (values.help) { |
146 | 115 | process.exit(0); |
147 | 116 | } |
148 | 117 |
|
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]); |
161 | 119 | // 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); |
169 | 121 | if (values.verbose) { |
170 | | - console.log('Found NSS version:'); |
171 | | - console.log(release); |
| 122 | + console.log(`Updating to NSS version ${version}`); |
172 | 123 | } |
173 | 124 |
|
174 | 125 | // Fetch certdata.txt and overwrite the local copy. |
@@ -213,14 +164,15 @@ const added = [ ...diff.matchAll(certsAddedRE) ].map((m) => m[1]); |
213 | 164 | const removed = [ ...diff.matchAll(certsRemovedRE) ].map((m) => m[1]); |
214 | 165 |
|
215 | 166 | const commitMsg = [ |
216 | | - `crypto: update root certificates to NSS ${release[kNSSVersion]}`, |
| 167 | + `crypto: update root certificates to NSS ${version}`, |
217 | 168 | '', |
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}.`, |
222 | 170 | '', |
223 | 171 | ]; |
| 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 | +} |
224 | 176 | if (added.length > 0) { |
225 | 177 | commitMsg.push('Certificates added:'); |
226 | 178 | commitMsg.push(...added.map((cert) => `- ${cert}`)); |
@@ -234,7 +186,7 @@ if (removed.length > 0) { |
234 | 186 | commitMsg.push(`[0] ${certdataURL}`); |
235 | 187 | const delimiter = randomUUID(); |
236 | 188 | const properties = [ |
237 | | - `NEW_VERSION=${release[kNSSVersion]}`, |
| 189 | + `NEW_VERSION=${version}`, |
238 | 190 | `COMMIT_MSG<<${delimiter}`, |
239 | 191 | ...commitMsg, |
240 | 192 | delimiter, |
|