57c99012f4b4
match Koh whitespace and line endings when reading declared versions (v3.0.203)
- time
- 2026-10-03 14:02 UTC · 1 week ago
- machine
- Junes-MacBook-Pro.local
- agent
- gpt-6.1-sol
- session
- review declared repo version
- parent
- 65b4734faa48
↓ kepr-57c99012.tar.gzatlas at this save
files changed4 files
package.json+1 −1src/describe/version.ts+5 −4src/mirror/store.ts+4 −2tests/version.test.ts+39 −0package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.0.202",3+ "version": "3.0.203",44 "private": true,55 "type": "module",66 "scripts": {src/describe/version.ts+5 −4
@@ -10,11 +10,12 @@1010 "build.gradle",1111 ];1212 const VERSION = /^[A-Za-z0-9.-]{1,32}$/;13+const ASCII_SPACE = /^[ \n\t\v\f\r]+|[ \n\t\v\f\r]+$/g;1314 1415 /** The declared version, reading files at one save through `read` (null for a missing file). */1516 export function declaredVersion(read: (path: string) => string | null): string | null {1617 const sources: (() => string | null)[] = [17- () => read("VERSION")?.trim() || null,18+ () => read("VERSION")?.replace(ASCII_SPACE, "") || null,1819 () => lineVersion(read("build.zig.zon"), ".version"),1920 () => lineVersion(read("v.mod"), "version"),2021 () => lineVersion(read("package.json"), '"version"'),@@ -31,7 +32,7 @@3132 3233 // The value of the first line the key starts that assigns a quoted literal.3334 function lineVersion(text: string | null, key: string): string | null {34- for (const line of text?.split(/\r?\n/) ?? []) {35+ for (const line of text?.split(/\r\n|[\r\n]/) ?? []) {3536 const trimmed = line.replace(/^[ \t]+/, "");3637 if (trimmed === "" || trimmed.startsWith("//") || trimmed.startsWith("#")) continue;3738 if (!trimmed.startsWith(key)) continue;@@ -59,13 +60,13 @@5960 const open = rest.indexOf("<string>");6061 const close = rest.indexOf("</string>");6162 if (open < 0 || close <= open + "<string>".length) return null;62- return rest.slice(open + "<string>".length, close).trim() || null;63+ return rest.slice(open + "<string>".length, close).replace(ASCII_SPACE, "") || null;6364 }6465 6566 // `versionName` assigned a quoted literal, Kotlin (`= "1.2"`) or Groovy (`"1.2"`); comments and6667 // `versionNameSuffix` are not it.6768 function gradleVersion(text: string | null): string | null {68- for (const line of text?.split(/\r?\n/) ?? []) {69+ for (const line of text?.split(/\r\n|[\r\n]/) ?? []) {6970 const trimmed = line.replace(/^[ \t]+/, "");7071 if (trimmed === "" || /^(\/\/|\/\*|\*)/.test(trimmed)) continue;7172 if (!/^versionName[ =]/.test(trimmed)) continue;src/mirror/store.ts+4 −2
@@ -143,7 +143,7 @@143143 // Vendored code is someone else's: counting Koh 3's bundled SQLite made it read as 290k lines of C.144144 const VENDORED = /(^|\/)(vendor|third_party|node_modules)\//;145145 // Bumped when what the recount records changes, so every mirror recounts once when it opens.146-const STATS_RULE = "source-only, known objects, declared version";146+const STATS_RULE = "source-only, known objects, declared version (ASCII whitespace, BOM)";147147 148148 const open = new Map<string, Database>();149149 @@ -529,7 +529,9 @@529529 AND f.path = ?`,530530 )531531 .get(save, path);532- return row && row.size <= MAX_MANIFEST ? new TextDecoder().decode(inflateSync(row.data)) : null;532+ return row && row.size <= MAX_MANIFEST533+ ? new TextDecoder("utf-8", { ignoreBOM: true }).decode(inflateSync(row.data))534+ : null;533535 }534536 535537 // Lines and languages never change between syncs, so they are counted once here, not per view. Theytests/version.test.ts+39 −0
@@ -13,6 +13,30 @@1313 const from = (files: Record<string, string>) => declaredVersion((path) => files[path] ?? null);1414 1515 describe("the declared version", () => {16+ test("reads CR, LF and CRLF lines as Koh does", () => {17+ for (const ending of ["\r", "\n", "\r\n"]) {18+ expect(from({ "v.mod": `Module {${ending}version: '1.2.3'${ending}}` })).toBe("1.2.3");19+ expect(20+ from({ "app/build.gradle.kts": `android {${ending}versionName = "1.2.3"${ending}}` }),21+ ).toBe("1.2.3");22+ }23+ });24+25+ test("trims only Koh's ASCII whitespace and refuses an invalid first answer", () => {26+ expect(from({ VERSION: " \n\t\v\f\r1.2.3 \n\t\v\f\r" })).toBe("1.2.3");27+ for (const space of ["\u00a0", "\ufeff"]) {28+ expect(29+ from({ VERSION: `${space}1.2.3${space}`, "Cargo.toml": 'version = "9.0.0"' }),30+ ).toBeNull();31+ expect(32+ from({33+ "Info.plist": `<key>CFBundleShortVersionString</key><string>${space}1.2.3${space}</string>`,34+ "app/build.gradle.kts": 'versionName = "9.0.0"',35+ }),36+ ).toBeNull();37+ }38+ });39+1640 test("is the first source that answers, VERSION first", () => {1741 expect(from({ VERSION: " 2.0.1\n", "package.json": '{ "version": "1.0.0" }' })).toBe("2.0.1");1842 expect(from({ "package.json": '{\n "name": "x",\n "version": "3.0.199",\n}' })).toBe(@@ -71,4 +95,19 @@7195 mirrorDir: join(inst.dataDir, "mirror"),7296 });7397 expect(await meta()).toStartWith("v2.0.0 · ");98+});99+100+test("a BOM in a saved VERSION is preserved and refuses a lower-priority version", async () => {101+ const repos = await makeRepos(["app"]);102+ const work = join(repos.bucketDir, "..", "work", "app");103+ await writeFile(join(work, "VERSION"), "\ufeff1.2.3");104+ await writeFile(join(work, "Cargo.toml"), 'version = "9.0.0"');105+ repos.koh("app", "save", "declare a version with a BOM");106+ repos.koh("app", "offer");107+ const inst = await instance(repos.bucketDir);108+ inst.kepr.run("UPDATE repos SET public = 1");109+ const page = await (await inst.app.request("/app")).text();110+ const meta = /<p class="repo-meta">([^<]*)<\/p>/.exec(page)?.[1];111+ expect(meta).toBeDefined();112+ expect(meta).not.toStartWith("v");74113 });