├── .nojekyll ├── LICENSE.txt ├── README.md ├── auto.sh ├── bzip2.wasm ├── bzip2_impls.c ├── db.html ├── dummy_main.c ├── icon.svg ├── index.html ├── misc.js ├── view.html └── xz-embedded.wasm /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wh0/nix-cache-view/efe5bcaae23a768bf9719ef357e9ed516ea6054a/.nojekyll -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 wh0 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nix cache view 2 | 3 | _Nix cache view_ is a static web app for exploring a Nix binary cache over HTTP. 4 | 5 | [Project on GitHub](https://github.com/wh0/nix-cache-view) 6 | [Public deployment](https://wh0.github.io/nix-cache-view/) 7 | 8 | Information about binary caches: 9 | 10 | - [Serving a Nix store via HTTP (Nix Manual)](https://nixos.org/manual/nix/stable/package-management/binary-cache-substituter.html) 11 | - [Binary Cache (NixOS Wiki)](https://nixos.wiki/wiki/Binary_Cache) 12 | -------------------------------------------------------------------------------- /auto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eux 2 | 3 | cd /tmp 4 | curl https://tukaani.org/xz/xz-embedded-20210201.tar.gz | tar -xz 5 | curl https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz | tar -xz 6 | curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-12/wasi-sdk_12.0_amd64.deb | dpkg -x - unpack 7 | 8 | cd /tmp/xz-embedded-20210201 9 | /tmp/unpack/opt/wasi-sdk/bin/clang \ 10 | --sysroot=/tmp/unpack/opt/wasi-sdk/share/wasi-sysroot \ 11 | --target=wasm32-wasi \ 12 | -Wl,--export,malloc \ 13 | -Wl,--export,xz_crc32_init \ 14 | -Wl,--export,xz_crc64_init \ 15 | -Wl,--export,xz_dec_init \ 16 | -Wl,--export,xz_dec_run \ 17 | -Wl,--no-entry \ 18 | -Ilinux/include/linux \ 19 | -Iuserspace \ 20 | -DXZ_USE_CRC64 \ 21 | -O2 \ 22 | linux/lib/xz/xz_crc32.c \ 23 | linux/lib/xz/xz_crc64.c \ 24 | linux/lib/xz/xz_dec_stream.c \ 25 | linux/lib/xz/xz_dec_lzma2.c \ 26 | linux/lib/xz/xz_dec_bcj.c \ 27 | ~/dummy_main.c \ 28 | -o ~/xz-embedded.wasm 29 | 30 | cd /tmp/bzip2-1.0.8 31 | /tmp/unpack/opt/wasi-sdk/bin/clang \ 32 | --sysroot=/tmp/unpack/opt/wasi-sdk/share/wasi-sysroot \ 33 | --target=wasm32-wasi \ 34 | -Wl,--export,malloc \ 35 | -Wl,--export,BZ2_bzBuffToBuffDecompress \ 36 | -Wl,--no-entry \ 37 | -DBZ_NO_STDIO \ 38 | -O2 \ 39 | blocksort.c \ 40 | huffman.c \ 41 | crctable.c \ 42 | randtable.c \ 43 | compress.c \ 44 | decompress.c \ 45 | bzlib.c \ 46 | ~/dummy_main.c \ 47 | ~/bzip2_impls.c \ 48 | -o ~/bzip2.wasm 49 | -------------------------------------------------------------------------------- /bzip2.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wh0/nix-cache-view/efe5bcaae23a768bf9719ef357e9ed516ea6054a/bzip2.wasm -------------------------------------------------------------------------------- /bzip2_impls.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void bz_internal_error(int errcode) { 4 | abort(); 5 | } 6 | -------------------------------------------------------------------------------- /db.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /dummy_main.c: -------------------------------------------------------------------------------- 1 | int main() { 2 | return 0; 3 | } 4 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nix cache view 6 | 17 | 26 |
27 |

28 |

29 |

30 |

31 |

32 | 33 |

34 |
35 | 79 | -------------------------------------------------------------------------------- /misc.js: -------------------------------------------------------------------------------- 1 | async function fetchOk(url) { 2 | const res = await fetch(url); 3 | if (!res.ok) throw new Error(`fetch ${url} status ${res.status} not ok`); 4 | return res; 5 | } 6 | 7 | async function fetchOkText(url) { 8 | const res = await fetchOk(url); 9 | return await res.text(); 10 | } 11 | 12 | async function fetchOkBuf(url) { 13 | const res = await fetchOk(url); 14 | return await res.arrayBuffer(); 15 | } 16 | 17 | const WASM_IMPORTS = { 18 | wasi_snapshot_preview1: { 19 | proc_exit(rval) { 20 | throw new Error(`wasi proc_exit ${rval}`); 21 | }, 22 | }, 23 | }; 24 | 25 | async function xzDecompress(inBuf, outSize) { 26 | console.log('fetch + instantiateStreaming'); 27 | const inst = (await WebAssembly.instantiateStreaming(fetch('xz-embedded.wasm'), WASM_IMPORTS)).instance; 28 | console.log('xz_crc32_init'); 29 | inst.exports.xz_crc32_init(); 30 | console.log('xz_crc64_init'); 31 | inst.exports.xz_crc64_init(); 32 | const decP = inst.exports.xz_dec_init(0 /* XZ_SINGLE */, 64 * 1024 * 1024); 33 | if (!decP) throw new Error('xz_dec_init null'); 34 | const bufP = inst.exports.malloc(24 /* sizeof(struct xz_buf) */); 35 | if (!bufP) throw new Error('malloc buf null'); 36 | 37 | const inSize = inBuf.byteLength; 38 | 39 | const inP = inst.exports.malloc(inSize); 40 | if (!inP) throw new Error(`malloc in ${inSize} null`); 41 | const outP = inst.exports.malloc(outSize); 42 | if (!outP) throw new Error(`malloc out ${outSize} null`); 43 | 44 | const inU8 = new Uint8Array(inst.exports.memory.buffer, inP, inSize); 45 | inU8.set(new Uint8Array(inBuf)); 46 | const bufU32A = new Uint32Array(inst.exports.memory.buffer, bufP, 6); 47 | bufU32A[0 /* in */] = inP; 48 | bufU32A[1 /* in_pos */] = 0; 49 | bufU32A[2 /* in_size */] = inSize; 50 | bufU32A[3 /* out */] = outP; 51 | bufU32A[4 /* out_pos */] = 0; 52 | bufU32A[5 /* out_size */] = outSize; 53 | 54 | console.log('xz_dec_run'); 55 | const runResult = inst.exports.xz_dec_run(decP, bufP); 56 | if (runResult !== 1 /* XZ_STREAM_END */) throw new Error(`xz_dec_run result ${runResult} not stream end`); 57 | 58 | const bufU32B = new Uint32Array(inst.exports.memory.buffer, bufP, 6); 59 | const outPos = bufU32B[4 /* out_pos */]; 60 | const outBuf = inst.exports.memory.buffer.slice(outP, outP + outPos); 61 | return outBuf; 62 | } 63 | 64 | async function bzip2Decompress(inBuf, outSize) { 65 | console.log('fetch + instantiateStreaming'); 66 | const inst = (await WebAssembly.instantiateStreaming(fetch('bzip2.wasm'), WASM_IMPORTS)).instance; 67 | 68 | const inSize = inBuf.byteLength; 69 | 70 | const outP = inst.exports.malloc(outSize); 71 | if (!outP) throw new Error(`malloc out ${outSize} null`); 72 | const outSizeP = inst.exports.malloc(4 /* sizeof(unsigned int) */); 73 | if (!outSizeP) throw new Error(`malloc out_size null`); 74 | const inP = inst.exports.malloc(inSize); 75 | if (!inP) throw new Error(`malloc in ${inSize} null`); 76 | 77 | const outSizeBufU32A = new Uint32Array(inst.exports.memory.buffer, outSizeP, 1); 78 | outSizeBufU32A[0] = outSize; 79 | const inU8 = new Uint8Array(inst.exports.memory.buffer, inP, inSize); 80 | inU8.set(new Uint8Array(inBuf)); 81 | 82 | console.log('BZ2_bzBuffToBuffDecompress'); 83 | const decompressResult = inst.exports.BZ2_bzBuffToBuffDecompress(outP, outSizeP, inP, inSize, 0, 0); 84 | if (decompressResult !== 0 /* BZ_OK */) throw new Error(`BZ2_bzBuffToBuffDecompress result ${decompressResult} not ok`); 85 | 86 | const outSizeBufU32B = new Uint32Array(inst.exports.memory.buffer, outSizeP, 1); 87 | const uncompressedSize = outSizeBufU32B[0]; 88 | const outBuf = inst.exports.memory.buffer.slice(outP, outP + uncompressedSize); 89 | return outBuf; 90 | } 91 | 92 | const DECOMPRESS_METHODS = { 93 | xz: xzDecompress, 94 | bzip2: bzip2Decompress, 95 | none: (inBuf, outSize) => inBuf, 96 | }; 97 | 98 | function narinfoVisitFields(narinfoText, handler) { 99 | const pattern = /^([^:]*): (.*)\n/mg; 100 | let m; 101 | while (m = pattern.exec(narinfoText)) { 102 | handler(m[1], m[2]); 103 | } 104 | } 105 | 106 | function narinfoParse(narinfoText) { 107 | const narinfo = { 108 | path: null, 109 | url: null, 110 | compression: null, 111 | fileHash: null, 112 | fileSize: null, 113 | narHash: null, 114 | narSize: null, 115 | references: [], 116 | deriver: null, 117 | sigs: [], 118 | ca: null, 119 | }; 120 | narinfoVisitFields(narinfoText, (k, v) => { 121 | switch (k) { 122 | case 'StorePath': 123 | narinfo.path = v; 124 | break; 125 | case 'URL': 126 | narinfo.url = v; 127 | break; 128 | case 'Compression': 129 | narinfo.compression = v; 130 | break; 131 | case 'FileHash': 132 | narinfo.fileHash = v; 133 | break; 134 | case 'FileSize': 135 | narinfo.fileSize = +v; 136 | break; 137 | case 'NarHash': 138 | narinfo.narHash = v; 139 | break; 140 | case 'NarSize': 141 | narinfo.narSize = +v; 142 | break; 143 | case 'References': 144 | if (v) { 145 | narinfo.references.push(...v.split(' ')); 146 | } 147 | break; 148 | case 'Deriver': 149 | if (v !== 'unknown-deriver') { 150 | narinfo.deriver = v; 151 | } 152 | break; 153 | case 'Sig': 154 | narinfo.sigs.push(v); 155 | break; 156 | case 'CA': 157 | narinfo.ca = v; 158 | break; 159 | } 160 | }); 161 | return narinfo; 162 | } 163 | 164 | const PATH_PATTERN = /\/([0-9a-z]{32})-([^/]+)(.*)/; 165 | 166 | function pathMatch(path) { 167 | return PATH_PATTERN.exec(path); 168 | } 169 | 170 | function pathMatchHash(match) { 171 | return match[1]; 172 | } 173 | 174 | function pathMatchName(match) { 175 | return match[2]; 176 | } 177 | 178 | function pathMatchHashStart(match) { 179 | return match.index + 1; 180 | } 181 | 182 | function pathMatchHashEnd(match) { 183 | return pathMatchHashStart(match) + match[1].length; 184 | } 185 | 186 | function pathMatchNameStart(match) { 187 | return pathMatchHashEnd(match) + 1; 188 | } 189 | 190 | function pathMatchNameEnd(match) { 191 | return pathMatchNameStart(match) + match[2].length; 192 | } 193 | 194 | function pathHash(path) { 195 | return pathMatchHash(pathMatch(path)); 196 | } 197 | 198 | function pathName(path) { 199 | return pathMatchName(pathMatch(path)); 200 | } 201 | 202 | function basenameHash(basename) { 203 | return basename.slice(0, 32); 204 | } 205 | 206 | function basenameName(basename) { 207 | return basename.slice(33); 208 | } 209 | 210 | function basenameHashStart(basename) { 211 | return 0; 212 | } 213 | 214 | function basenameHashEnd(basename) { 215 | return 32; 216 | } 217 | 218 | function basenameNameStart(basename) { 219 | return 33; 220 | } 221 | 222 | function basenameNameEnd(basename) { 223 | return basename.length; 224 | } 225 | 226 | function nameIsDerivation(name) { 227 | return name.endsWith('.drv'); 228 | } 229 | 230 | function narReadInt(reader) { 231 | const i = reader.data.getBigInt64(reader.pos, true); 232 | reader.pos += 8; 233 | return Number(i); 234 | }; 235 | 236 | function narReadU8(reader) { 237 | const length = narReadInt(reader); 238 | const paddedLength = (length + 7) & -8; 239 | const u8 = new Uint8Array(reader.buf, reader.pos, length); 240 | reader.pos += paddedLength; 241 | return u8; 242 | }; 243 | 244 | function narReadString(reader) { 245 | return new TextDecoder().decode(narReadU8(reader)); 246 | } 247 | 248 | function narExpectString(reader, expected) { 249 | const s = narReadString(reader); 250 | if (s !== expected) throw new Error(`unexpected '${s}', expected '${expected}'`); 251 | } 252 | 253 | function narReadPairs(reader, handler) { 254 | narExpectString(reader, '('); 255 | while (true) { 256 | const k = narReadString(reader); 257 | if (k === ')') break; 258 | handler(k); 259 | } 260 | } 261 | 262 | function narReadDirEntry(reader) { 263 | const entry = {}; 264 | narReadPairs(reader, (k) => { 265 | switch (k) { 266 | case 'name': 267 | entry.name = narReadString(reader); 268 | break; 269 | case 'node': 270 | entry.node = narReadNode(reader); 271 | break; 272 | default: 273 | throw new Error(`unrecognized key ${k}`); 274 | } 275 | }); 276 | return entry; 277 | } 278 | 279 | function narReadNode(reader) { 280 | const node = {}; 281 | narReadPairs(reader, (k) => { 282 | switch (k) { 283 | case 'type': 284 | node.type = narReadString(reader); 285 | switch (node.type) { 286 | case 'regular': 287 | node.executable = false; 288 | break; 289 | case 'directory': 290 | node.entries = []; 291 | break; 292 | } 293 | break; 294 | case 'executable': 295 | narExpectString(reader, ''); 296 | node.executable = true; 297 | break; 298 | case 'contents': 299 | node.contents = narReadU8(reader); 300 | break; 301 | case 'target': 302 | node.target = narReadString(reader); 303 | break; 304 | case 'entry': 305 | node.entries.push(narReadDirEntry(reader)); 306 | break; 307 | default: 308 | throw new Error(`unrecognized key ${k}`); 309 | } 310 | }); 311 | return node; 312 | } 313 | 314 | function narRead(buf) { 315 | const reader = { 316 | buf, 317 | data: new DataView(buf), 318 | pos: 0, 319 | }; 320 | narExpectString(reader, 'nix-archive-1'); 321 | return {root: narReadNode(reader)}; 322 | } 323 | 324 | function narNavigate(nar, path) { 325 | let node = nar.root; 326 | const names = path.split('/'); 327 | for (const name of names) { 328 | if (!name) continue; 329 | if (node.type !== 'directory') throw new Error(`navigate nar node type ${node.type}, expected directory`); 330 | const entry = node.entries.find((e) => e.name === name); 331 | if (!entry) throw new Error(`navigate nar name ${name} not found`); 332 | node = entry.node; 333 | } 334 | return node; 335 | } 336 | 337 | function drvParse(drvText) { 338 | const drvMunged = drvText.replace(/"(?:[^"\\]|\\.)*"|(Derive)|(\()|(\))/g, (orig, derive, lparen, rparen) => { 339 | if (derive) return ''; 340 | if (lparen) return '['; 341 | if (rparen) return ']'; 342 | return orig; 343 | }); 344 | return JSON.parse(drvMunged); 345 | } 346 | 347 | async function cacheGetNarinfo(base, hash) { 348 | const narinfoUrl = `${base}/${hash}.narinfo`; 349 | const narinfoText = await fetchOkText(narinfoUrl); 350 | return narinfoParse(narinfoText); 351 | } 352 | 353 | function cacheFileUrl(base, narinfo) { 354 | if (/\w+:\/\//.test(narinfo.url)) { 355 | return narinfo.url; 356 | } 357 | return `${base}/${narinfo.url}`; 358 | } 359 | 360 | function cacheCheckSupportedCompression(narinfo) { 361 | if (!narinfo.compression in DECOMPRESS_METHODS) throw new Error(`narinfo unsupported compression ${narinfo.compression}`); 362 | } 363 | 364 | async function cacheGetFile(base, narinfo) { 365 | const fileUrl = cacheFileUrl(base, narinfo); 366 | const fileBuf = await fetchOkBuf(fileUrl); 367 | if (fileBuf.byteLength !== narinfo.fileSize) throw new Error(`compressed nar ${fileBuf.byteLength} bytes, expected ${narinfo.fileSize}`); 368 | return fileBuf; 369 | } 370 | 371 | async function cacheDecompressFile(fileBuf, narinfo) { 372 | const narBuf = await DECOMPRESS_METHODS[narinfo.compression](fileBuf, narinfo.narSize); 373 | if (narBuf.byteLength !== narinfo.narSize) throw new Error(`nar ${narBuf.byteLength} bytes, expected ${narinfo.narSize}`); 374 | return narBuf; 375 | } 376 | 377 | function dbParse(dumpText) { 378 | const db = {}; 379 | const linePattern = /^(.*)\n/gm; 380 | while (true) { 381 | const m = linePattern.exec(dumpText); 382 | if (!m) break; 383 | const path = m[1]; 384 | const narHash = linePattern.exec(dumpText)[1]; 385 | const narSize = +linePattern.exec(dumpText)[1]; 386 | const deriver = linePattern.exec(dumpText)[1]; 387 | const numReferences = +linePattern.exec(dumpText)[1]; 388 | const references = new Array(numReferences); 389 | for (let i = 0; i < numReferences; i++) { 390 | references[i] = linePattern.exec(dumpText)[1]; 391 | } 392 | db[path] = {narHash, narSize, deriver, references}; 393 | } 394 | return db; 395 | } 396 | 397 | function dbReferrers(db) { 398 | const referrers = {}; 399 | for (const path in db) { 400 | for (const reference in db[path].references) { 401 | if (!(reference in referrers)) { 402 | referrers[reference] = []; 403 | } 404 | referrers[reference].push(path); 405 | } 406 | } 407 | return referrers; 408 | } 409 | 410 | function uiNodify(text, pattern, replacer) { 411 | const frag = document.createDocumentFragment(); 412 | let lastEnd = 0; 413 | while (true) { 414 | const m = pattern.exec(text); 415 | if (!m) break; 416 | const node = replacer(m, text); 417 | if (!node) continue; 418 | if (lastEnd < m.index) { 419 | frag.appendChild(document.createTextNode(text.slice(lastEnd, m.index))); 420 | } 421 | frag.appendChild(node); 422 | lastEnd = pattern.lastIndex; 423 | } 424 | if (lastEnd < text.length) { 425 | frag.appendChild(document.createTextNode(text.slice(lastEnd))); 426 | } 427 | return frag; 428 | } 429 | -------------------------------------------------------------------------------- /view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | view 6 | 61 |

62 | 66 |

67 | 91 |

92 | 96 |

97 | 142 | 143 | 531 | -------------------------------------------------------------------------------- /xz-embedded.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wh0/nix-cache-view/efe5bcaae23a768bf9719ef357e9ed516ea6054a/xz-embedded.wasm --------------------------------------------------------------------------------