@opengeni/contracts 0.40.0 → 0.44.1
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.
- package/dist/atlassian.d.ts +278 -0
- package/dist/atlassian.js +55 -0
- package/dist/atlassian.js.map +1 -0
- package/dist/chunk-52SGB2QO.js +688 -0
- package/dist/chunk-52SGB2QO.js.map +1 -0
- package/dist/{chunk-7JZMI22I.js → chunk-5UDNP7AO.js} +34 -2
- package/dist/chunk-5UDNP7AO.js.map +1 -0
- package/dist/chunk-BVGIYW7D.js +11 -0
- package/dist/chunk-BVGIYW7D.js.map +1 -0
- package/dist/chunk-CADSX6UN.js +1145 -0
- package/dist/chunk-CADSX6UN.js.map +1 -0
- package/dist/chunk-D2HKF2EF.js +1460 -0
- package/dist/chunk-D2HKF2EF.js.map +1 -0
- package/dist/chunk-FJBG5E4O.js +194 -0
- package/dist/chunk-FJBG5E4O.js.map +1 -0
- package/dist/chunk-H5BVCZKF.js +382 -0
- package/dist/chunk-H5BVCZKF.js.map +1 -0
- package/dist/chunk-LSB7GRLG.js +779 -0
- package/dist/chunk-LSB7GRLG.js.map +1 -0
- package/dist/chunk-MYBZRDTY.js +340 -0
- package/dist/chunk-MYBZRDTY.js.map +1 -0
- package/dist/chunk-RIY7GYQS.js +1204 -0
- package/dist/chunk-RIY7GYQS.js.map +1 -0
- package/dist/chunk-TZ6YEYN4.js +386 -0
- package/dist/chunk-TZ6YEYN4.js.map +1 -0
- package/dist/chunk-VA22KRHS.js +15177 -0
- package/dist/chunk-VA22KRHS.js.map +1 -0
- package/dist/chunk-Y3MLSLB4.js +392 -0
- package/dist/chunk-Y3MLSLB4.js.map +1 -0
- package/dist/chunk-YRKRZ6OF.js +134 -0
- package/dist/chunk-YRKRZ6OF.js.map +1 -0
- package/dist/company-profile.d.ts +629 -0
- package/dist/document-artifact-commands.d.ts +282 -0
- package/dist/document-artifact-commands.js +54 -0
- package/dist/document-artifact-commands.js.map +1 -0
- package/dist/document-artifact-query.d.ts +2 -0
- package/dist/document-artifact-query.js +54 -0
- package/dist/document-artifact-query.js.map +1 -0
- package/dist/editable-artifact-binary.d.ts +56 -0
- package/dist/editable-artifact-causal-frontier.d.ts +12 -0
- package/dist/editable-artifact-causal-frontier.js +18 -0
- package/dist/editable-artifact-causal-frontier.js.map +1 -0
- package/dist/editable-artifact-codec-registry.d.ts +48 -0
- package/dist/editable-artifact-codec-registry.js +20 -0
- package/dist/editable-artifact-codec-registry.js.map +1 -0
- package/dist/editable-artifact-committed-transaction.d.ts +35 -0
- package/dist/editable-artifact-committed-transaction.js +19 -0
- package/dist/editable-artifact-committed-transaction.js.map +1 -0
- package/dist/editable-artifact-live.d.ts +276 -0
- package/dist/editable-artifact-live.js +907 -0
- package/dist/editable-artifact-live.js.map +1 -0
- package/dist/editable-artifact-serialized-commit.d.ts +53 -0
- package/dist/editable-artifact-serialized-commit.js +26 -0
- package/dist/editable-artifact-serialized-commit.js.map +1 -0
- package/dist/editable-artifact-versions.d.ts +6 -0
- package/dist/editable-artifact-versions.js +11 -0
- package/dist/editable-artifact-versions.js.map +1 -0
- package/dist/editable-artifacts.d.ts +46 -0
- package/dist/editable-artifacts.js +236 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/event-preview.d.ts +1 -1
- package/dist/google-drive.d.ts +114 -2
- package/dist/google-drive.js +70 -14
- package/dist/google-drive.js.map +1 -1
- package/dist/image-generation.d.ts +70 -0
- package/dist/index.d.ts +4460 -155
- package/dist/index.js +1127 -30
- package/dist/interaction.d.ts +6895 -0
- package/dist/memory-slack-delivery.d.ts +397 -0
- package/dist/presentation-artifact-commands.d.ts +349 -0
- package/dist/presentation-artifact-commands.js +66 -0
- package/dist/presentation-artifact-commands.js.map +1 -0
- package/dist/presentation-artifact-query.d.ts +2 -0
- package/dist/presentation-artifact-query.js +66 -0
- package/dist/presentation-artifact-query.js.map +1 -0
- package/dist/retained-output.d.ts +24 -0
- package/dist/sandbox-snapshots.d.ts +9 -0
- package/dist/scoped-knowledge.d.ts +14 -13
- package/dist/slack-bot-scopes.d.ts +4 -0
- package/dist/slack-bot-scopes.js +3 -1
- package/dist/spreadsheet-artifact-commands.d.ts +101 -0
- package/dist/spreadsheet-artifact-commands.js +27 -0
- package/dist/spreadsheet-artifact-commands.js.map +1 -0
- package/dist/spreadsheet-artifact-date.d.ts +4 -0
- package/dist/spreadsheet-artifact-query.d.ts +120 -0
- package/dist/spreadsheet-artifact-query.js +48 -0
- package/dist/spreadsheet-artifact-query.js.map +1 -0
- package/dist/tool-catalog.d.ts +576 -0
- package/dist/video-generation.d.ts +532 -0
- package/dist/video-generation.js +51 -0
- package/dist/video-generation.js.map +1 -0
- package/dist/workspace-learning-policy.d.ts +208 -0
- package/package.json +61 -1
- package/src/atlassian.ts +160 -0
- package/src/company-profile.ts +306 -0
- package/src/document-artifact-commands.ts +1709 -0
- package/src/document-artifact-query.ts +2 -0
- package/src/editable-artifact-binary.ts +396 -0
- package/src/editable-artifact-causal-frontier.ts +154 -0
- package/src/editable-artifact-codec-registry.ts +261 -0
- package/src/editable-artifact-committed-transaction.ts +478 -0
- package/src/editable-artifact-live.ts +1379 -0
- package/src/editable-artifact-serialized-commit.ts +494 -0
- package/src/editable-artifact-versions.ts +8 -0
- package/src/editable-artifacts.ts +473 -0
- package/src/google-drive.ts +68 -15
- package/src/image-generation.ts +106 -0
- package/src/index.ts +2326 -325
- package/src/interaction.ts +2292 -0
- package/src/memory-slack-delivery.ts +183 -0
- package/src/presentation-artifact-commands.ts +2035 -0
- package/src/presentation-artifact-query.ts +2 -0
- package/src/retained-output.ts +149 -2
- package/src/sandbox-snapshots.ts +28 -1
- package/src/scoped-knowledge.ts +18 -4
- package/src/slack-bot-scopes.ts +34 -1
- package/src/spreadsheet-artifact-commands.ts +999 -0
- package/src/spreadsheet-artifact-date.ts +21 -0
- package/src/spreadsheet-artifact-query.ts +1411 -0
- package/src/tool-catalog.ts +334 -0
- package/src/video-generation.ts +394 -0
- package/src/workspace-learning-policy.ts +233 -0
- package/dist/chunk-5HR2GPSC.js +0 -11031
- package/dist/chunk-5HR2GPSC.js.map +0 -1
- package/dist/chunk-7JZMI22I.js.map +0 -1
|
@@ -0,0 +1,1411 @@
|
|
|
1
|
+
import { spreadsheetSheetId, type EditableArtifactStableId } from "./spreadsheet-artifact-commands";
|
|
2
|
+
import {
|
|
3
|
+
canonicalSpreadsheetDateFromMilliseconds,
|
|
4
|
+
canonicalSpreadsheetDateMilliseconds,
|
|
5
|
+
} from "./spreadsheet-artifact-date";
|
|
6
|
+
|
|
7
|
+
/** Canonical private kernel query/projection ABI shared by browser and native bindings. */
|
|
8
|
+
export const SPREADSHEET_ARTIFACT_QUERY_VERSION = 1 as const;
|
|
9
|
+
export const SPREADSHEET_ARTIFACT_QUERY_MAX_BYTES = 68;
|
|
10
|
+
export const SPREADSHEET_ARTIFACT_PROJECTION_MAX_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
export const SPREADSHEET_ARTIFACT_VIEWPORT_MAX_AREA = 1_048_576;
|
|
12
|
+
export const SPREADSHEET_ARTIFACT_VIEWPORT_MAX_CELLS = 262_144;
|
|
13
|
+
export const SPREADSHEET_ARTIFACT_METADATA_MAX_SHEETS = 10_000;
|
|
14
|
+
export const SPREADSHEET_ARTIFACT_PROJECTION_MAX_STRING_BYTES = 4 * 1024 * 1024;
|
|
15
|
+
export const SPREADSHEET_ARTIFACT_VIEWPORT_MIN_RESPONSE_BYTES = 92;
|
|
16
|
+
export const SPREADSHEET_ARTIFACT_METADATA_MIN_RESPONSE_BYTES = 48;
|
|
17
|
+
|
|
18
|
+
const QUERY_MAGIC = new TextEncoder().encode("OGAKQ001");
|
|
19
|
+
const PROJECTION_MAGIC = new TextEncoder().encode("OGAKV001");
|
|
20
|
+
const QUERY_HEADER_BYTES = 28;
|
|
21
|
+
const PROJECTION_HEADER_BYTES = 36;
|
|
22
|
+
const CHECKSUM_BYTES = 8;
|
|
23
|
+
const VIEWPORT_QUERY_PAYLOAD_BYTES = 32;
|
|
24
|
+
const VIEWPORT_PROJECTION_PREFIX_BYTES = 48;
|
|
25
|
+
const METADATA_PROJECTION_PREFIX_BYTES = 4;
|
|
26
|
+
const GENERATIONS_FLAG = 1;
|
|
27
|
+
const UINT32_MAX = 0xffff_ffff;
|
|
28
|
+
const UINT64_MAX = 0xffff_ffff_ffff_ffffn;
|
|
29
|
+
const FNV_OFFSET = 0xcbf29ce484222325n;
|
|
30
|
+
const FNV_PRIME = 0x100000001b3n;
|
|
31
|
+
const textEncoder = new TextEncoder();
|
|
32
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
33
|
+
|
|
34
|
+
export type SpreadsheetArtifactViewportQuery = Readonly<{
|
|
35
|
+
sheetId: EditableArtifactStableId;
|
|
36
|
+
startRow: number;
|
|
37
|
+
startColumn: number;
|
|
38
|
+
rowCount: number;
|
|
39
|
+
columnCount: number;
|
|
40
|
+
maxCells: number;
|
|
41
|
+
/** Entire OGAKV001 envelope, not merely its payload. */
|
|
42
|
+
maxBytes: number;
|
|
43
|
+
}>;
|
|
44
|
+
|
|
45
|
+
export type SpreadsheetArtifactMetadataQuery = Readonly<{
|
|
46
|
+
maxSheets: number;
|
|
47
|
+
/** Entire OGAKV001 envelope, not merely its payload. */
|
|
48
|
+
maxBytes: number;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
export type SpreadsheetArtifactKernelQuery =
|
|
52
|
+
| Readonly<{ kind: "viewport"; query: SpreadsheetArtifactViewportQuery }>
|
|
53
|
+
| Readonly<{
|
|
54
|
+
kind: "workbook-metadata";
|
|
55
|
+
query: SpreadsheetArtifactMetadataQuery;
|
|
56
|
+
}>;
|
|
57
|
+
|
|
58
|
+
export type SpreadsheetArtifactFormulaError =
|
|
59
|
+
| "null"
|
|
60
|
+
| "divide_by_zero"
|
|
61
|
+
| "value"
|
|
62
|
+
| "reference"
|
|
63
|
+
| "name"
|
|
64
|
+
| "number"
|
|
65
|
+
| "not_available"
|
|
66
|
+
| "spill"
|
|
67
|
+
| "calculation"
|
|
68
|
+
| Readonly<{ custom: string }>;
|
|
69
|
+
|
|
70
|
+
export type SpreadsheetArtifactProjectedCellValue =
|
|
71
|
+
| Readonly<{ kind: "empty" }>
|
|
72
|
+
| Readonly<{ kind: "boolean"; value: boolean }>
|
|
73
|
+
| Readonly<{ kind: "number"; value: number }>
|
|
74
|
+
| Readonly<{ kind: "date"; value: string }>
|
|
75
|
+
| Readonly<{ kind: "text"; value: string }>
|
|
76
|
+
| Readonly<{ kind: "error"; value: SpreadsheetArtifactFormulaError }>;
|
|
77
|
+
|
|
78
|
+
export type SpreadsheetArtifactProjectedCell = Readonly<{
|
|
79
|
+
/** Absolute zero-based row, reconstructed from the relative wire coordinate. */
|
|
80
|
+
row: number;
|
|
81
|
+
/** Absolute zero-based column, reconstructed from the relative wire coordinate. */
|
|
82
|
+
column: number;
|
|
83
|
+
formula: string | null;
|
|
84
|
+
value: SpreadsheetArtifactProjectedCellValue;
|
|
85
|
+
}>;
|
|
86
|
+
|
|
87
|
+
/** Complete and non-truncated. A limit breach is an error, never a partial response. */
|
|
88
|
+
export type SpreadsheetArtifactViewportProjection = Readonly<{
|
|
89
|
+
revision: bigint;
|
|
90
|
+
sheetId: EditableArtifactStableId;
|
|
91
|
+
generationId: EditableArtifactStableId | null;
|
|
92
|
+
startRow: number;
|
|
93
|
+
startColumn: number;
|
|
94
|
+
rowCount: number;
|
|
95
|
+
columnCount: number;
|
|
96
|
+
cells: readonly SpreadsheetArtifactProjectedCell[];
|
|
97
|
+
}>;
|
|
98
|
+
|
|
99
|
+
export type SpreadsheetArtifactUsedBounds = Readonly<{
|
|
100
|
+
startRow: number;
|
|
101
|
+
startColumn: number;
|
|
102
|
+
endRow: number;
|
|
103
|
+
endColumn: number;
|
|
104
|
+
}>;
|
|
105
|
+
|
|
106
|
+
export type SpreadsheetArtifactSheetMetadata = Readonly<{
|
|
107
|
+
sheetId: EditableArtifactStableId;
|
|
108
|
+
generationId: EditableArtifactStableId | null;
|
|
109
|
+
name: string;
|
|
110
|
+
usedBounds: SpreadsheetArtifactUsedBounds | null;
|
|
111
|
+
}>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Version 1 deliberately models no row/column dimensions, hidden state, or
|
|
115
|
+
* merges. It also carries no style projection. These are false capability
|
|
116
|
+
* facts, not omitted authoritative data.
|
|
117
|
+
*/
|
|
118
|
+
export type SpreadsheetArtifactModeledFeatures = Readonly<{
|
|
119
|
+
dimensions: false;
|
|
120
|
+
hidden: false;
|
|
121
|
+
merges: false;
|
|
122
|
+
}>;
|
|
123
|
+
|
|
124
|
+
/** Complete ordered workbook catalog; never a truncated prefix. */
|
|
125
|
+
export type SpreadsheetArtifactMetadataProjection = Readonly<{
|
|
126
|
+
revision: bigint;
|
|
127
|
+
modeledFeatures: SpreadsheetArtifactModeledFeatures;
|
|
128
|
+
sheets: readonly SpreadsheetArtifactSheetMetadata[];
|
|
129
|
+
}>;
|
|
130
|
+
|
|
131
|
+
export type SpreadsheetArtifactKernelProjection =
|
|
132
|
+
| Readonly<{
|
|
133
|
+
kind: "viewport";
|
|
134
|
+
projection: SpreadsheetArtifactViewportProjection;
|
|
135
|
+
}>
|
|
136
|
+
| Readonly<{
|
|
137
|
+
kind: "workbook-metadata";
|
|
138
|
+
projection: SpreadsheetArtifactMetadataProjection;
|
|
139
|
+
}>;
|
|
140
|
+
|
|
141
|
+
export function encodeSpreadsheetViewportKernelQuery(
|
|
142
|
+
input: SpreadsheetArtifactViewportQuery,
|
|
143
|
+
): Uint8Array {
|
|
144
|
+
const query = normalizeViewportQuery(input);
|
|
145
|
+
const payload = new FixedWriter(VIEWPORT_QUERY_PAYLOAD_BYTES);
|
|
146
|
+
payload.stableId(query.sheetId);
|
|
147
|
+
payload.u32(query.startRow);
|
|
148
|
+
payload.u32(query.startColumn);
|
|
149
|
+
payload.u32(query.rowCount);
|
|
150
|
+
payload.u32(query.columnCount);
|
|
151
|
+
return encodeQueryFrame(0, query.maxCells, query.maxBytes, payload.finish());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function encodeSpreadsheetMetadataKernelQuery(
|
|
155
|
+
input: SpreadsheetArtifactMetadataQuery,
|
|
156
|
+
): Uint8Array {
|
|
157
|
+
const query = normalizeMetadataQuery(input);
|
|
158
|
+
return encodeQueryFrame(1, query.maxSheets, query.maxBytes, new Uint8Array());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function encodeSpreadsheetArtifactKernelQuery(
|
|
162
|
+
input: SpreadsheetArtifactKernelQuery,
|
|
163
|
+
): Uint8Array {
|
|
164
|
+
const envelope = exactRecord(input, "spreadsheet kernel query");
|
|
165
|
+
exactKeys(envelope, ["kind", "query"], "spreadsheet kernel query");
|
|
166
|
+
if (envelope.kind === "viewport") {
|
|
167
|
+
return encodeSpreadsheetViewportKernelQuery(envelope.query as SpreadsheetArtifactViewportQuery);
|
|
168
|
+
}
|
|
169
|
+
if (envelope.kind === "workbook-metadata") {
|
|
170
|
+
return encodeSpreadsheetMetadataKernelQuery(envelope.query as SpreadsheetArtifactMetadataQuery);
|
|
171
|
+
}
|
|
172
|
+
throw queryError("unknown spreadsheet kernel query kind");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function decodeSpreadsheetArtifactKernelQuery(
|
|
176
|
+
bytes: Uint8Array,
|
|
177
|
+
): SpreadsheetArtifactKernelQuery {
|
|
178
|
+
const frame = decodeQueryFrame(bytes);
|
|
179
|
+
if (frame.kind === 0) {
|
|
180
|
+
if (frame.payload.byteLength < VIEWPORT_QUERY_PAYLOAD_BYTES) {
|
|
181
|
+
throw queryError("spreadsheet viewport query is truncated");
|
|
182
|
+
}
|
|
183
|
+
if (frame.payload.byteLength > VIEWPORT_QUERY_PAYLOAD_BYTES) {
|
|
184
|
+
throw queryError("spreadsheet viewport query contains trailing bytes");
|
|
185
|
+
}
|
|
186
|
+
const reader = new Reader(frame.payload);
|
|
187
|
+
const query = normalizeViewportQuery({
|
|
188
|
+
sheetId: reader.stableId("viewport sheet id"),
|
|
189
|
+
startRow: reader.u32(),
|
|
190
|
+
startColumn: reader.u32(),
|
|
191
|
+
rowCount: reader.u32(),
|
|
192
|
+
columnCount: reader.u32(),
|
|
193
|
+
maxCells: frame.maxItems,
|
|
194
|
+
maxBytes: frame.maxBytes,
|
|
195
|
+
});
|
|
196
|
+
reader.done("spreadsheet viewport query contains trailing bytes");
|
|
197
|
+
return Object.freeze({ kind: "viewport", query });
|
|
198
|
+
}
|
|
199
|
+
if (frame.payload.byteLength !== 0) {
|
|
200
|
+
throw queryError("spreadsheet metadata query contains trailing bytes");
|
|
201
|
+
}
|
|
202
|
+
return Object.freeze({
|
|
203
|
+
kind: "workbook-metadata",
|
|
204
|
+
query: normalizeMetadataQuery({
|
|
205
|
+
maxSheets: frame.maxItems,
|
|
206
|
+
maxBytes: frame.maxBytes,
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function assertCanonicalSpreadsheetArtifactKernelQueryBytes(bytes: Uint8Array): Uint8Array {
|
|
212
|
+
const canonical = encodeSpreadsheetArtifactKernelQuery(
|
|
213
|
+
decodeSpreadsheetArtifactKernelQuery(bytes),
|
|
214
|
+
);
|
|
215
|
+
if (!equalBytes(bytes, canonical)) throw queryError("spreadsheet kernel query is not canonical");
|
|
216
|
+
return bytes;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function encodeSpreadsheetViewportKernelProjection(
|
|
220
|
+
input: SpreadsheetArtifactViewportProjection,
|
|
221
|
+
maximumBytes = SPREADSHEET_ARTIFACT_PROJECTION_MAX_BYTES,
|
|
222
|
+
): Uint8Array {
|
|
223
|
+
const projection = normalizeViewportProjection(input);
|
|
224
|
+
const maximum = effectiveProjectionMaximum(
|
|
225
|
+
maximumBytes,
|
|
226
|
+
SPREADSHEET_ARTIFACT_VIEWPORT_MIN_RESPONSE_BYTES,
|
|
227
|
+
);
|
|
228
|
+
const payload = new DynamicWriter(maximum - PROJECTION_HEADER_BYTES - CHECKSUM_BYTES);
|
|
229
|
+
payload.stableId(projection.sheetId);
|
|
230
|
+
payload.optionalStableId(projection.generationId);
|
|
231
|
+
payload.u32(projection.startRow);
|
|
232
|
+
payload.u32(projection.startColumn);
|
|
233
|
+
payload.u32(projection.rowCount);
|
|
234
|
+
payload.u32(projection.columnCount);
|
|
235
|
+
for (const cell of projection.cells) {
|
|
236
|
+
payload.u32(cell.row - projection.startRow);
|
|
237
|
+
payload.u32(cell.column - projection.startColumn);
|
|
238
|
+
payload.cell(cell);
|
|
239
|
+
}
|
|
240
|
+
return encodeProjectionFrame(
|
|
241
|
+
0,
|
|
242
|
+
projection.revision,
|
|
243
|
+
projection.generationId === null ? 0 : GENERATIONS_FLAG,
|
|
244
|
+
projection.cells.length,
|
|
245
|
+
payload.finish(),
|
|
246
|
+
maximum,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function encodeSpreadsheetMetadataKernelProjection(
|
|
251
|
+
input: SpreadsheetArtifactMetadataProjection,
|
|
252
|
+
maximumBytes = SPREADSHEET_ARTIFACT_PROJECTION_MAX_BYTES,
|
|
253
|
+
): Uint8Array {
|
|
254
|
+
const projection = normalizeMetadataProjection(input);
|
|
255
|
+
const maximum = effectiveProjectionMaximum(
|
|
256
|
+
maximumBytes,
|
|
257
|
+
SPREADSHEET_ARTIFACT_METADATA_MIN_RESPONSE_BYTES,
|
|
258
|
+
);
|
|
259
|
+
const payload = new DynamicWriter(maximum - PROJECTION_HEADER_BYTES - CHECKSUM_BYTES);
|
|
260
|
+
payload.u32(0);
|
|
261
|
+
for (const sheet of projection.sheets) {
|
|
262
|
+
payload.stableId(sheet.sheetId);
|
|
263
|
+
payload.optionalStableId(sheet.generationId);
|
|
264
|
+
payload.string(sheet.name);
|
|
265
|
+
if (sheet.usedBounds === null) {
|
|
266
|
+
payload.u8(0);
|
|
267
|
+
} else {
|
|
268
|
+
payload.u8(1);
|
|
269
|
+
payload.u32(sheet.usedBounds.startRow);
|
|
270
|
+
payload.u32(sheet.usedBounds.startColumn);
|
|
271
|
+
payload.u32(sheet.usedBounds.endRow);
|
|
272
|
+
payload.u32(sheet.usedBounds.endColumn);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const generations = projection.sheets.length > 0 && projection.sheets[0]!.generationId !== null;
|
|
276
|
+
return encodeProjectionFrame(
|
|
277
|
+
1,
|
|
278
|
+
projection.revision,
|
|
279
|
+
generations ? GENERATIONS_FLAG : 0,
|
|
280
|
+
projection.sheets.length,
|
|
281
|
+
payload.finish(),
|
|
282
|
+
maximum,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function encodeSpreadsheetArtifactKernelProjection(
|
|
287
|
+
input: SpreadsheetArtifactKernelProjection,
|
|
288
|
+
maximumBytes = SPREADSHEET_ARTIFACT_PROJECTION_MAX_BYTES,
|
|
289
|
+
): Uint8Array {
|
|
290
|
+
const envelope = exactRecord(input, "spreadsheet kernel projection");
|
|
291
|
+
exactKeys(envelope, ["kind", "projection"], "spreadsheet kernel projection");
|
|
292
|
+
if (envelope.kind === "viewport") {
|
|
293
|
+
return encodeSpreadsheetViewportKernelProjection(
|
|
294
|
+
envelope.projection as SpreadsheetArtifactViewportProjection,
|
|
295
|
+
maximumBytes,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
if (envelope.kind === "workbook-metadata") {
|
|
299
|
+
return encodeSpreadsheetMetadataKernelProjection(
|
|
300
|
+
envelope.projection as SpreadsheetArtifactMetadataProjection,
|
|
301
|
+
maximumBytes,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
throw projectionError("unknown spreadsheet kernel projection kind");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function decodeSpreadsheetArtifactKernelProjection(
|
|
308
|
+
bytes: Uint8Array,
|
|
309
|
+
): SpreadsheetArtifactKernelProjection {
|
|
310
|
+
const frame = decodeProjectionFrame(bytes);
|
|
311
|
+
if (frame.kind === 0) {
|
|
312
|
+
return Object.freeze({
|
|
313
|
+
kind: "viewport",
|
|
314
|
+
projection: decodeViewportPayload(frame),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return Object.freeze({
|
|
318
|
+
kind: "workbook-metadata",
|
|
319
|
+
projection: decodeMetadataPayload(frame),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function decodeSpreadsheetViewportKernelProjection(
|
|
324
|
+
bytes: Uint8Array,
|
|
325
|
+
expected?: SpreadsheetArtifactViewportQuery,
|
|
326
|
+
): SpreadsheetArtifactViewportProjection {
|
|
327
|
+
const decoded = decodeSpreadsheetArtifactKernelProjection(bytes);
|
|
328
|
+
if (decoded.kind !== "viewport") throw projectionError("expected a viewport projection");
|
|
329
|
+
if (expected !== undefined) {
|
|
330
|
+
const query = normalizeViewportQuery(expected);
|
|
331
|
+
enforceProjectionEnvelopeLimit(bytes, query.maxBytes);
|
|
332
|
+
if (
|
|
333
|
+
decoded.projection.cells.length >
|
|
334
|
+
Math.min(query.maxCells, SPREADSHEET_ARTIFACT_VIEWPORT_MAX_CELLS)
|
|
335
|
+
) {
|
|
336
|
+
throw projectionError("viewport projection exceeds its requested cell limit");
|
|
337
|
+
}
|
|
338
|
+
if (
|
|
339
|
+
decoded.projection.sheetId !== query.sheetId ||
|
|
340
|
+
decoded.projection.startRow !== query.startRow ||
|
|
341
|
+
decoded.projection.startColumn !== query.startColumn ||
|
|
342
|
+
decoded.projection.rowCount !== query.rowCount ||
|
|
343
|
+
decoded.projection.columnCount !== query.columnCount
|
|
344
|
+
) {
|
|
345
|
+
throw projectionError("viewport projection does not match its query");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return decoded.projection;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function decodeSpreadsheetMetadataKernelProjection(
|
|
352
|
+
bytes: Uint8Array,
|
|
353
|
+
expected?: SpreadsheetArtifactMetadataQuery,
|
|
354
|
+
): SpreadsheetArtifactMetadataProjection {
|
|
355
|
+
const decoded = decodeSpreadsheetArtifactKernelProjection(bytes);
|
|
356
|
+
if (decoded.kind !== "workbook-metadata") {
|
|
357
|
+
throw projectionError("expected a workbook metadata projection");
|
|
358
|
+
}
|
|
359
|
+
if (expected !== undefined) {
|
|
360
|
+
const query = normalizeMetadataQuery(expected);
|
|
361
|
+
enforceProjectionEnvelopeLimit(bytes, query.maxBytes);
|
|
362
|
+
if (
|
|
363
|
+
decoded.projection.sheets.length >
|
|
364
|
+
Math.min(query.maxSheets, SPREADSHEET_ARTIFACT_METADATA_MAX_SHEETS)
|
|
365
|
+
) {
|
|
366
|
+
throw projectionError("metadata projection exceeds its requested sheet limit");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return decoded.projection;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function assertCanonicalSpreadsheetArtifactKernelProjectionBytes(
|
|
373
|
+
bytes: Uint8Array,
|
|
374
|
+
): Uint8Array {
|
|
375
|
+
const canonical = encodeSpreadsheetArtifactKernelProjection(
|
|
376
|
+
decodeSpreadsheetArtifactKernelProjection(bytes),
|
|
377
|
+
);
|
|
378
|
+
if (!equalBytes(bytes, canonical)) {
|
|
379
|
+
throw projectionError("spreadsheet kernel projection is not canonical");
|
|
380
|
+
}
|
|
381
|
+
return bytes;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
type ProjectionFrame = Readonly<{
|
|
385
|
+
kind: 0 | 1;
|
|
386
|
+
flags: number;
|
|
387
|
+
revision: bigint;
|
|
388
|
+
itemCount: number;
|
|
389
|
+
payload: Uint8Array;
|
|
390
|
+
}>;
|
|
391
|
+
|
|
392
|
+
function encodeQueryFrame(
|
|
393
|
+
kind: 0 | 1,
|
|
394
|
+
maxItems: number,
|
|
395
|
+
maxBytes: number,
|
|
396
|
+
payload: Uint8Array,
|
|
397
|
+
): Uint8Array {
|
|
398
|
+
const output = new FixedWriter(QUERY_HEADER_BYTES + payload.byteLength + CHECKSUM_BYTES);
|
|
399
|
+
output.bytes(QUERY_MAGIC);
|
|
400
|
+
output.u16(SPREADSHEET_ARTIFACT_QUERY_VERSION);
|
|
401
|
+
output.u16(0);
|
|
402
|
+
output.u8(kind);
|
|
403
|
+
output.bytes(new Uint8Array(3));
|
|
404
|
+
output.u32(maxItems);
|
|
405
|
+
output.u32(maxBytes);
|
|
406
|
+
output.u32(payload.byteLength);
|
|
407
|
+
output.bytes(payload);
|
|
408
|
+
output.u64(fnv1a64(output.view()));
|
|
409
|
+
const bytes = output.finish();
|
|
410
|
+
if (bytes.byteLength > SPREADSHEET_ARTIFACT_QUERY_MAX_BYTES) {
|
|
411
|
+
throw queryError("spreadsheet kernel query exceeds its byte limit");
|
|
412
|
+
}
|
|
413
|
+
return bytes;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function decodeQueryFrame(bytes: Uint8Array): Readonly<{
|
|
417
|
+
kind: 0 | 1;
|
|
418
|
+
maxItems: number;
|
|
419
|
+
maxBytes: number;
|
|
420
|
+
payload: Uint8Array;
|
|
421
|
+
}> {
|
|
422
|
+
requireBytes(bytes, "spreadsheet kernel query");
|
|
423
|
+
if (bytes.byteLength > SPREADSHEET_ARTIFACT_QUERY_MAX_BYTES) {
|
|
424
|
+
throw queryError("spreadsheet kernel query exceeds its byte limit");
|
|
425
|
+
}
|
|
426
|
+
if (bytes.byteLength < QUERY_HEADER_BYTES + CHECKSUM_BYTES) {
|
|
427
|
+
throw queryError("spreadsheet kernel query is truncated");
|
|
428
|
+
}
|
|
429
|
+
const reader = new Reader(bytes);
|
|
430
|
+
if (!equalBytes(reader.bytes(8), QUERY_MAGIC))
|
|
431
|
+
throw queryError("spreadsheet query magic is invalid");
|
|
432
|
+
const version = reader.u16();
|
|
433
|
+
if (version !== SPREADSHEET_ARTIFACT_QUERY_VERSION) {
|
|
434
|
+
throw queryError(`unsupported spreadsheet query version: ${version}`);
|
|
435
|
+
}
|
|
436
|
+
if (reader.u16() !== 0) throw queryError("spreadsheet query reserved flags must be zero");
|
|
437
|
+
const kindTag = reader.u8();
|
|
438
|
+
if (kindTag > 1) throw queryError("spreadsheet query kind is invalid");
|
|
439
|
+
if (reader.u8() !== 0 || reader.u8() !== 0 || reader.u8() !== 0) {
|
|
440
|
+
throw queryError("spreadsheet query reserved bytes must be zero");
|
|
441
|
+
}
|
|
442
|
+
const maxItems = reader.u32();
|
|
443
|
+
const maxBytes = reader.u32();
|
|
444
|
+
const payloadLength = reader.u32();
|
|
445
|
+
const expected = QUERY_HEADER_BYTES + payloadLength + CHECKSUM_BYTES;
|
|
446
|
+
if (bytes.byteLength < expected) throw queryError("spreadsheet kernel query is truncated");
|
|
447
|
+
if (bytes.byteLength > expected)
|
|
448
|
+
throw queryError("spreadsheet kernel query contains trailing bytes");
|
|
449
|
+
const payload = reader.bytes(payloadLength);
|
|
450
|
+
const checksum = reader.u64();
|
|
451
|
+
reader.done("spreadsheet kernel query contains trailing bytes");
|
|
452
|
+
if (checksum !== fnv1a64(bytes.subarray(0, -CHECKSUM_BYTES))) {
|
|
453
|
+
throw queryError("spreadsheet kernel query checksum does not match");
|
|
454
|
+
}
|
|
455
|
+
return Object.freeze({ kind: kindTag as 0 | 1, maxItems, maxBytes, payload });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function encodeProjectionFrame(
|
|
459
|
+
kind: 0 | 1,
|
|
460
|
+
revision: bigint,
|
|
461
|
+
flags: number,
|
|
462
|
+
itemCount: number,
|
|
463
|
+
payload: Uint8Array,
|
|
464
|
+
maximum: number,
|
|
465
|
+
): Uint8Array {
|
|
466
|
+
const output = new FixedWriter(PROJECTION_HEADER_BYTES + payload.byteLength + CHECKSUM_BYTES);
|
|
467
|
+
output.bytes(PROJECTION_MAGIC);
|
|
468
|
+
output.u16(SPREADSHEET_ARTIFACT_QUERY_VERSION);
|
|
469
|
+
output.u16(flags);
|
|
470
|
+
output.u8(kind);
|
|
471
|
+
output.bytes(new Uint8Array(3));
|
|
472
|
+
output.u64(revision);
|
|
473
|
+
output.u32(itemCount);
|
|
474
|
+
output.u64(BigInt(payload.byteLength));
|
|
475
|
+
output.bytes(payload);
|
|
476
|
+
output.u64(fnv1a64(output.view()));
|
|
477
|
+
const bytes = output.finish();
|
|
478
|
+
if (bytes.byteLength > maximum || bytes.byteLength > SPREADSHEET_ARTIFACT_PROJECTION_MAX_BYTES) {
|
|
479
|
+
throw projectionError("spreadsheet kernel projection exceeds its byte limit");
|
|
480
|
+
}
|
|
481
|
+
return bytes;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function decodeProjectionFrame(bytes: Uint8Array): ProjectionFrame {
|
|
485
|
+
requireBytes(bytes, "spreadsheet kernel projection");
|
|
486
|
+
if (bytes.byteLength > SPREADSHEET_ARTIFACT_PROJECTION_MAX_BYTES) {
|
|
487
|
+
throw projectionError("spreadsheet kernel projection exceeds its byte limit");
|
|
488
|
+
}
|
|
489
|
+
if (bytes.byteLength < PROJECTION_HEADER_BYTES + CHECKSUM_BYTES) {
|
|
490
|
+
throw projectionError("spreadsheet kernel projection is truncated");
|
|
491
|
+
}
|
|
492
|
+
const reader = new Reader(bytes);
|
|
493
|
+
if (!equalBytes(reader.bytes(8), PROJECTION_MAGIC)) {
|
|
494
|
+
throw projectionError("spreadsheet projection magic is invalid");
|
|
495
|
+
}
|
|
496
|
+
const version = reader.u16();
|
|
497
|
+
if (version !== SPREADSHEET_ARTIFACT_QUERY_VERSION) {
|
|
498
|
+
throw projectionError(`unsupported spreadsheet projection version: ${version}`);
|
|
499
|
+
}
|
|
500
|
+
const flags = reader.u16();
|
|
501
|
+
if ((flags & ~GENERATIONS_FLAG) !== 0) {
|
|
502
|
+
throw projectionError("spreadsheet projection flags are noncanonical");
|
|
503
|
+
}
|
|
504
|
+
const kindTag = reader.u8();
|
|
505
|
+
if (kindTag > 1) throw projectionError("spreadsheet projection kind is invalid");
|
|
506
|
+
if (reader.u8() !== 0 || reader.u8() !== 0 || reader.u8() !== 0) {
|
|
507
|
+
throw projectionError("spreadsheet projection reserved bytes must be zero");
|
|
508
|
+
}
|
|
509
|
+
const revision = reader.u64();
|
|
510
|
+
const itemCount = reader.u32();
|
|
511
|
+
const payloadLengthBig = reader.u64();
|
|
512
|
+
if (payloadLengthBig > BigInt(SPREADSHEET_ARTIFACT_PROJECTION_MAX_BYTES)) {
|
|
513
|
+
throw projectionError("spreadsheet projection payload exceeds its byte limit");
|
|
514
|
+
}
|
|
515
|
+
const payloadLength = Number(payloadLengthBig);
|
|
516
|
+
const expected = PROJECTION_HEADER_BYTES + payloadLength + CHECKSUM_BYTES;
|
|
517
|
+
if (bytes.byteLength < expected)
|
|
518
|
+
throw projectionError("spreadsheet kernel projection is truncated");
|
|
519
|
+
if (bytes.byteLength > expected) {
|
|
520
|
+
throw projectionError("spreadsheet kernel projection contains trailing bytes");
|
|
521
|
+
}
|
|
522
|
+
const payload = reader.bytes(payloadLength);
|
|
523
|
+
const checksum = reader.u64();
|
|
524
|
+
reader.done("spreadsheet kernel projection contains trailing bytes");
|
|
525
|
+
if (checksum !== fnv1a64(bytes.subarray(0, -CHECKSUM_BYTES))) {
|
|
526
|
+
throw projectionError("spreadsheet kernel projection checksum does not match");
|
|
527
|
+
}
|
|
528
|
+
return Object.freeze({
|
|
529
|
+
kind: kindTag as 0 | 1,
|
|
530
|
+
flags,
|
|
531
|
+
revision,
|
|
532
|
+
itemCount,
|
|
533
|
+
payload,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function decodeViewportPayload(frame: ProjectionFrame): SpreadsheetArtifactViewportProjection {
|
|
538
|
+
if (frame.itemCount > SPREADSHEET_ARTIFACT_VIEWPORT_MAX_CELLS) {
|
|
539
|
+
throw projectionError("viewport projection exceeds its hard cell limit");
|
|
540
|
+
}
|
|
541
|
+
if (frame.payload.byteLength < VIEWPORT_PROJECTION_PREFIX_BYTES + frame.itemCount * 10) {
|
|
542
|
+
throw projectionError("viewport projection payload is truncated");
|
|
543
|
+
}
|
|
544
|
+
const reader = new Reader(frame.payload);
|
|
545
|
+
const sheetId = reader.stableId("viewport sheet id");
|
|
546
|
+
const generationId = reader.optionalStableId((frame.flags & GENERATIONS_FLAG) !== 0);
|
|
547
|
+
const startRow = reader.u32();
|
|
548
|
+
const startColumn = reader.u32();
|
|
549
|
+
const rowCount = reader.u32();
|
|
550
|
+
const columnCount = reader.u32();
|
|
551
|
+
validateViewportGeometry(startRow, startColumn, rowCount, columnCount);
|
|
552
|
+
const cells: SpreadsheetArtifactProjectedCell[] = [];
|
|
553
|
+
let previousOrdinal = -1;
|
|
554
|
+
for (let index = 0; index < frame.itemCount; index += 1) {
|
|
555
|
+
const relativeRow = reader.u32();
|
|
556
|
+
const relativeColumn = reader.u32();
|
|
557
|
+
if (relativeRow >= rowCount || relativeColumn >= columnCount) {
|
|
558
|
+
throw projectionError("viewport cell lies outside its response extent");
|
|
559
|
+
}
|
|
560
|
+
const ordinal = relativeRow * columnCount + relativeColumn;
|
|
561
|
+
if (!Number.isSafeInteger(ordinal) || ordinal <= previousOrdinal) {
|
|
562
|
+
throw projectionError("viewport cells are not unique strict row-major entries");
|
|
563
|
+
}
|
|
564
|
+
previousOrdinal = ordinal;
|
|
565
|
+
const formula = reader.formula();
|
|
566
|
+
const value = reader.cellValue();
|
|
567
|
+
if (formula === null && value.kind === "empty") {
|
|
568
|
+
throw projectionError("sparse viewport contains an empty cell");
|
|
569
|
+
}
|
|
570
|
+
cells.push(
|
|
571
|
+
Object.freeze({
|
|
572
|
+
row: startRow + relativeRow,
|
|
573
|
+
column: startColumn + relativeColumn,
|
|
574
|
+
formula,
|
|
575
|
+
value,
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
reader.done("viewport projection payload contains trailing bytes");
|
|
580
|
+
return Object.freeze({
|
|
581
|
+
revision: frame.revision,
|
|
582
|
+
sheetId,
|
|
583
|
+
generationId,
|
|
584
|
+
startRow,
|
|
585
|
+
startColumn,
|
|
586
|
+
rowCount,
|
|
587
|
+
columnCount,
|
|
588
|
+
cells: Object.freeze(cells),
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function decodeMetadataPayload(frame: ProjectionFrame): SpreadsheetArtifactMetadataProjection {
|
|
593
|
+
if (frame.itemCount > SPREADSHEET_ARTIFACT_METADATA_MAX_SHEETS) {
|
|
594
|
+
throw projectionError("metadata projection exceeds its hard sheet limit");
|
|
595
|
+
}
|
|
596
|
+
if (frame.payload.byteLength < METADATA_PROJECTION_PREFIX_BYTES + frame.itemCount * 37) {
|
|
597
|
+
throw projectionError("metadata projection payload is truncated");
|
|
598
|
+
}
|
|
599
|
+
const reader = new Reader(frame.payload);
|
|
600
|
+
if (reader.u32() !== 0) {
|
|
601
|
+
throw projectionError("metadata projection contains unknown modeled feature bits");
|
|
602
|
+
}
|
|
603
|
+
const hasGenerations = (frame.flags & GENERATIONS_FLAG) !== 0;
|
|
604
|
+
if (frame.itemCount === 0 && hasGenerations) {
|
|
605
|
+
throw projectionError("empty metadata projection must not set the generations flag");
|
|
606
|
+
}
|
|
607
|
+
const sheets: SpreadsheetArtifactSheetMetadata[] = [];
|
|
608
|
+
const ids = new Set<string>();
|
|
609
|
+
const names = new Set<string>();
|
|
610
|
+
for (let index = 0; index < frame.itemCount; index += 1) {
|
|
611
|
+
const sheetId = reader.stableId("metadata sheet id");
|
|
612
|
+
if (ids.has(sheetId)) throw projectionError("metadata projection contains duplicate sheet ids");
|
|
613
|
+
ids.add(sheetId);
|
|
614
|
+
const generationId = reader.optionalStableId(hasGenerations);
|
|
615
|
+
const name = validateSheetName(reader.string(), "metadata sheet name");
|
|
616
|
+
const foldedName = name.toLowerCase();
|
|
617
|
+
if (names.has(foldedName)) {
|
|
618
|
+
throw projectionError("metadata projection contains duplicate case-insensitive sheet names");
|
|
619
|
+
}
|
|
620
|
+
names.add(foldedName);
|
|
621
|
+
const boundsTag = reader.u8();
|
|
622
|
+
if (boundsTag > 1) throw projectionError("metadata used-bounds tag is invalid");
|
|
623
|
+
let usedBounds: SpreadsheetArtifactUsedBounds | null = null;
|
|
624
|
+
if (boundsTag === 1) {
|
|
625
|
+
const startRow = reader.u32();
|
|
626
|
+
const startColumn = reader.u32();
|
|
627
|
+
const endRow = reader.u32();
|
|
628
|
+
const endColumn = reader.u32();
|
|
629
|
+
if (startRow > endRow || startColumn > endColumn) {
|
|
630
|
+
throw projectionError("metadata used bounds are not ordered");
|
|
631
|
+
}
|
|
632
|
+
usedBounds = Object.freeze({ startRow, startColumn, endRow, endColumn });
|
|
633
|
+
}
|
|
634
|
+
sheets.push(Object.freeze({ sheetId, generationId, name, usedBounds }));
|
|
635
|
+
}
|
|
636
|
+
reader.done("metadata projection payload contains trailing bytes");
|
|
637
|
+
return Object.freeze({
|
|
638
|
+
revision: frame.revision,
|
|
639
|
+
modeledFeatures: MODELED_FEATURES_NONE,
|
|
640
|
+
sheets: Object.freeze(sheets),
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function normalizeViewportQuery(input: unknown): SpreadsheetArtifactViewportQuery {
|
|
645
|
+
const query = exactRecord(input, "spreadsheet viewport query");
|
|
646
|
+
exactKeys(
|
|
647
|
+
query,
|
|
648
|
+
["columnCount", "maxBytes", "maxCells", "rowCount", "sheetId", "startColumn", "startRow"],
|
|
649
|
+
"spreadsheet viewport query",
|
|
650
|
+
);
|
|
651
|
+
const sheetId = spreadsheetSheetId(requireString(query.sheetId, "viewport sheetId"));
|
|
652
|
+
const startRow = uint32(query.startRow, "viewport startRow");
|
|
653
|
+
const startColumn = uint32(query.startColumn, "viewport startColumn");
|
|
654
|
+
const rowCount = positiveUint32(query.rowCount, "viewport rowCount");
|
|
655
|
+
const columnCount = positiveUint32(query.columnCount, "viewport columnCount");
|
|
656
|
+
validateViewportGeometry(startRow, startColumn, rowCount, columnCount);
|
|
657
|
+
const maxCells = positiveUint32(query.maxCells, "viewport maxCells");
|
|
658
|
+
const maxBytes = positiveUint32(query.maxBytes, "viewport maxBytes");
|
|
659
|
+
if (maxBytes < SPREADSHEET_ARTIFACT_VIEWPORT_MIN_RESPONSE_BYTES) {
|
|
660
|
+
throw queryError("viewport maxBytes cannot fit an empty response");
|
|
661
|
+
}
|
|
662
|
+
return Object.freeze({
|
|
663
|
+
sheetId,
|
|
664
|
+
startRow,
|
|
665
|
+
startColumn,
|
|
666
|
+
rowCount,
|
|
667
|
+
columnCount,
|
|
668
|
+
maxCells,
|
|
669
|
+
maxBytes,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function normalizeMetadataQuery(input: unknown): SpreadsheetArtifactMetadataQuery {
|
|
674
|
+
const query = exactRecord(input, "spreadsheet metadata query");
|
|
675
|
+
exactKeys(query, ["maxBytes", "maxSheets"], "spreadsheet metadata query");
|
|
676
|
+
const maxSheets = positiveUint32(query.maxSheets, "metadata maxSheets");
|
|
677
|
+
const maxBytes = positiveUint32(query.maxBytes, "metadata maxBytes");
|
|
678
|
+
if (maxBytes < SPREADSHEET_ARTIFACT_METADATA_MIN_RESPONSE_BYTES) {
|
|
679
|
+
throw queryError("metadata maxBytes cannot fit an empty response");
|
|
680
|
+
}
|
|
681
|
+
return Object.freeze({ maxSheets, maxBytes });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function normalizeViewportProjection(input: unknown): SpreadsheetArtifactViewportProjection {
|
|
685
|
+
const projection = exactRecord(input, "spreadsheet viewport projection");
|
|
686
|
+
exactKeys(
|
|
687
|
+
projection,
|
|
688
|
+
[
|
|
689
|
+
"cells",
|
|
690
|
+
"columnCount",
|
|
691
|
+
"generationId",
|
|
692
|
+
"revision",
|
|
693
|
+
"rowCount",
|
|
694
|
+
"sheetId",
|
|
695
|
+
"startColumn",
|
|
696
|
+
"startRow",
|
|
697
|
+
],
|
|
698
|
+
"spreadsheet viewport projection",
|
|
699
|
+
);
|
|
700
|
+
const revision = uint64(projection.revision, "viewport revision");
|
|
701
|
+
const sheetId = spreadsheetSheetId(requireString(projection.sheetId, "viewport sheetId"));
|
|
702
|
+
const generationId = nullableGenerationId(projection.generationId, "viewport generationId");
|
|
703
|
+
const startRow = uint32(projection.startRow, "viewport startRow");
|
|
704
|
+
const startColumn = uint32(projection.startColumn, "viewport startColumn");
|
|
705
|
+
const rowCount = positiveUint32(projection.rowCount, "viewport rowCount");
|
|
706
|
+
const columnCount = positiveUint32(projection.columnCount, "viewport columnCount");
|
|
707
|
+
validateViewportGeometry(startRow, startColumn, rowCount, columnCount);
|
|
708
|
+
if (!Array.isArray(projection.cells)) throw projectionError("viewport cells must be an array");
|
|
709
|
+
if (projection.cells.length > SPREADSHEET_ARTIFACT_VIEWPORT_MAX_CELLS) {
|
|
710
|
+
throw projectionError("viewport projection exceeds its hard cell limit");
|
|
711
|
+
}
|
|
712
|
+
const cells: SpreadsheetArtifactProjectedCell[] = [];
|
|
713
|
+
let previousOrdinal = -1;
|
|
714
|
+
for (let index = 0; index < projection.cells.length; index += 1) {
|
|
715
|
+
const cell = normalizeProjectedCell(ownArrayElement(projection.cells, index, "viewport cells"));
|
|
716
|
+
if (
|
|
717
|
+
cell.row < startRow ||
|
|
718
|
+
cell.row >= startRow + rowCount ||
|
|
719
|
+
cell.column < startColumn ||
|
|
720
|
+
cell.column >= startColumn + columnCount
|
|
721
|
+
) {
|
|
722
|
+
throw projectionError("viewport cell lies outside its response extent");
|
|
723
|
+
}
|
|
724
|
+
const ordinal = (cell.row - startRow) * columnCount + (cell.column - startColumn);
|
|
725
|
+
if (!Number.isSafeInteger(ordinal) || ordinal <= previousOrdinal) {
|
|
726
|
+
throw projectionError("viewport cells are not unique strict row-major entries");
|
|
727
|
+
}
|
|
728
|
+
previousOrdinal = ordinal;
|
|
729
|
+
if (cell.formula === null && cell.value.kind === "empty") {
|
|
730
|
+
throw projectionError("sparse viewport contains an empty cell");
|
|
731
|
+
}
|
|
732
|
+
cells.push(cell);
|
|
733
|
+
}
|
|
734
|
+
return Object.freeze({
|
|
735
|
+
revision,
|
|
736
|
+
sheetId,
|
|
737
|
+
generationId,
|
|
738
|
+
startRow,
|
|
739
|
+
startColumn,
|
|
740
|
+
rowCount,
|
|
741
|
+
columnCount,
|
|
742
|
+
cells: Object.freeze(cells),
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function normalizeMetadataProjection(input: unknown): SpreadsheetArtifactMetadataProjection {
|
|
747
|
+
const projection = exactRecord(input, "spreadsheet metadata projection");
|
|
748
|
+
exactKeys(
|
|
749
|
+
projection,
|
|
750
|
+
["modeledFeatures", "revision", "sheets"],
|
|
751
|
+
"spreadsheet metadata projection",
|
|
752
|
+
);
|
|
753
|
+
const revision = uint64(projection.revision, "metadata revision");
|
|
754
|
+
const features = exactRecord(projection.modeledFeatures, "metadata modeledFeatures");
|
|
755
|
+
exactKeys(features, ["dimensions", "hidden", "merges"], "metadata modeledFeatures");
|
|
756
|
+
if (features.dimensions !== false || features.hidden !== false || features.merges !== false) {
|
|
757
|
+
throw projectionError("version 1 metadata modeled features must all be false");
|
|
758
|
+
}
|
|
759
|
+
if (!Array.isArray(projection.sheets)) throw projectionError("metadata sheets must be an array");
|
|
760
|
+
if (projection.sheets.length > SPREADSHEET_ARTIFACT_METADATA_MAX_SHEETS) {
|
|
761
|
+
throw projectionError("metadata projection exceeds its hard sheet limit");
|
|
762
|
+
}
|
|
763
|
+
const ids = new Set<string>();
|
|
764
|
+
const names = new Set<string>();
|
|
765
|
+
const sheets: SpreadsheetArtifactSheetMetadata[] = [];
|
|
766
|
+
let generationPresence: boolean | null = null;
|
|
767
|
+
for (let index = 0; index < projection.sheets.length; index += 1) {
|
|
768
|
+
const value = exactRecord(
|
|
769
|
+
ownArrayElement(projection.sheets, index, "metadata sheets"),
|
|
770
|
+
"metadata sheet",
|
|
771
|
+
);
|
|
772
|
+
exactKeys(value, ["generationId", "name", "sheetId", "usedBounds"], "metadata sheet");
|
|
773
|
+
const sheetId = spreadsheetSheetId(requireString(value.sheetId, "metadata sheetId"));
|
|
774
|
+
if (ids.has(sheetId)) throw projectionError("metadata projection contains duplicate sheet ids");
|
|
775
|
+
ids.add(sheetId);
|
|
776
|
+
const generationId = nullableGenerationId(value.generationId, "metadata generationId");
|
|
777
|
+
const present = generationId !== null;
|
|
778
|
+
if (generationPresence !== null && generationPresence !== present) {
|
|
779
|
+
throw projectionError("metadata generations must be uniformly present or absent");
|
|
780
|
+
}
|
|
781
|
+
generationPresence = present;
|
|
782
|
+
const name = validateSheetName(
|
|
783
|
+
requireString(value.name, "metadata sheet name"),
|
|
784
|
+
"metadata sheet name",
|
|
785
|
+
);
|
|
786
|
+
const foldedName = name.toLowerCase();
|
|
787
|
+
if (names.has(foldedName)) {
|
|
788
|
+
throw projectionError("metadata projection contains duplicate case-insensitive sheet names");
|
|
789
|
+
}
|
|
790
|
+
names.add(foldedName);
|
|
791
|
+
const usedBounds = value.usedBounds === null ? null : normalizeUsedBounds(value.usedBounds);
|
|
792
|
+
sheets.push(Object.freeze({ sheetId, generationId, name, usedBounds }));
|
|
793
|
+
}
|
|
794
|
+
return Object.freeze({
|
|
795
|
+
revision,
|
|
796
|
+
modeledFeatures: MODELED_FEATURES_NONE,
|
|
797
|
+
sheets: Object.freeze(sheets),
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function normalizeProjectedCell(input: unknown): SpreadsheetArtifactProjectedCell {
|
|
802
|
+
const cell = exactRecord(input, "projected cell");
|
|
803
|
+
exactKeys(cell, ["column", "formula", "row", "value"], "projected cell");
|
|
804
|
+
const row = uint32(cell.row, "projected cell row");
|
|
805
|
+
const column = uint32(cell.column, "projected cell column");
|
|
806
|
+
let formula: string | null;
|
|
807
|
+
if (cell.formula === null) {
|
|
808
|
+
formula = null;
|
|
809
|
+
} else {
|
|
810
|
+
formula = requireString(cell.formula, "projected cell formula");
|
|
811
|
+
if (formula.length === 0) throw projectionError("projected cell formula must not be empty");
|
|
812
|
+
boundedUtf8(
|
|
813
|
+
formula,
|
|
814
|
+
SPREADSHEET_ARTIFACT_PROJECTION_MAX_STRING_BYTES,
|
|
815
|
+
"projected cell formula",
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
return Object.freeze({
|
|
819
|
+
row,
|
|
820
|
+
column,
|
|
821
|
+
formula,
|
|
822
|
+
value: normalizeProjectedValue(cell.value),
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function normalizeProjectedValue(input: unknown): SpreadsheetArtifactProjectedCellValue {
|
|
827
|
+
const value = exactRecord(input, "projected cell value");
|
|
828
|
+
if (value.kind === "empty") {
|
|
829
|
+
exactKeys(value, ["kind"], "empty projected cell value");
|
|
830
|
+
return EMPTY_VALUE;
|
|
831
|
+
}
|
|
832
|
+
if (value.kind === "boolean") {
|
|
833
|
+
exactKeys(value, ["kind", "value"], "boolean projected cell value");
|
|
834
|
+
if (typeof value.value !== "boolean") throw projectionError("boolean cell value is invalid");
|
|
835
|
+
return Object.freeze({ kind: "boolean", value: value.value });
|
|
836
|
+
}
|
|
837
|
+
if (value.kind === "number") {
|
|
838
|
+
exactKeys(value, ["kind", "value"], "number projected cell value");
|
|
839
|
+
if (typeof value.value !== "number" || !Number.isFinite(value.value)) {
|
|
840
|
+
throw projectionError("number cell value must be finite");
|
|
841
|
+
}
|
|
842
|
+
return Object.freeze({
|
|
843
|
+
kind: "number",
|
|
844
|
+
value: value.value === 0 ? 0 : value.value,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
if (value.kind === "date") {
|
|
848
|
+
exactKeys(value, ["kind", "value"], "date projected cell value");
|
|
849
|
+
return Object.freeze({
|
|
850
|
+
kind: "date",
|
|
851
|
+
value: canonicalDateString(value.value),
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
if (value.kind === "text") {
|
|
855
|
+
exactKeys(value, ["kind", "value"], "text projected cell value");
|
|
856
|
+
const text = requireString(value.value, "text cell value");
|
|
857
|
+
boundedUtf8(text, SPREADSHEET_ARTIFACT_PROJECTION_MAX_STRING_BYTES, "text cell value");
|
|
858
|
+
return Object.freeze({ kind: "text", value: text });
|
|
859
|
+
}
|
|
860
|
+
if (value.kind === "error") {
|
|
861
|
+
exactKeys(value, ["kind", "value"], "error projected cell value");
|
|
862
|
+
return Object.freeze({
|
|
863
|
+
kind: "error",
|
|
864
|
+
value: normalizeFormulaError(value.value),
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
throw projectionError("projected cell value kind is invalid");
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function canonicalDateString(input: unknown): string {
|
|
871
|
+
canonicalSpreadsheetDateMilliseconds(input);
|
|
872
|
+
return input as string;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function normalizeFormulaError(input: unknown): SpreadsheetArtifactFormulaError {
|
|
876
|
+
if (typeof input === "string" && FORMULA_ERROR_TAGS.has(input)) {
|
|
877
|
+
return input as Exclude<SpreadsheetArtifactFormulaError, { custom: string }>;
|
|
878
|
+
}
|
|
879
|
+
const error = exactRecord(input, "custom formula error");
|
|
880
|
+
exactKeys(error, ["custom"], "custom formula error");
|
|
881
|
+
const custom = requireString(error.custom, "custom formula error");
|
|
882
|
+
boundedUtf8(custom, SPREADSHEET_ARTIFACT_PROJECTION_MAX_STRING_BYTES, "custom formula error");
|
|
883
|
+
return Object.freeze({ custom });
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function normalizeUsedBounds(input: unknown): SpreadsheetArtifactUsedBounds {
|
|
887
|
+
const bounds = exactRecord(input, "metadata used bounds");
|
|
888
|
+
exactKeys(bounds, ["endColumn", "endRow", "startColumn", "startRow"], "metadata used bounds");
|
|
889
|
+
const startRow = uint32(bounds.startRow, "used bounds startRow");
|
|
890
|
+
const startColumn = uint32(bounds.startColumn, "used bounds startColumn");
|
|
891
|
+
const endRow = uint32(bounds.endRow, "used bounds endRow");
|
|
892
|
+
const endColumn = uint32(bounds.endColumn, "used bounds endColumn");
|
|
893
|
+
if (startRow > endRow || startColumn > endColumn) {
|
|
894
|
+
throw projectionError("metadata used bounds are not ordered");
|
|
895
|
+
}
|
|
896
|
+
return Object.freeze({ startRow, startColumn, endRow, endColumn });
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function validateViewportGeometry(
|
|
900
|
+
startRow: number,
|
|
901
|
+
startColumn: number,
|
|
902
|
+
rowCount: number,
|
|
903
|
+
columnCount: number,
|
|
904
|
+
): void {
|
|
905
|
+
if (rowCount < 1 || columnCount < 1) {
|
|
906
|
+
throw queryError("viewport extents must be nonzero");
|
|
907
|
+
}
|
|
908
|
+
if (rowCount - 1 > UINT32_MAX - startRow || columnCount - 1 > UINT32_MAX - startColumn) {
|
|
909
|
+
throw queryError("viewport extent exceeds uint32 coordinates");
|
|
910
|
+
}
|
|
911
|
+
const area = rowCount * columnCount;
|
|
912
|
+
if (!Number.isSafeInteger(area) || area > SPREADSHEET_ARTIFACT_VIEWPORT_MAX_AREA) {
|
|
913
|
+
throw queryError("viewport area exceeds its hard limit");
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function validateSheetName(value: string, label: string): string {
|
|
918
|
+
boundedUtf8(value, SPREADSHEET_ARTIFACT_PROJECTION_MAX_STRING_BYTES, label);
|
|
919
|
+
if (
|
|
920
|
+
value.length === 0 ||
|
|
921
|
+
value.trim() !== value ||
|
|
922
|
+
value.length > 31 ||
|
|
923
|
+
/[\\/?*[\]:\0]/u.test(value)
|
|
924
|
+
) {
|
|
925
|
+
throw projectionError(`${label} does not match the public spreadsheet model`);
|
|
926
|
+
}
|
|
927
|
+
return value;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function nullableGenerationId(value: unknown, label: string): EditableArtifactStableId | null {
|
|
931
|
+
return value === null ? null : spreadsheetSheetId(requireString(value, label));
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function enforceProjectionEnvelopeLimit(bytes: Uint8Array, requested: number): void {
|
|
935
|
+
if (bytes.byteLength > Math.min(requested, SPREADSHEET_ARTIFACT_PROJECTION_MAX_BYTES)) {
|
|
936
|
+
throw projectionError("spreadsheet projection exceeds its requested byte limit");
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function effectiveProjectionMaximum(value: number, minimum: number): number {
|
|
941
|
+
const maximum = positiveUint32(value, "projection maximumBytes");
|
|
942
|
+
const effective = Math.min(maximum, SPREADSHEET_ARTIFACT_PROJECTION_MAX_BYTES);
|
|
943
|
+
if (effective < minimum) throw projectionError("projection maximum cannot fit an empty response");
|
|
944
|
+
return effective;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function requireBytes(value: unknown, label: string): asserts value is Uint8Array {
|
|
948
|
+
if (!(value instanceof Uint8Array)) throw new TypeError(`${label} must be a Uint8Array`);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function requireString(value: unknown, label: string): string {
|
|
952
|
+
if (typeof value !== "string") throw new TypeError(`${label} must be a string`);
|
|
953
|
+
return value;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function uint32(value: unknown, label: string): number {
|
|
957
|
+
if (!Number.isInteger(value) || (value as number) < 0 || (value as number) > UINT32_MAX) {
|
|
958
|
+
throw new TypeError(`${label} must be a uint32`);
|
|
959
|
+
}
|
|
960
|
+
return value as number;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function positiveUint32(value: unknown, label: string): number {
|
|
964
|
+
const number = uint32(value, label);
|
|
965
|
+
if (number === 0) throw new TypeError(`${label} must be nonzero`);
|
|
966
|
+
return number;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function uint64(value: unknown, label: string): bigint {
|
|
970
|
+
if (typeof value !== "bigint" || value < 0n || value > UINT64_MAX) {
|
|
971
|
+
throw new TypeError(`${label} must be a uint64 bigint`);
|
|
972
|
+
}
|
|
973
|
+
return value;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function exactRecord(value: unknown, label: string): Record<string, unknown> {
|
|
977
|
+
if (
|
|
978
|
+
typeof value !== "object" ||
|
|
979
|
+
value === null ||
|
|
980
|
+
Array.isArray(value) ||
|
|
981
|
+
Object.getPrototypeOf(value) !== Object.prototype
|
|
982
|
+
) {
|
|
983
|
+
throw new TypeError(`${label} must be a plain record`);
|
|
984
|
+
}
|
|
985
|
+
return value as Record<string, unknown>;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function exactKeys(
|
|
989
|
+
value: Record<string, unknown>,
|
|
990
|
+
expected: readonly string[],
|
|
991
|
+
label: string,
|
|
992
|
+
): void {
|
|
993
|
+
const keys = Reflect.ownKeys(value);
|
|
994
|
+
if (keys.some((key) => typeof key !== "string"))
|
|
995
|
+
throw new TypeError(`${label} has symbol fields`);
|
|
996
|
+
const actual = (keys as string[]).sort(compareCodeUnits);
|
|
997
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
998
|
+
throw new TypeError(`${label} fields must be exactly: ${expected.join(", ")}`);
|
|
999
|
+
}
|
|
1000
|
+
for (const key of actual) {
|
|
1001
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1002
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
1003
|
+
throw new TypeError(`${label} fields must be enumerable data properties`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function ownArrayElement(value: readonly unknown[], index: number, label: string): unknown {
|
|
1009
|
+
if (!Object.hasOwn(value, index)) throw new TypeError(`${label} must be dense`);
|
|
1010
|
+
return value[index];
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function compareCodeUnits(left: string, right: string): number {
|
|
1014
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function boundedUtf8(value: string, maximum: number, label: string): Uint8Array {
|
|
1018
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1019
|
+
const code = value.charCodeAt(index);
|
|
1020
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
1021
|
+
const next = value.charCodeAt(index + 1);
|
|
1022
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) {
|
|
1023
|
+
throw new TypeError(`${label} contains an unpaired surrogate`);
|
|
1024
|
+
}
|
|
1025
|
+
index += 1;
|
|
1026
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
1027
|
+
throw new TypeError(`${label} contains an unpaired surrogate`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const bytes = textEncoder.encode(value);
|
|
1031
|
+
if (bytes.byteLength > maximum) throw new RangeError(`${label} exceeds its byte limit`);
|
|
1032
|
+
return bytes;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function equalBytes(left: Uint8Array, right: Uint8Array): boolean {
|
|
1036
|
+
return left.byteLength === right.byteLength && left.every((byte, index) => byte === right[index]);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function fnv1a64(bytes: Uint8Array): bigint {
|
|
1040
|
+
let hash = FNV_OFFSET;
|
|
1041
|
+
for (const byte of bytes) {
|
|
1042
|
+
hash ^= BigInt(byte);
|
|
1043
|
+
hash = BigInt.asUintN(64, hash * FNV_PRIME);
|
|
1044
|
+
}
|
|
1045
|
+
return hash;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function queryError(message: string): TypeError {
|
|
1049
|
+
return new TypeError(message);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function projectionError(message: string): TypeError {
|
|
1053
|
+
return new TypeError(message);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const MODELED_FEATURES_NONE: SpreadsheetArtifactModeledFeatures = Object.freeze({
|
|
1057
|
+
dimensions: false,
|
|
1058
|
+
hidden: false,
|
|
1059
|
+
merges: false,
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
const EMPTY_VALUE: SpreadsheetArtifactProjectedCellValue = Object.freeze({
|
|
1063
|
+
kind: "empty",
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
const FORMULA_ERRORS = [
|
|
1067
|
+
"null",
|
|
1068
|
+
"divide_by_zero",
|
|
1069
|
+
"value",
|
|
1070
|
+
"reference",
|
|
1071
|
+
"name",
|
|
1072
|
+
"number",
|
|
1073
|
+
"not_available",
|
|
1074
|
+
"spill",
|
|
1075
|
+
"calculation",
|
|
1076
|
+
] as const;
|
|
1077
|
+
|
|
1078
|
+
const FORMULA_ERROR_TAGS = new Set<string>(FORMULA_ERRORS);
|
|
1079
|
+
|
|
1080
|
+
class FixedWriter {
|
|
1081
|
+
private readonly buffer: Uint8Array;
|
|
1082
|
+
private readonly viewBuffer: DataView;
|
|
1083
|
+
private offset = 0;
|
|
1084
|
+
|
|
1085
|
+
constructor(length: number) {
|
|
1086
|
+
this.buffer = new Uint8Array(length);
|
|
1087
|
+
this.viewBuffer = new DataView(this.buffer.buffer);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
view(): Uint8Array {
|
|
1091
|
+
return this.buffer.subarray(0, this.offset);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
finish(): Uint8Array {
|
|
1095
|
+
if (this.offset !== this.buffer.byteLength)
|
|
1096
|
+
throw new TypeError("fixed binary writer is incomplete");
|
|
1097
|
+
return this.buffer;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
u8(value: number): void {
|
|
1101
|
+
this.reserve(1);
|
|
1102
|
+
this.viewBuffer.setUint8(this.offset, value);
|
|
1103
|
+
this.offset += 1;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
u16(value: number): void {
|
|
1107
|
+
this.reserve(2);
|
|
1108
|
+
this.viewBuffer.setUint16(this.offset, value, true);
|
|
1109
|
+
this.offset += 2;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
u32(value: number): void {
|
|
1113
|
+
this.reserve(4);
|
|
1114
|
+
this.viewBuffer.setUint32(this.offset, value, true);
|
|
1115
|
+
this.offset += 4;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
u64(value: bigint): void {
|
|
1119
|
+
this.reserve(8);
|
|
1120
|
+
this.viewBuffer.setBigUint64(this.offset, value, true);
|
|
1121
|
+
this.offset += 8;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
stableId(value: EditableArtifactStableId): void {
|
|
1125
|
+
const id = spreadsheetSheetId(value);
|
|
1126
|
+
this.u64(BigInt(`0x${id.slice(16)}`));
|
|
1127
|
+
this.u64(BigInt(`0x${id.slice(0, 16)}`));
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
bytes(value: Uint8Array): void {
|
|
1131
|
+
this.reserve(value.byteLength);
|
|
1132
|
+
this.buffer.set(value, this.offset);
|
|
1133
|
+
this.offset += value.byteLength;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private reserve(length: number): void {
|
|
1137
|
+
if (this.offset + length > this.buffer.byteLength)
|
|
1138
|
+
throw new RangeError("fixed binary writer overflow");
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
class DynamicWriter {
|
|
1143
|
+
private buffer: Uint8Array;
|
|
1144
|
+
private viewBuffer: DataView;
|
|
1145
|
+
private offset = 0;
|
|
1146
|
+
|
|
1147
|
+
constructor(private readonly maximum: number) {
|
|
1148
|
+
this.buffer = new Uint8Array(Math.min(1_024, maximum));
|
|
1149
|
+
this.viewBuffer = new DataView(this.buffer.buffer);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
finish(): Uint8Array {
|
|
1153
|
+
return this.buffer.slice(0, this.offset);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
u8(value: number): void {
|
|
1157
|
+
this.reserve(1);
|
|
1158
|
+
this.viewBuffer.setUint8(this.offset, value);
|
|
1159
|
+
this.offset += 1;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
u32(value: number): void {
|
|
1163
|
+
this.reserve(4);
|
|
1164
|
+
this.viewBuffer.setUint32(this.offset, value, true);
|
|
1165
|
+
this.offset += 4;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
u64(value: bigint): void {
|
|
1169
|
+
this.reserve(8);
|
|
1170
|
+
this.viewBuffer.setBigUint64(this.offset, value, true);
|
|
1171
|
+
this.offset += 8;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
f64(value: number): void {
|
|
1175
|
+
this.reserve(8);
|
|
1176
|
+
this.viewBuffer.setFloat64(this.offset, value === 0 ? 0 : value, true);
|
|
1177
|
+
this.offset += 8;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
stableId(value: EditableArtifactStableId): void {
|
|
1181
|
+
const id = spreadsheetSheetId(value);
|
|
1182
|
+
this.u64(BigInt(`0x${id.slice(16)}`));
|
|
1183
|
+
this.u64(BigInt(`0x${id.slice(0, 16)}`));
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
optionalStableId(value: EditableArtifactStableId | null): void {
|
|
1187
|
+
if (value === null) this.bytes(new Uint8Array(16));
|
|
1188
|
+
else this.stableId(value);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
string(value: string): void {
|
|
1192
|
+
const bytes = boundedUtf8(
|
|
1193
|
+
value,
|
|
1194
|
+
SPREADSHEET_ARTIFACT_PROJECTION_MAX_STRING_BYTES,
|
|
1195
|
+
"projection string",
|
|
1196
|
+
);
|
|
1197
|
+
this.u32(bytes.byteLength);
|
|
1198
|
+
this.bytes(bytes);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
cell(cell: SpreadsheetArtifactProjectedCell): void {
|
|
1202
|
+
if (cell.formula === null) {
|
|
1203
|
+
this.u8(0);
|
|
1204
|
+
} else {
|
|
1205
|
+
this.u8(1);
|
|
1206
|
+
this.string(cell.formula);
|
|
1207
|
+
}
|
|
1208
|
+
const value = cell.value;
|
|
1209
|
+
switch (value.kind) {
|
|
1210
|
+
case "empty":
|
|
1211
|
+
this.u8(0);
|
|
1212
|
+
break;
|
|
1213
|
+
case "boolean":
|
|
1214
|
+
this.u8(value.value ? 2 : 1);
|
|
1215
|
+
break;
|
|
1216
|
+
case "number":
|
|
1217
|
+
this.u8(3);
|
|
1218
|
+
this.f64(value.value);
|
|
1219
|
+
break;
|
|
1220
|
+
case "date":
|
|
1221
|
+
this.u8(6);
|
|
1222
|
+
this.i64(BigInt(Date.parse(value.value)));
|
|
1223
|
+
break;
|
|
1224
|
+
case "text":
|
|
1225
|
+
this.u8(4);
|
|
1226
|
+
this.string(value.value);
|
|
1227
|
+
break;
|
|
1228
|
+
case "error":
|
|
1229
|
+
this.u8(5);
|
|
1230
|
+
this.formulaError(value.value);
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
private formulaError(error: SpreadsheetArtifactFormulaError): void {
|
|
1236
|
+
if (typeof error === "string") {
|
|
1237
|
+
this.u8(FORMULA_ERRORS.indexOf(error as (typeof FORMULA_ERRORS)[number]));
|
|
1238
|
+
} else {
|
|
1239
|
+
this.u8(9);
|
|
1240
|
+
this.string(error.custom);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
private bytes(value: Uint8Array): void {
|
|
1245
|
+
this.reserve(value.byteLength);
|
|
1246
|
+
this.buffer.set(value, this.offset);
|
|
1247
|
+
this.offset += value.byteLength;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
private i64(value: bigint): void {
|
|
1251
|
+
this.reserve(8);
|
|
1252
|
+
this.viewBuffer.setBigInt64(this.offset, value, true);
|
|
1253
|
+
this.offset += 8;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
private reserve(additional: number): void {
|
|
1257
|
+
const required = this.offset + additional;
|
|
1258
|
+
if (!Number.isSafeInteger(required) || required > this.maximum) {
|
|
1259
|
+
throw projectionError("spreadsheet projection exceeds its byte limit");
|
|
1260
|
+
}
|
|
1261
|
+
if (required <= this.buffer.byteLength) return;
|
|
1262
|
+
let size = Math.max(1, this.buffer.byteLength);
|
|
1263
|
+
while (size < required) size = Math.min(this.maximum, size * 2);
|
|
1264
|
+
const next = new Uint8Array(size);
|
|
1265
|
+
next.set(this.buffer.subarray(0, this.offset));
|
|
1266
|
+
this.buffer = next;
|
|
1267
|
+
this.viewBuffer = new DataView(next.buffer);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
class Reader {
|
|
1272
|
+
private readonly viewBuffer: DataView;
|
|
1273
|
+
private offset = 0;
|
|
1274
|
+
|
|
1275
|
+
constructor(private readonly input: Uint8Array) {
|
|
1276
|
+
this.viewBuffer = new DataView(input.buffer, input.byteOffset, input.byteLength);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
u8(): number {
|
|
1280
|
+
this.reserve(1);
|
|
1281
|
+
return this.input[this.offset++]!;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
u16(): number {
|
|
1285
|
+
this.reserve(2);
|
|
1286
|
+
const value = this.viewBuffer.getUint16(this.offset, true);
|
|
1287
|
+
this.offset += 2;
|
|
1288
|
+
return value;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
u32(): number {
|
|
1292
|
+
this.reserve(4);
|
|
1293
|
+
const value = this.viewBuffer.getUint32(this.offset, true);
|
|
1294
|
+
this.offset += 4;
|
|
1295
|
+
return value;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
u64(): bigint {
|
|
1299
|
+
this.reserve(8);
|
|
1300
|
+
const value = this.viewBuffer.getBigUint64(this.offset, true);
|
|
1301
|
+
this.offset += 8;
|
|
1302
|
+
return value;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
f64(): number {
|
|
1306
|
+
this.reserve(8);
|
|
1307
|
+
const value = this.viewBuffer.getFloat64(this.offset, true);
|
|
1308
|
+
this.offset += 8;
|
|
1309
|
+
if (!Number.isFinite(value) || Object.is(value, -0)) {
|
|
1310
|
+
throw projectionError("spreadsheet projection number is noncanonical");
|
|
1311
|
+
}
|
|
1312
|
+
return value;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
stableId(label: string): EditableArtifactStableId {
|
|
1316
|
+
const counter = this.u64().toString(16).padStart(16, "0");
|
|
1317
|
+
const namespace = this.u64().toString(16).padStart(16, "0");
|
|
1318
|
+
try {
|
|
1319
|
+
return spreadsheetSheetId(`${namespace}${counter}`);
|
|
1320
|
+
} catch {
|
|
1321
|
+
throw projectionError(`${label} is reserved or noncanonical`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
optionalStableId(present: boolean): EditableArtifactStableId | null {
|
|
1326
|
+
if (present) return this.stableId("projection generation id");
|
|
1327
|
+
const bytes = this.bytes(16);
|
|
1328
|
+
if (bytes.some((byte) => byte !== 0)) {
|
|
1329
|
+
throw projectionError("projection has a generation id without its flag");
|
|
1330
|
+
}
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
string(): string {
|
|
1335
|
+
const length = this.u32();
|
|
1336
|
+
if (length > SPREADSHEET_ARTIFACT_PROJECTION_MAX_STRING_BYTES) {
|
|
1337
|
+
throw projectionError("projection string exceeds its byte limit");
|
|
1338
|
+
}
|
|
1339
|
+
try {
|
|
1340
|
+
return textDecoder.decode(this.bytes(length));
|
|
1341
|
+
} catch {
|
|
1342
|
+
throw projectionError("projection string is not canonical UTF-8");
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
formula(): string | null {
|
|
1347
|
+
const tag = this.u8();
|
|
1348
|
+
if (tag === 0) return null;
|
|
1349
|
+
if (tag !== 1) throw projectionError("projection formula tag is invalid");
|
|
1350
|
+
const formula = this.string();
|
|
1351
|
+
if (formula.length === 0) throw projectionError("projection formula must not be empty");
|
|
1352
|
+
return formula;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
cellValue(): SpreadsheetArtifactProjectedCellValue {
|
|
1356
|
+
switch (this.u8()) {
|
|
1357
|
+
case 0:
|
|
1358
|
+
return EMPTY_VALUE;
|
|
1359
|
+
case 1:
|
|
1360
|
+
return Object.freeze({ kind: "boolean", value: false });
|
|
1361
|
+
case 2:
|
|
1362
|
+
return Object.freeze({ kind: "boolean", value: true });
|
|
1363
|
+
case 3:
|
|
1364
|
+
return Object.freeze({ kind: "number", value: this.f64() });
|
|
1365
|
+
case 4:
|
|
1366
|
+
return Object.freeze({ kind: "text", value: this.string() });
|
|
1367
|
+
case 5:
|
|
1368
|
+
return Object.freeze({ kind: "error", value: this.formulaError() });
|
|
1369
|
+
case 6:
|
|
1370
|
+
return Object.freeze({
|
|
1371
|
+
kind: "date",
|
|
1372
|
+
value: canonicalSpreadsheetDateFromMilliseconds(this.i64()),
|
|
1373
|
+
});
|
|
1374
|
+
default:
|
|
1375
|
+
throw projectionError("projection cell value tag is invalid");
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
done(message: string): void {
|
|
1380
|
+
if (this.offset !== this.input.byteLength) throw new TypeError(message);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
private i64(): bigint {
|
|
1384
|
+
const bytes = this.bytes(8);
|
|
1385
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getBigInt64(0, true);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
bytes(length: number): Uint8Array {
|
|
1389
|
+
this.reserve(length);
|
|
1390
|
+
const bytes = this.input.subarray(this.offset, this.offset + length);
|
|
1391
|
+
this.offset += length;
|
|
1392
|
+
return bytes;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
private formulaError(): SpreadsheetArtifactFormulaError {
|
|
1396
|
+
const tag = this.u8();
|
|
1397
|
+
if (tag <= 8) return FORMULA_ERRORS[tag]!;
|
|
1398
|
+
if (tag === 9) return Object.freeze({ custom: this.string() });
|
|
1399
|
+
throw projectionError("projection formula error tag is invalid");
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
private reserve(length: number): void {
|
|
1403
|
+
if (
|
|
1404
|
+
!Number.isSafeInteger(length) ||
|
|
1405
|
+
length < 0 ||
|
|
1406
|
+
this.offset + length > this.input.byteLength
|
|
1407
|
+
) {
|
|
1408
|
+
throw projectionError("spreadsheet projection payload is truncated");
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|