diff --git a/package.json b/package.json index 3c4511b..3feb0a0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "aphrodite": "^2.1.0", - "base-64": "^0.1.0", + "base64url": "^3.0.1", "better-sqlite3": "^5.4.0", "chalk": "2.4.2", "commonmark": "^0.29.0", diff --git a/src/core/_getProjectIds.js b/src/core/_getProjectIds.js index 89eadc2..f045abb 100644 --- a/src/core/_getProjectIds.js +++ b/src/core/_getProjectIds.js @@ -9,28 +9,36 @@ // This file is tested in ./project_io.test.js const path = require("path"); -const base64 = require("base-64"); +const base64url = require("base64url"); const fs = require("fs-extra"); -module.exports = function getProjectIds( +/** + * Get the ids for every project saved on the filesystem. + * + * It is not guaranteed that it will be possible to load the id in question. + * (For example, the project may be malformed, or may have an outdated compat + * version.) + */ +module.exports = async function getProjectIds( sourcecredDirectory /*: string */ -) /*: $ReadOnlyArray */ { +) /*: Promise<$ReadOnlyArray> */ { const projectsPath = path.join(sourcecredDirectory, "projects"); let entries = []; try { - entries = fs.readdirSync(projectsPath); + entries = await fs.readdir(projectsPath); } catch { return []; } - const projectIds = []; - for (const entry of entries) { - const jsonPath = path.join(projectsPath, entry, "project.json"); + const getProjectId = async (entry) => { try { - fs.statSync(jsonPath); - projectIds.push(base64.decode(entry)); + const jsonPath = path.join(projectsPath, entry, "project.json"); + await fs.stat(jsonPath); + return base64url.decode(entry); } catch { - continue; + return null; } - } - return projectIds; + }; + + const maybeProjectIds = await Promise.all(entries.map(getProjectId)); + return maybeProjectIds.filter((x) => x != null); }; diff --git a/src/core/project.js b/src/core/project.js index b22a525..887a73c 100644 --- a/src/core/project.js +++ b/src/core/project.js @@ -1,6 +1,6 @@ // @flow -import base64 from "base-64"; +import base64url from "base64url"; import {type RepoId} from "../core/repoId"; import {toCompat, fromCompat, type Compatible} from "../util/compat"; @@ -43,5 +43,5 @@ export function projectFromJSON(j: ProjectJSON): Project { * or retrieved via XHR from the frontend. */ export function encodeProjectId(id: ProjectId): string { - return base64.encode(id); + return base64url.encode(id); } diff --git a/src/core/project.test.js b/src/core/project.test.js index 90c5f53..982b1d4 100644 --- a/src/core/project.test.js +++ b/src/core/project.test.js @@ -1,10 +1,16 @@ // @flow -import {projectToJSON, projectFromJSON, type Project} from "./project"; +import base64url from "base64url"; +import { + projectToJSON, + projectFromJSON, + type Project, + encodeProjectId, +} from "./project"; import {makeRepoId} from "./repoId"; -describe("core/project.js", () => { +describe("core/project", () => { const foobar = Object.freeze(makeRepoId("foo", "bar")); const foozod = Object.freeze(makeRepoId("foo", "zod")); const p1: Project = Object.freeze({ @@ -26,4 +32,14 @@ describe("core/project.js", () => { check(p2); }); }); + describe("encodeProjectId", () => { + it("is a base64-url encoded id", () => { + const project = {id: "foo bar", repoIds: []}; + const encoded = encodeProjectId(project.id); + expect(encoded).toEqual(base64url.encode("foo bar")); + }); + it("is decodable to identity", () => { + expect(base64url.decode(encodeProjectId("foo bar"))).toEqual("foo bar"); + }); + }); }); diff --git a/src/core/project_io.js b/src/core/project_io.js index 1bf121f..5160061 100644 --- a/src/core/project_io.js +++ b/src/core/project_io.js @@ -28,7 +28,7 @@ import _getProjectIds from "./_getProjectIds"; */ export function getProjectIds( sourcecredDirectory: string -): $ReadOnlyArray { +): Promise<$ReadOnlyArray> { return _getProjectIds(sourcecredDirectory); } diff --git a/src/core/project_io.test.js b/src/core/project_io.test.js index 7ed254a..5601fe3 100644 --- a/src/core/project_io.test.js +++ b/src/core/project_io.test.js @@ -15,7 +15,7 @@ import { import {makeRepoId} from "./repoId"; -describe("core/project_io.js", () => { +describe("core/project_io", () => { const foobar = Object.freeze(makeRepoId("foo", "bar")); const foozod = Object.freeze(makeRepoId("foo", "zod")); const p1: Project = Object.freeze({ @@ -30,7 +30,7 @@ describe("core/project_io.js", () => { it("setupProjectDirectory results in a loadable project", async () => { const sourcecredDirectory = tmp.dirSync().name; await setupProjectDirectory(p1, sourcecredDirectory); - const ps = getProjectIds(sourcecredDirectory); + const ps = await getProjectIds(sourcecredDirectory); expect(ps).toEqual([p1.id]); expect(await loadProject(p1.id, sourcecredDirectory)).toEqual(p1); }); @@ -38,7 +38,7 @@ describe("core/project_io.js", () => { const sourcecredDirectory = tmp.dirSync().name; await setupProjectDirectory(p1, sourcecredDirectory); await setupProjectDirectory(p2, sourcecredDirectory); - const ps = getProjectIds(sourcecredDirectory); + const ps = await getProjectIds(sourcecredDirectory); expect(ps).toHaveLength(2); expect(ps.slice().sort()).toEqual([p2.id, p1.id]); expect(await loadProject(p1.id, sourcecredDirectory)).toEqual(p1); @@ -46,7 +46,7 @@ describe("core/project_io.js", () => { }); it("getProjectIds returns no projects if none were setup", async () => { const sourcecredDirectory = tmp.dirSync().name; - const ps = getProjectIds(sourcecredDirectory); + const ps = await getProjectIds(sourcecredDirectory); expect(ps).toHaveLength(0); }); it("setupProjectDirectory returns the right directory", async () => { @@ -72,7 +72,7 @@ describe("core/project_io.js", () => { const sourcecredDirectory = tmp.dirSync().name; await setupProjectDirectory(p1, sourcecredDirectory); await fs.mkdirp(path.join(sourcecredDirectory, "projects", "foobar")); - const ps = getProjectIds(sourcecredDirectory); + const ps = await getProjectIds(sourcecredDirectory); expect(ps).toEqual([p1.id]); }); it("getProjectIds ignores non-project file entries", async () => { @@ -82,7 +82,7 @@ describe("core/project_io.js", () => { path.join(sourcecredDirectory, "projects", "foobar"), "1234" ); - const ps = getProjectIds(sourcecredDirectory); + const ps = await getProjectIds(sourcecredDirectory); expect(ps).toEqual([p1.id]); }); it("loadProject throws an error on inconsistent id", async () => { diff --git a/yarn.lock b/yarn.lock index f5541d1..27a0194 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1678,16 +1678,16 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base-64@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" - integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs= - base64-js@^1.0.2: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw== +base64url@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"