@@ -0,0 +1,208 @@
5+from pathlib import Path
6+from urllib.parse import quote
8+from noun import Cell, deep, intbytes, mug
14+ "ico": "image/x-icon",
15+ "jpeg": "image/jpeg",
17+ "js": "application/javascript",
18+ "json": "application/json",
20+ "svg": "image/svg+xml",
22+ "webmanifest": "application/manifest+json",
24+ "woff2": "font/woff2",
26+BASE32 = "0123456789abcdefghijklmnopqrstuv"
31+ self.output = bytearray()
36+ def write(self, value, count):
39+ self.buffer |= value << self.used
42+ byte_count = self.used // 8
44+ self.output.extend(self.buffer.to_bytes((self.used + 7) // 8, "little")[:byte_count])
45+ self.buffer >>= byte_count * 8
46+ self.used -= byte_count * 8
50+ self.output.append(self.buffer)
53+ return bytes(self.output)
61+ references[noun] = writer.cursor
67+ atom_length = atom.bit_length()
68+ length_size = atom_length.bit_length()
69+ writer.write(1 << length_size, length_size + 1)
70+ writer.write(atom_length & ((1 << (length_size - 1)) - 1), length_size - 1)
71+ writer.write(atom, atom_length)
78+ duplicate = references.get(noun)
80+ if duplicate is not None:
89+ if duplicate is not None:
90+ atom_size = noun.bit_length()
91+ reference_size = duplicate.bit_length()
92+ if atom_size >= reference_size:
100+ return writer.finish()
104+ return int.from_bytes(value.encode(), "little")
107+def path_noun(parts):
109+ for part in reversed(parts):
110+ result = Cell(cord(part), result)
120+ if a.head == b.head:
121+ return dor(a.tail, b.tail)
122+ return dor(a.head, b.head)
131+ return dor(a, b) if a_mug == b_mug else a_mug < b_mug
134+def map_put(tree, entry):
136+ return Cell(entry, Cell(0, 0))
138+ if entry.head == node.head:
139+ return Cell(entry, tree.tail)
140+ left = tree.tail.head
141+ right = tree.tail.tail
142+ if gor(entry.head, node.head):
143+ return Cell(node, Cell(map_put(left, entry), right))
144+ return Cell(node, Cell(left, map_put(right, entry)))
147+def shas(salt, serialized):
148+ value_hash = int.from_bytes(hashlib.sha256(serialized).digest(), "little")
149+ length = max(32, len(intbytes(salt)))
150+ digest = hashlib.sha256((salt ^ value_hash).to_bytes(length, "little")).digest()
151+ return int.from_bytes(digest, "little")
154+def sham(serialized):
155+ hashed = shas(cord("sham"), serialized)
156+ mask = (1 << 128) - 1
157+ return (hashed & mask) ^ (hashed >> 128)
165+ value, digit = divmod(value, 32)
166+ digits.append(BASE32[digit])
167+ digits = "".join(reversed(digits))
168+ first_length = (len(digits) - 1) % 5 + 1
169+ groups = [digits[:first_length]]
170+ groups.extend(digits[i:i + 5] for i in range(first_length, len(digits), 5))
171+ return "0v" + ".".join(groups)
174+def file_entry(root, file_path):
175+ relative = file_path.relative_to(root)
177+ for part in relative.parts:
178+ parts.extend(quote(segment, safe="-_~") for segment in part.split("."))
179+ extension = relative.suffix.removeprefix(".").lower()
180+ if extension not in MIME_TYPES:
181+ raise ValueError(f"no MIME type configured for {relative}")
182+ data = file_path.read_bytes()
183+ mime = Cell(path_noun(MIME_TYPES[extension].split("/")), Cell(len(data), int.from_bytes(data, "little")))
184+ return Cell(path_noun(parts), mime)
187+def make_glob(source, destination):
189+ files = sorted((path for path in root.rglob("*") if path.is_file()), key=lambda path: path.as_posix())
190+ if not (root / "index.html").is_file():
191+ raise ValueError("glob source must contain index.html")
193+ for file_path in files:
194+ glob = map_put(glob, file_entry(root, file_path))
195+ serialized = jam_bytes(glob)
196+ glob_hash = scow_uv(sham(serialized))
197+ output = Path(destination)
198+ output.mkdir(parents=True, exist_ok=True)
199+ filename = f"glob-{glob_hash}.glob"
200+ (output / filename).write_bytes(serialized)
201+ (output / "hash").write_text(glob_hash + "\n")
205+if __name__ == "__main__":
206+ if len(sys.argv) != 3:
207+ raise SystemExit(f"usage: {sys.argv[0]} SOURCE_DIRECTORY OUTPUT_DIRECTORY")
208+ make_glob(sys.argv[1], sys.argv[2])