Skip to content
Merged
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
67 changes: 44 additions & 23 deletions bend2/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -805,48 +805,69 @@ async function cli_login(): Promise<string> {
// pkg_files is the package the loader read for this file, the entry first:
// every .bend file at its namespace (the entry at its name), every foreign
// .c or .js file at its path from the entry's directory; base and the
// store's packages stay out. A path that climbs above the entry's directory
// takes the entry's ancestor directories along, as many as the deepest climb.
// store's packages stay out. The entry's directory is the one the loader
// roots namespaces at, so an entry linked from another directory, or one
// at the file system's root, is refused.
// A LICENSE beside a published file goes along; a license/ directory, in
// any case, is refused (it clashes with LICENSE on a case-blind disk).
// A leading byte order mark is dropped: the hub stores the text as sent,
// and an importer's fetch drops the mark, so a file sent with one would
// never match its hash.
function pkg_files(file: string, book: Bend.Book,
seen: Map<string, string | null>): Record<string, string> {
const dir = fs.realpathSync(path.dirname(file)) + "/";
const ent = fs.realpathSync(file);
const dir = path.join(path.dirname(ent), "/");
if (dir !== path.join(fs.realpathSync(path.dirname(file)), "/")) {
throw "Error: " + file + " cannot be published: it links to " + ent
+ ", in another directory; publish that file";
}
if (dir === "/") {
throw "Error: " + file + " cannot be published: it is at the file"
+ " system's root; move it into a directory";
}
const raws = [...[...seen].flatMap(([real, ns]): [string, string][] =>
real === BASE || ns === null || ns.startsWith("0x") ? []
: [[ns === "" ? path.basename(file) : ns + ".bend", real]]),
...Object.entries(book.tlds).flatMap(([k, tld]): [string, string][] =>
tld.$ !== "Def" || tld.i === undefined || tld.b === true
|| k.startsWith("0x") ? [] : tld.i.map((f) =>
[f.startsWith(dir) ? f.slice(dir.length) : f, f]))];
const ups = raws.map(([p]) => path.posix.normalize(p).split("/")
.filter((s) => s === "..").length);
const anc = fs.realpathSync(path.dirname(file)).split("/")
.slice(-Math.max(0, ...ups) || Infinity);
[path.posix.normalize(f.slice(dir.length)), f]))];
// .bend files come first, so a foreign file's .bend, its directory a
// prefix of the file's path, is checked inside dir before its path is cut
const files: Record<string, string> = {};
for (const [raw, real] of raws) {
const p = path.posix.join(...anc, raw);
if (p.startsWith("/") || p.startsWith("..")) {
throw "Error: " + real + " cannot be published (an absolute import,"
+ " or a climb above the file system)";
}
for (const [p, src] of raws) {
if (p.split("/").slice(0, -1).some((s) => s.toLowerCase() === "license")) {
throw "Error: " + real + " cannot be published: it is in a directory"
throw "Error: " + src + " cannot be published: it is in a directory"
+ " named license, which clashes with a LICENSE file; rename it";
}
files[p] = fs.readFileSync(real, "utf8").replace(/^\uFEFF/, "");
if (fs.readdirSync(path.dirname(real)).includes("LICENSE")) {
files[path.posix.join(path.posix.dirname(p), "LICENSE")] =
fs.readFileSync(path.join(path.dirname(real), "LICENSE"), "utf8")
.replace(/^\uFEFF/, "");
files[p] = pkg_read(p, src, dir, "move it");
if (fs.readdirSync(path.dirname(src)).includes("LICENSE")) {
const lic = path.posix.join(path.posix.dirname(p), "LICENSE");
files[lic] = pkg_read(lic, path.join(path.dirname(src), "LICENSE"), dir,
"it is the LICENSE beside " + src + "; copy it");
}
}
return files;
}

// pkg_read is the text of src, published at p: p and src's real path stay
// inside dir, the entry's directory. p is the import's own path, normalized
// as text, since an importer resolves it inside the package that way; one
// that climbs out and back in names a file the package lacks. A leading
// byte order mark is dropped: the hub stores the text as sent, and an
// importer's fetch drops the mark, so a file sent with one would never
// match its hash.
function pkg_read(p: string, src: string, dir: string, fix: string): string {
const real = fs.realpathSync(src);
if (!real.startsWith(dir)) {
throw "Error: " + src + " cannot be published: it is outside " + dir
+ " (the entry's directory); " + fix + " inside";
}
if (p.startsWith("../") || p.startsWith("/")) {
throw "Error: " + src + " cannot be published: its import climbs out of"
+ " the package (" + p + "); import it by a path that stays inside "
+ dir;
}
return fs.readFileSync(real, "utf8").replace(/^\uFEFF/, "");
}

function sha256(text: string): string {
return crypto.createHash("sha256").update(text).digest("hex");
}
Expand Down
50 changes: 49 additions & 1 deletion gates/ping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
// latest.json fallback name the version, no sha256 and the move notice; the
// formula carries the sum; --publish ships LICENSE files, names the license
// as the hub does, sends no leading byte order mark (a package published
// from a file with one imports), refuses a License/ directory, and every
// from a file with one imports), refuses a License/ directory and a file
// leaving the entry's directory (a climb, even one back in, a link out, a
// linked entry, LICENSE included) before mining, publishes a link inside
// and a nested file's climb that stays inside, and every
// request carries User-Agent: bend/<ver>. SKIP when the site repo is not at
// lib.SITE.

Expand Down Expand Up @@ -561,6 +564,51 @@ try {
dir.code === 1 && dir.err.includes("in a directory named license")
&& !dir.err.includes("mining")
&& seen.filter((s) => s.startsWith("POST / ")).length === posts);
const ffi = (at: string) => "import Base\ndef boot(a: U32) -> IO(Unit):\n"
+ ' import "' + at + '"\n\ndef main() -> IO(Unit):\n do IO<Unit>:\n'
+ ' IO.print("hi\\n")\n';
const link = (at: string, to: string) => {
fs.mkdirSync(path.dirname(path.join(TMP, "pub", at)), { recursive: true });
fs.symlinkSync(to, path.join(TMP, "pub", at));
};
fs.mkdirSync(path.join(TMP, "pub", "out"), { recursive: true });
fs.writeFileSync(path.join(TMP, "pub", "out.c"), "// secret\n");
fs.writeFileSync(path.join(TMP, "pub", "out", "x.c"), "// secret\n");
fs.writeFileSync(path.join(TMP, "pub", "out", "lic_link.bend"), two);
link("file/p.c", "../out.c");
link("tree/d", "../out");
link("lic/LICENSE", "../out.c");
link("link/lic_link.bend", "../out/lic_link.bend");
const away = "cannot be published: it is outside ";
const outs: [string, Record<string, string>, string][] = [
["climb", { "lic_climb.bend": ffi("../out.c") }, away],
["file", { "lic_file.bend": ffi("./p.c") }, away],
["tree", { "lic_tree.bend": ffi("./d/x.c") }, away],
["lic", { "lic_lic.bend": two }, "; it is the LICENSE beside "],
["link", {}, "cannot be published: it links to "],
["up", { "lic_up.bend": use("../out/lic_link.bend") }, away],
["back", { "lic_back.bend": ffi("../back/ok.c"), "ok.c": "// ok\n" },
"its import climbs out of the package (../back/ok.c)"]];
for (const [dir, pkg, why] of outs) {
const count = seen.filter((s) => s.startsWith("POST / ")).length;
const run = await publish(dir, pkg);
check("a published " + dir + " leaving the entry's directory is refused"
+ " before mining: " + run.err, run.code === 1
&& run.err.includes(why)
&& !run.err.includes("mining")
&& seen.filter((s) => s.startsWith("POST / ")).length === count);
}
link("in/p.c", "real.c");
const inside = await publish("in", { "lic_in.bend": ffi("./p.c"),
"real.c": "// ok\n" });
const nest = { "lic_nest.bend": use("sub/f.bend"), "ok.c": "// ok\n",
"sub/f.bend": two + '\ndef boot(a: U32) -> IO(Unit):\n import "../ok.c"\n' };
const up = await publish("nest", nest);
check("a link inside the entry's directory goes, and a nested file's climb"
+ " that stays inside goes at its path: " + inside.err + up.err,
inside.code === 0 && inside.out.startsWith(pkg_hash({ "lic_in.bend":
ffi("./p.c"), "p.c": "// ok\n" }) + "\n") && up.code === 0
&& up.out.startsWith(pkg_hash(nest) + "\n"));
fs.writeFileSync(path.join(TMP, "pkg.bend"), "import Base\nimport " + hash
+ "/sub/two.bend as T\nimport bend-ping-probe@1.0.0.0/x.bend as X\n"
+ "def main() -> Nat:\n T.two\n");
Expand Down