gds-lens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,143 @@
1
+ // Turns the bytes read off disk into the bytes the viewer parses: a gzipped
2
+ // layout (`.gds.gz` and friends) is expanded here, and everything downstream --
3
+ // the webview, the parse Worker, the wasm module's own GDSII/OASIS sniffing --
4
+ // sees exactly what an uncompressed file would have produced.
5
+ //
6
+ // Expanding out here rather than inside the wasm module is a memory decision.
7
+ // The viewer's entire size budget is the 32-bit wasm heap: the file's bytes and
8
+ // the flattened geometry built from them both have to fit in 4 GB (see "Limits"
9
+ // in README.md), and that heap is the one address space that can least afford a
10
+ // second full copy of the file. JavaScript has no such ceiling, so doing it
11
+ // here spends the compressed copy and the decompressor's scratch space where
12
+ // they cost nothing, and hands the wasm side a single buffer.
13
+ //
14
+ // Built on the platform's DecompressionStream rather than Node's zlib, so the
15
+ // same code runs in a browser, a Worker and Node unchanged. That makes
16
+ // expansion async, which is the one shape change from a zlib version; it also
17
+ // makes the size cap ours to enforce, since there is no `maxOutputLength` to
18
+ // hand off to -- that's what the running total in gunzip() is for.
19
+ //
20
+ // No imports, no DOM and no wasm, the same shape as marker-parsers.js and
21
+ // load-errors.js. Published on its own as `gds-lens/layout-bytes`.
22
+ "use strict";
23
+
24
+ // gzip's magic number: RFC 1952's ID1/ID2. Detection is by content rather than
25
+ // by a ".gz" on the name, matching how GDSII vs OASIS is already decided (see
26
+ // detect_format in gds_common.hpp). That way a layout named with an unexpected
27
+ // extension still loads, and so does a plain ".gds" that is secretly gzipped --
28
+ // which is how these arrive out of some flows.
29
+ const GZIP_ID1 = 0x1f;
30
+ const GZIP_ID2 = 0x8b;
31
+
32
+ // Smallest possible gzip member: a 10-byte header plus an 8-byte trailer.
33
+ const GZIP_MIN_BYTES = 18;
34
+
35
+ function looksGzipped(bytes) {
36
+ return !!bytes && bytes.length >= 2 && bytes[0] === GZIP_ID1 && bytes[1] === GZIP_ID2;
37
+ }
38
+
39
+ // The uncompressed size gzip records in its trailer (RFC 1952's ISIZE), or null
40
+ // if the input is too short to hold one.
41
+ //
42
+ // Only ever used to word a message. It's stored modulo 2^32, and for a
43
+ // multi-member file it describes the last member alone, so it is a hint about
44
+ // the file rather than a fact about it -- which is exactly why it isn't what
45
+ // enforces the limit below. The running total in gunzip() does that, by stopping
46
+ // the moment the output overruns instead of trusting what the file claims about
47
+ // itself.
48
+ //
49
+ // Read by hand rather than with Buffer.readUInt32LE: the caller's bytes come
50
+ // from vscode.workspace.fs.readFile, which yields a plain Uint8Array. The top
51
+ // byte is multiplied rather than shifted, since `<< 24` would make sizes over
52
+ // 2 GB come back negative.
53
+ function gzipStoredSize(bytes) {
54
+ if (!bytes || bytes.length < GZIP_MIN_BYTES) return null;
55
+ const end = bytes.length;
56
+ return bytes[end - 4] + bytes[end - 3] * 0x100 + bytes[end - 2] * 0x10000 +
57
+ bytes[end - 1] * 0x1000000;
58
+ }
59
+
60
+ // Expands one gzip buffer, refusing to accumulate more than `cap` bytes.
61
+ //
62
+ // The write side is started but deliberately not awaited before reading. A
63
+ // DecompressionStream applies backpressure, so `writer.write()` of a whole
64
+ // layout doesn't settle until the read loop below has drained it -- awaiting it
65
+ // first would deadlock. Its rejection is swallowed because every failure the
66
+ // stream can have (a bad header, a truncated body, corrupt deflate data) also
67
+ // surfaces from `reader.read()`, which is where the throw wants to come from;
68
+ // without the catch, the same error would additionally go unhandled here.
69
+ async function gunzip(bytes, cap) {
70
+ const stream = new DecompressionStream("gzip");
71
+ const writer = stream.writable.getWriter();
72
+ const pump = writer.write(bytes).then(() => writer.close()).catch(() => {});
73
+
74
+ const reader = stream.readable.getReader();
75
+ const chunks = [];
76
+ let total = 0;
77
+ for (;;) {
78
+ const { done, value } = await reader.read();
79
+ if (done) break;
80
+ total += value.byteLength;
81
+ // `>` rather than `>=`: a file expanding to exactly the cap still fits.
82
+ if (total > cap) {
83
+ await reader.cancel();
84
+ await pump;
85
+ const err = new Error(`expands past the ${cap} byte limit`);
86
+ err.tooLarge = true;
87
+ throw err;
88
+ }
89
+ chunks.push(value);
90
+ }
91
+ await pump;
92
+
93
+ // A layout that arrived in one chunk is handed straight back rather than
94
+ // copied into a second buffer of the same size. At these sizes the copy is
95
+ // the expensive part, not the bookkeeping.
96
+ if (chunks.length === 1) return chunks[0];
97
+ const out = new Uint8Array(total);
98
+ let at = 0;
99
+ for (const chunk of chunks) {
100
+ out.set(chunk, at);
101
+ at += chunk.byteLength;
102
+ }
103
+ return out;
104
+ }
105
+
106
+ // Uncompressed layout bytes, whatever the input was.
107
+ //
108
+ // { ok: true, bytes, gzipped, storedSize }
109
+ // { ok: false, reason: "too-large" | "corrupt", storedSize, limit, detail }
110
+ //
111
+ // Not-gzipped input is passed straight back, untouched and uncopied, so the
112
+ // ordinary path costs one two-byte comparison. maxBytes caps what a compressed
113
+ // file is allowed to expand to; pass a non-finite value for no cap. The reason
114
+ // codes are split because the two failures need different things said about
115
+ // them -- one is about this machine's limits, the other about the file being
116
+ // broken or half-written -- and the prose for both lives with the caller's other
117
+ // messages rather than here.
118
+ async function decodeLayoutBytes(bytes, maxBytes) {
119
+ if (!looksGzipped(bytes)) return { ok: true, bytes: bytes, gzipped: false, storedSize: null };
120
+
121
+ const storedSize = gzipStoredSize(bytes);
122
+ const cap = Number.isFinite(maxBytes) ? maxBytes : Infinity;
123
+ try {
124
+ return { ok: true, bytes: await gunzip(bytes, cap), gzipped: true, storedSize: storedSize };
125
+ } catch (err) {
126
+ // The `tooLarge` flag is the cap above being hit -- the one failure here
127
+ // that's about size rather than about the data. Everything else is the
128
+ // decompressor refusing the stream itself, which means the compressed
129
+ // data can't be read at all.
130
+ return {
131
+ ok: false,
132
+ reason: err && err.tooLarge ? "too-large" : "corrupt",
133
+ storedSize: storedSize,
134
+ // Reported back rather than left for the caller to remember: it is
135
+ // the one number a "too large" message needs, and the caller that
136
+ // passed it is not always the one wording the failure.
137
+ limit: cap,
138
+ detail: err && err.message ? err.message : String(err)
139
+ };
140
+ }
141
+ }
142
+
143
+ export { looksGzipped, gzipStoredSize, decodeLayoutBytes };
@@ -0,0 +1,64 @@
1
+ // Turns the engine-level failures a layout load can hit into text a layout
2
+ // engineer can act on.
3
+ //
4
+ // Imported by both sides of the load: the main thread (viewer.js) and the
5
+ // parse Worker, which are separate script contexts and each get their own
6
+ // copy through the bundler. No imports of its own, and no DOM or wasm, so
7
+ // Node unit tests import it directly too.
8
+ //
9
+ // Running out of room in the wasm heap surfaces as one of a handful of
10
+ // unhelpful strings depending on which allocation happened to be the one that
11
+ // failed -- "memory access out of bounds" when a bounds check catches it
12
+ // first, "Aborted()" when Emscripten's allocator gives up, a RangeError when
13
+ // it's a JS-side typed array. They all mean the same thing to the user.
14
+ const OOM_PATTERN = /memory access out of bounds|Cannot enlarge memory|Aborted\(|out of memory|Array buffer allocation failed|Invalid (typed )?array length/i;
15
+
16
+ const OOM_MESSAGE =
17
+ "Out of memory: this layout is too large to open.\n\n" +
18
+ "The viewer parses layouts in a 32-bit WebAssembly module, so the whole " +
19
+ "flattened design has to fit in 4 GB. Layouts that reuse cells (arrays " +
20
+ "and repeated placements) go much further than fully flattened ones, " +
21
+ "because a repeated cell is drawn as GPU instances instead of being " +
22
+ "copied for every placement.";
23
+
24
+ // `prefix` labels non-memory failures with where they came from (the Worker
25
+ // passes one); out-of-memory keeps its own wording either way, since the
26
+ // cause is the layout rather than the component that happened to notice.
27
+ function describeLoadFailure(err, prefix) {
28
+ const text = err && err.message ? err.message : String(err);
29
+ if (OOM_PATTERN.test(text)) return `${OOM_MESSAGE}\n\n(${text})`;
30
+ return prefix ? `${prefix}: ${text}` : text;
31
+ }
32
+
33
+ function isOutOfMemory(err) {
34
+ return OOM_PATTERN.test(err && err.message ? err.message : String(err));
35
+ }
36
+
37
+ // Wording for a failed gzip expansion (the {ok:false} half of what
38
+ // decodeLayoutBytes returns; see layout-bytes.js, which deliberately leaves
39
+ // the prose to its caller).
40
+ //
41
+ // The two reasons need different things said. "too-large" is about this
42
+ // machine: the file is fine, there is nowhere to put it, and the number worth
43
+ // quoting is the limit. "corrupt" is about the file: it is truncated or was
44
+ // written by something that stopped halfway, and no limit would have helped.
45
+ function describeDecodeFailure(result) {
46
+ if (!result || result.ok) return "";
47
+ if (result.reason === "too-large") {
48
+ const gb = (result.limit / (1024 * 1024 * 1024)).toFixed(1);
49
+ const claimed = result.storedSize
50
+ ? ` The file's own trailer claims ${(result.storedSize / (1024 * 1024)).toFixed(0)} MB uncompressed.`
51
+ : "";
52
+ return "This compressed layout is too large to expand.\n\n"
53
+ + `Expanding stopped at the ${gb} GB limit.${claimed} The viewer parses `
54
+ + "layouts in a 32-bit WebAssembly module, so the expanded file and the "
55
+ + "geometry built from it both have to fit in one 4 GB address space. "
56
+ + "An uncompressed copy of the same design will not help; a design that "
57
+ + "reuses cells rather than flattening them will.";
58
+ }
59
+ return "This layout looks gzipped, but the compressed data could not be read.\n\n"
60
+ + "Usually a truncated or half-written file -- a copy that was interrupted, "
61
+ + `or a download that stopped early. (${result.detail})`;
62
+ }
63
+
64
+ export { describeLoadFailure, isOutOfMemory, describeDecodeFailure };