@stowage/core 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -12,7 +12,7 @@ npm install @stowage/core
12
12
  ## Example
13
13
 
14
14
  A function written against `Storage` runs against every adapter: `memoryStorage()` in a test,
15
- `fsStorage()` on a laptop, `s3Storage()` in production.
15
+ `fsStorage()` on a laptop, `s3Storage()` or `azureBlobStorage()` in production.
16
16
 
17
17
  ```ts
18
18
  import { isStorageError, type Storage } from "@stowage/core";
@@ -32,23 +32,23 @@ export async function readSettings(storage: Storage): Promise<unknown> {
32
32
 
33
33
  ## Runtimes
34
34
 
35
- Node 24 and later, Bun, Deno and `workerd` at the compatibility date `2026-09-01`. CI last ran green on Bun 1.4.2 and Deno 2.9.6.
35
+ Node 24 and later, Bun, Deno and `workerd` at the compatibility date `2026-09-01` without Node APIs. CI last ran green on Bun 1.4.2 and Deno 2.9.6.
36
36
 
37
- The bundle measures 1.3 kB minified and gzipped.
37
+ The bundle measures 3.3 kB minified and gzipped.
38
38
 
39
39
  ## Limits
40
40
 
41
41
  This section is empty. `@stowage/core` declares no capability; each storage declares its own, out
42
- of the four names of [spec 4.9](https://github.com/stowage-js/stowage/blob/@stowage/core@0.1.0/docs/spec.md#49-capabilities).
42
+ of the five names of [spec 4.9](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/docs/spec.md#49-capabilities).
43
43
 
44
44
  ## Notes
45
45
 
46
46
  A failure reaches the caller in one of two shapes: a `StorageError`, which `isStorageError` tells
47
47
  apart, or the runtime's `AbortError` once a signal fired
48
- ([spec 4.10](https://github.com/stowage-js/stowage/blob/@stowage/core@0.1.0/docs/spec.md#410-errors)).
48
+ ([spec 4.10](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/docs/spec.md#410-errors)).
49
49
  A name added to `StorageErrorCode` or `capabilityNames` is a minor release, so a `switch` over
50
50
  either needs a default branch
51
- ([spec 9](https://github.com/stowage-js/stowage/blob/@stowage/core@0.1.0/docs/spec.md#9-versions)).
51
+ ([spec 10](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/docs/spec.md#10-versions)).
52
52
 
53
53
  ```ts
54
54
  import { isStorageError } from "@stowage/core";
@@ -70,12 +70,49 @@ export function describeFailure(failure: unknown): string {
70
70
  }
71
71
  ```
72
72
 
73
+ An adapter written outside this repository takes what two adapters here need on the wire from
74
+ this package rather than writing it again
75
+ ([spec 4.13](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/docs/spec.md#413-exports-for-adapter-authors)):
76
+
77
+ - `invalidKeyReason` checks a key against its rule of spec 4.8, as the first act of every
78
+ operation.
79
+ - `errorCodeForStatus` and `isTransientStatus` are the status mapping of spec 4.10, and
80
+ `withRetry` the retry loop around one request.
81
+ - `parseXml` reads the XML an answer document is written in, and rejects anything outside that
82
+ subset with `XmlSyntaxError`.
83
+ - `readEnvironment` reads one environment variable, and answers `""` where it is unset or the
84
+ runtime cannot read it.
85
+ - `isUserMetadataKey`, `encodeUserMetadataValue`, `decodeUserMetadataValue` and
86
+ `userMetadataByteLength` carry user metadata in headers, and measure the 2 KB of spec 4.3.
87
+ - `PresignedPut` is what `presignPut` resolves with on every adapter that declares
88
+ `presignedUrls`.
89
+
90
+ ```ts
91
+ import { invalidKeyReason, StorageError } from "@stowage/core";
92
+
93
+ export function requireWritableKey(bucket: string, key: string): void {
94
+ const reason = invalidKeyReason(key, "writable");
95
+
96
+ if (reason === undefined) return;
97
+
98
+ throw new StorageError({
99
+ code: "InvalidKey",
100
+ message: reason,
101
+ operation: "put",
102
+ bucket,
103
+ provider: "my-provider",
104
+ key,
105
+ attempts: 0,
106
+ });
107
+ }
108
+ ```
109
+
73
110
  ## Specification
74
111
 
75
- [`docs/spec.md` at `@stowage/core@0.1.0`](https://github.com/stowage-js/stowage/blob/@stowage/core@0.1.0/docs/spec.md#4-the-core-api-stowagecore)
112
+ [`docs/spec.md` at `@stowage/core@0.2.0`](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/docs/spec.md#4-the-core-api-stowagecore)
76
113
  is the contract: a caller may rely on what it states and on nothing else this package happens to
77
- export. The [terms it uses](https://github.com/stowage-js/stowage/blob/@stowage/core@0.1.0/CONTEXT.md)
78
- and the [decisions behind it](https://github.com/stowage-js/stowage/tree/@stowage/core@0.1.0/docs/adr)
114
+ export. The [terms it uses](https://github.com/stowage-js/stowage/blob/@stowage/core@0.2.0/CONTEXT.md)
115
+ and the [decisions behind it](https://github.com/stowage-js/stowage/tree/@stowage/core@0.2.0/docs/adr)
79
116
  are at the same tag.
80
117
 
81
118
  ## License
package/dist/index.d.ts CHANGED
@@ -8,10 +8,14 @@
8
8
  * - `presignedUrls`: the concrete type carries `presignGet` and `presignPut`. Where not
9
9
  * declared, neither method exists on the type.
10
10
  * - `rangeReads`: `get` honors `range`. Where not declared, a `range` is `Unsupported`.
11
- * - `userMetadata`: `put` stores `userMetadata`, `stat` and `get` return it, `copy` keeps it.
12
- * Where not declared, a non-empty `userMetadata` is `Unsupported` and reads return `{}`.
11
+ * - `userMetadata`: `put` stores `userMetadata` whose keys are ASCII identifiers,
12
+ * `[A-Za-z_][A-Za-z0-9_]*`; `stat` and `get` return it, `copy` keeps it. Where not declared,
13
+ * a non-empty `userMetadata` is `Unsupported` and reads return `{}`.
14
+ * - `userMetadataTokenKeys`: beside `userMetadata`, a key may be any ASCII HTTP token, such as
15
+ * `content-hash`. Where not declared, a key outside identifiers is `Unsupported` naming it;
16
+ * without `userMetadata` too, the call is `Unsupported` naming that.
13
17
  */
14
- export declare const capabilityNames: readonly ["keyBytesPreserved", "presignedUrls", "rangeReads", "userMetadata"];
18
+ export declare const capabilityNames: readonly ["keyBytesPreserved", "presignedUrls", "rangeReads", "userMetadata", "userMetadataTokenKeys"];
15
19
  /** One name out of {@link capabilityNames}. */
16
20
  type CapabilityName = (typeof capabilityNames)[number];
17
21
  //#endregion
@@ -23,6 +27,16 @@ type ResolverOptions = {
23
27
  /** A value, or a function that yields one. No core signature mentions it (ADR 0007). */
24
28
  type Resolvable<T> = T | ((options?: ResolverOptions) => T | Promise<T>);
25
29
  //#endregion
30
+ //#region src/environment.d.ts
31
+ /**
32
+ * ADR 0007: `process.env` is the one route through Node, Bun, Deno's compatibility layer
33
+ * and a Worker under `nodejs_compat`. A Worker without it has no `process` at all, and
34
+ * Deno without `--allow-env` throws `NotCapable` rather than answering `undefined`, so
35
+ * both leave the value empty. A name is read on its own, because enumerating
36
+ * `process.env` needs the unscoped permission in Deno.
37
+ */
38
+ export declare function readEnvironment(name: string): string;
39
+ //#endregion
26
40
  //#region src/errors.d.ts
27
41
  /**
28
42
  * What went wrong, as the one field a caller branches on (spec 4.10). A code added here is a
@@ -61,6 +75,19 @@ interface StorageErrorFields {
61
75
  readonly capability?: CapabilityName;
62
76
  readonly cause?: unknown;
63
77
  }
78
+ /**
79
+ * A failure the rules of spec section 4 decide, stated without the adapter that raises it:
80
+ * the adapter adds what only it knows, its bucket, the operation and the attempts, and
81
+ * raises it through its own error factory (spec 4.13).
82
+ */
83
+ type Refusal = {
84
+ readonly code: "Unsupported";
85
+ readonly message: string;
86
+ readonly capability: CapabilityName;
87
+ } | {
88
+ readonly code: "InvalidRequest" | "InvalidOption";
89
+ readonly message: string;
90
+ };
64
91
  export declare class StorageError extends Error {
65
92
  readonly code: StorageErrorCode;
66
93
  readonly operation: string;
@@ -88,32 +115,16 @@ type KeyRule = "writable" | "addressable" | "prefix";
88
115
  /** The reason a key violates the rule, or `undefined` where it holds. */
89
116
  export declare function invalidKeyReason(key: string, rule: KeyRule): string | undefined;
90
117
  //#endregion
91
- //#region src/retry.d.ts
92
- interface RetryOptions {
93
- readonly maxAttempts: number;
94
- readonly signal?: AbortSignal;
95
- }
96
- /**
97
- * Repeats `attempt` while it rejects with a `StorageError` whose `retryable` is `true`,
98
- * up to `maxAttempts` times, waiting a random delay between zero and
99
- * `min(5 s, 100 ms × 2^n)` before attempt `n + 1`. The error it finally rejects with
100
- * carries the number of attempts made.
101
- *
102
- * Spec 4.13 has this be the one definition of the loop of spec 7.5, so that an adapter
103
- * written outside this repository repeats on the same curve. `adapter-fs` and
104
- * `adapter-memory` have no request to send again and do not call it.
105
- */
106
- export declare function withRetry<T>(attempt: () => Promise<T>, options: RetryOptions): Promise<T>;
107
- //#endregion
108
- //#region src/status.d.ts
118
+ //#region src/presigned-put.d.ts
109
119
  /**
110
- * The code the status decides on its own, or `undefined` for a status that decides
111
- * nothing. It is the fallback of spec 4.10: an adapter maps a provider code first and
112
- * reaches here where it recognizes none.
120
+ * What `presignPut` resolves with on every adapter that declares `presignedUrls`, so the code
121
+ * that uploads through it never names the provider (ADR 0022).
113
122
  */
114
- export declare function errorCodeForStatus(status: number): StorageErrorCode | undefined;
115
- /** Whether the status names a condition that may be gone a moment later (spec 4.10). */
116
- export declare function isTransientStatus(status: number): boolean;
123
+ interface PresignedPut {
124
+ readonly url: string;
125
+ /** The headers the client sends beside the body. `Content-Length` is never among them. */
126
+ readonly headers: Readonly<Record<string, string>>;
127
+ }
117
128
  //#endregion
118
129
  //#region src/storage.d.ts
119
130
  type PutBody = Uint8Array | string | ReadableStream<Uint8Array>;
@@ -126,7 +137,8 @@ interface PutOptions extends OperationOptions {
126
137
  * Stored where the storage declares `userMetadata`, and `Unsupported` elsewhere unless it is
127
138
  * empty. Keys are non-empty ASCII HTTP tokens compared case-insensitively; values may hold
128
139
  * any Unicode. Keys and values together hold at most 2 KB of encoded header bytes, and more
129
- * is `InvalidRequest`.
140
+ * is `InvalidRequest`. A key outside ASCII identifiers, such as `content-hash`, is
141
+ * `Unsupported` where the storage does not declare `userMetadataTokenKeys`.
130
142
  */
131
143
  userMetadata?: Record<string, string>;
132
144
  }
@@ -194,10 +206,143 @@ interface Storage {
194
206
  stat(key: string, options?: OperationOptions): Promise<ObjectStat>;
195
207
  exists(key: string, options?: OperationOptions): Promise<boolean>;
196
208
  list(options?: ListOptions): ObjectListing;
209
+ /**
210
+ * Sends the keys in batches, one request per batch, and each adapter states its batch
211
+ * size. A per-key failure fills the report; a failure of a request as a whole rejects.
212
+ */
197
213
  delete(...keys: readonly string[]): Promise<DeleteReport>;
198
214
  deleteAll(prefix: string, options?: OperationOptions): Promise<DeleteReport>;
199
215
  copy(from: string, to: string, options?: OperationOptions): Promise<ObjectStat>;
200
216
  move(from: string, to: string, options?: OperationOptions): Promise<ObjectStat>;
201
217
  }
202
218
  //#endregion
203
- export type { ByteRange, CapabilityName, DeleteReport, GetOptions, KeyRule, ListOptions, ListPage, ObjectEntry, ObjectListing, ObjectStat, OperationOptions, PutBody, PutOptions, Resolvable, ResolverOptions, RetryOptions, Storage, StorageErrorCode, StorageErrorFields, StoredObject };
219
+ //#region src/range.d.ts
220
+ /** Refuses bounds spec 4.3 does not allow, which is decided before the object is looked up. */
221
+ export declare function rangeBoundsRefusal(range: ByteRange | undefined): Refusal | undefined;
222
+ /**
223
+ * Refuses a range that starts at or beyond the object's `size` bytes, which spec 4.3 names a
224
+ * refusal of the request rather than of the option, because a provider answers it with `416`.
225
+ */
226
+ export declare function rangeStartRefusal(range: ByteRange, size: number, key: string): Refusal | undefined;
227
+ /** The last byte the range names, both ends inclusive, clipped to the object's `size` bytes. */
228
+ export declare function lastByteOf(range: ByteRange | undefined, size: number): number;
229
+ /**
230
+ * Whether the whole object is the body the range asks for, clipped as spec 4.3 clips it. A
231
+ * provider may answer a range with the whole object and `200`, which RFC 9110 allows, and
232
+ * that answer is the one asked for exactly where this holds. An empty object holds no byte a
233
+ * range could start at, so no range covers it.
234
+ */
235
+ export declare function rangeCoversWhole(range: ByteRange, size: number): boolean;
236
+ /** The `Range` field of RFC 9110, both ends inclusive as `ByteRange` is. */
237
+ export declare function rangeHeader(range: ByteRange): string;
238
+ /**
239
+ * The size of the whole object out of a `206`'s `Content-Range`, which is what spec 4.4 has a
240
+ * ranged `get` report rather than the length of the range.
241
+ */
242
+ export declare function wholeSizeOf(contentRange: string | null): number | undefined;
243
+ //#endregion
244
+ //#region src/retry.d.ts
245
+ interface RetryOptions {
246
+ readonly maxAttempts: number;
247
+ readonly signal?: AbortSignal;
248
+ }
249
+ /**
250
+ * Repeats `attempt` while it rejects with a `StorageError` whose `retryable` is `true`,
251
+ * up to `maxAttempts` times, waiting a random delay between zero and
252
+ * `min(5 s, 100 ms × 2^n)` before attempt `n + 1`. The error it finally rejects with
253
+ * carries the number of attempts made.
254
+ *
255
+ * Spec 4.13 has this be the one definition of the loop of spec 7.5, so that an adapter
256
+ * written outside this repository repeats on the same curve. `adapter-fs` and
257
+ * `adapter-memory` have no request to send again and do not call it.
258
+ */
259
+ export declare function withRetry<T>(attempt: () => Promise<T>, options: RetryOptions): Promise<T>;
260
+ //#endregion
261
+ //#region src/status.d.ts
262
+ /**
263
+ * The code the status decides on its own, or `undefined` for a status that decides
264
+ * nothing. It is the fallback of spec 4.10: an adapter maps a provider code first and
265
+ * reaches here where it recognizes none.
266
+ */
267
+ export declare function errorCodeForStatus(status: number): StorageErrorCode | undefined;
268
+ /** Whether the status names a condition that may be gone a moment later (spec 4.10). */
269
+ export declare function isTransientStatus(status: number): boolean;
270
+ //#endregion
271
+ //#region src/upload-stream.d.ts
272
+ interface StreamUploadOptions {
273
+ readonly partSize: number;
274
+ readonly concurrency: number;
275
+ /** The provider's limit on the parts of one upload. */
276
+ readonly maxParts: number;
277
+ /** What the `InvalidRequest` for a stream above `maxParts` is told against. */
278
+ readonly bucket: string;
279
+ readonly provider: string;
280
+ readonly key: string;
281
+ readonly signal?: AbortSignal;
282
+ }
283
+ interface StreamUpload<T> {
284
+ /** The stream ended within the first part: the bytes go as one request. */
285
+ whole(bytes: Uint8Array<ArrayBuffer>): Promise<T>;
286
+ /** The first part filled: the adapter starts, sends through `sendParts`, and commits. */
287
+ multipart(sendParts: SendParts): Promise<T>;
288
+ }
289
+ type SendParts = <R>(send: (index: number, bytes: Uint8Array<ArrayBuffer>, signal: AbortSignal) => Promise<R>) => Promise<{
290
+ readonly results: readonly R[];
291
+ readonly size: number;
292
+ }>;
293
+ /**
294
+ * Reads the stream into parts of `partSize` and hands a stream that ends within the first
295
+ * to `whole`, any other to `multipart`. The reader, its buffers and the cancellation of
296
+ * the source stay here, so the spec promises one call rather than the order of five
297
+ * (ADR 0030).
298
+ *
299
+ * The source is canceled wherever the upload settles before the stream ended, as spec 4.2
300
+ * has it for a `put` that rejects before reading its body to the end.
301
+ */
302
+ export declare function uploadStream<T>(stream: ReadableStream<Uint8Array>, options: StreamUploadOptions, upload: StreamUpload<T>): Promise<T>;
303
+ //#endregion
304
+ //#region src/user-metadata.d.ts
305
+ type UserMetadataKeyRule = "token" | "identifier";
306
+ export declare function isUserMetadataKey(name: string, rule: UserMetadataKeyRule): boolean;
307
+ /** The value as a header carries it: as written where it travels so, else as encoded words. */
308
+ export declare function encodeUserMetadataValue(value: string, options?: {
309
+ always?: boolean;
310
+ }): string;
311
+ /**
312
+ * What spec 4.3 bounds at 2 KB: every key and its value as `encodeUserMetadataValue` writes
313
+ * it, without `always`, so the bound does not depend on what an adapter encodes beyond the rule.
314
+ */
315
+ export declare function userMetadataByteLength(userMetadata: Readonly<Record<string, string>>): number;
316
+ type UserMetadataCheck = {
317
+ readonly held: Readonly<Record<string, string>>;
318
+ } | {
319
+ readonly refusal: Refusal;
320
+ };
321
+ /**
322
+ * The user metadata as a storage holds it, keys folded to lower case as a header field name
323
+ * is, or the first refusal of spec 4.3, read off what the storage declares.
324
+ */
325
+ export declare function checkUserMetadata(userMetadata: Record<string, string> | undefined, capabilities: readonly CapabilityName[]): UserMetadataCheck;
326
+ /** A header value read back, with every form of encoded word RFC 2047 allows decoded. */
327
+ export declare function decodeUserMetadataValue(value: string): string;
328
+ //#endregion
329
+ //#region src/xml.d.ts
330
+ interface XmlElement {
331
+ readonly name: string;
332
+ readonly attributes: Readonly<Record<string, string>>;
333
+ readonly children: readonly XmlElement[];
334
+ /** The text directly inside the element, its children's left out. */
335
+ readonly text: string;
336
+ }
337
+ /** A document outside the subset `parseXml` reads, reported rather than read around (ADR 0003). */
338
+ export declare class XmlSyntaxError extends Error {
339
+ override readonly name = "XmlSyntaxError";
340
+ }
341
+ /**
342
+ * The root element of an XML document of the subset the providers answer with: elements,
343
+ * attributes, text, comments, the five named entities and a numeric character reference to
344
+ * any Unicode scalar value but `U+0000`, under one optional declaration (spec 4.13).
345
+ */
346
+ export declare function parseXml(document: string): XmlElement;
347
+ //#endregion
348
+ export type { ByteRange, CapabilityName, DeleteReport, GetOptions, KeyRule, ListOptions, ListPage, ObjectEntry, ObjectListing, ObjectStat, OperationOptions, PresignedPut, PutBody, PutOptions, Refusal, Resolvable, ResolverOptions, RetryOptions, SendParts, Storage, StorageErrorCode, StorageErrorFields, StoredObject, StreamUpload, StreamUploadOptions, UserMetadataCheck, UserMetadataKeyRule, XmlElement };
package/dist/index.js CHANGED
@@ -8,16 +8,38 @@
8
8
  * - `presignedUrls`: the concrete type carries `presignGet` and `presignPut`. Where not
9
9
  * declared, neither method exists on the type.
10
10
  * - `rangeReads`: `get` honors `range`. Where not declared, a `range` is `Unsupported`.
11
- * - `userMetadata`: `put` stores `userMetadata`, `stat` and `get` return it, `copy` keeps it.
12
- * Where not declared, a non-empty `userMetadata` is `Unsupported` and reads return `{}`.
11
+ * - `userMetadata`: `put` stores `userMetadata` whose keys are ASCII identifiers,
12
+ * `[A-Za-z_][A-Za-z0-9_]*`; `stat` and `get` return it, `copy` keeps it. Where not declared,
13
+ * a non-empty `userMetadata` is `Unsupported` and reads return `{}`.
14
+ * - `userMetadataTokenKeys`: beside `userMetadata`, a key may be any ASCII HTTP token, such as
15
+ * `content-hash`. Where not declared, a key outside identifiers is `Unsupported` naming it;
16
+ * without `userMetadata` too, the call is `Unsupported` naming that.
13
17
  */
14
18
  const capabilityNames = [
15
19
  "keyBytesPreserved",
16
20
  "presignedUrls",
17
21
  "rangeReads",
18
- "userMetadata"
22
+ "userMetadata",
23
+ "userMetadataTokenKeys"
19
24
  ];
20
25
  //#endregion
26
+ //#region src/environment.ts
27
+ /**
28
+ * ADR 0007: `process.env` is the one route through Node, Bun, Deno's compatibility layer
29
+ * and a Worker under `nodejs_compat`. A Worker without it has no `process` at all, and
30
+ * Deno without `--allow-env` throws `NotCapable` rather than answering `undefined`, so
31
+ * both leave the value empty. A name is read on its own, because enumerating
32
+ * `process.env` needs the unscoped permission in Deno.
33
+ */
34
+ function readEnvironment(name) {
35
+ try {
36
+ if (typeof process === "undefined") return "";
37
+ return process?.env?.[name] ?? "";
38
+ } catch {
39
+ return "";
40
+ }
41
+ }
42
+ //#endregion
21
43
  //#region src/errors.ts
22
44
  const storageErrorBrand = Symbol.for("stowage.error");
23
45
  var StorageError = class extends Error {
@@ -92,16 +114,18 @@ function withAttempts(failure, attempts) {
92
114
  //#endregion
93
115
  //#region src/keys.ts
94
116
  const writableKeyLimit = 1024;
95
- const utf8 = new TextEncoder();
117
+ const utf8$1 = new TextEncoder();
96
118
  /** The reason a key violates the rule, or `undefined` where it holds. */
97
119
  function invalidKeyReason(key, rule) {
98
120
  if (key === "") return rule === "prefix" ? void 0 : "is empty";
99
121
  const controlCharacter = firstControlCharacter(key);
100
122
  if (controlCharacter !== void 0) return `holds the control character ${controlCharacter}`;
123
+ const loneSurrogate = firstLoneSurrogate(key);
124
+ if (loneSurrogate !== void 0) return `holds the lone surrogate ${loneSurrogate}`;
101
125
  if (rule === "writable") {
102
126
  if (key.includes("\\")) return "holds a backslash";
103
127
  if (key.endsWith("/")) return "ends with a slash";
104
- const bytes = utf8.encode(key).length;
128
+ const bytes = utf8$1.encode(key).length;
105
129
  if (bytes > writableKeyLimit) return `is ${bytes} UTF-8 bytes, above the limit of ${writableKeyLimit}`;
106
130
  }
107
131
  return invalidSegmentReason(key);
@@ -109,9 +133,19 @@ function invalidKeyReason(key, rule) {
109
133
  function firstControlCharacter(key) {
110
134
  for (let index = 0; index < key.length; index += 1) {
111
135
  const code = key.charCodeAt(index);
112
- if (code <= 31 || code === 127) return `U+${code.toString(16).toUpperCase().padStart(4, "0")}`;
136
+ if (code <= 31 || code === 127) return codePointName(code);
137
+ }
138
+ }
139
+ function firstLoneSurrogate(key) {
140
+ if (key.isWellFormed()) return void 0;
141
+ for (const character of key) {
142
+ const code = character.codePointAt(0) ?? 0;
143
+ if (code >= 55296 && code <= 57343) return codePointName(code);
113
144
  }
114
145
  }
146
+ function codePointName(code) {
147
+ return `U+${code.toString(16).toUpperCase().padStart(4, "0")}`;
148
+ }
115
149
  function invalidSegmentReason(key) {
116
150
  const segments = key.split("/");
117
151
  for (const [index, segment] of segments.entries()) {
@@ -122,6 +156,59 @@ function invalidSegmentReason(key) {
122
156
  }
123
157
  }
124
158
  //#endregion
159
+ //#region src/range.ts
160
+ const invalidBounds = {
161
+ code: "InvalidOption",
162
+ message: "The option `range` takes two whole numbers from zero up, `start` at most `end`"
163
+ };
164
+ /** Refuses bounds spec 4.3 does not allow, which is decided before the object is looked up. */
165
+ function rangeBoundsRefusal(range) {
166
+ if (range === void 0) return void 0;
167
+ const { start, end } = range;
168
+ if (!isOffset(start) || end !== void 0 && (!isOffset(end) || end < start)) return invalidBounds;
169
+ }
170
+ /**
171
+ * Refuses a range that starts at or beyond the object's `size` bytes, which spec 4.3 names a
172
+ * refusal of the request rather than of the option, because a provider answers it with `416`.
173
+ */
174
+ function rangeStartRefusal(range, size, key) {
175
+ if (range.start < size) return void 0;
176
+ return {
177
+ code: "InvalidRequest",
178
+ message: `The range starts beyond the ${size} bytes under the key ${JSON.stringify(key)}`
179
+ };
180
+ }
181
+ /** The last byte the range names, both ends inclusive, clipped to the object's `size` bytes. */
182
+ function lastByteOf(range, size) {
183
+ return Math.min(range?.end ?? size - 1, size - 1);
184
+ }
185
+ /**
186
+ * Whether the whole object is the body the range asks for, clipped as spec 4.3 clips it. A
187
+ * provider may answer a range with the whole object and `200`, which RFC 9110 allows, and
188
+ * that answer is the one asked for exactly where this holds. An empty object holds no byte a
189
+ * range could start at, so no range covers it.
190
+ */
191
+ function rangeCoversWhole(range, size) {
192
+ return range.start === 0 && size > 0 && lastByteOf(range, size) === size - 1;
193
+ }
194
+ /** The `Range` field of RFC 9110, both ends inclusive as `ByteRange` is. */
195
+ function rangeHeader(range) {
196
+ return `bytes=${range.start}-${range.end ?? ""}`;
197
+ }
198
+ const contentRangePattern = /^bytes (\d+)-(\d+)\/(\d+)$/u;
199
+ /**
200
+ * The size of the whole object out of a `206`'s `Content-Range`, which is what spec 4.4 has a
201
+ * ranged `get` report rather than the length of the range.
202
+ */
203
+ function wholeSizeOf(contentRange) {
204
+ const found = contentRangePattern.exec(contentRange?.trim() ?? "");
205
+ const size = Number(found?.[3]);
206
+ return Number.isSafeInteger(size) ? size : void 0;
207
+ }
208
+ function isOffset(value) {
209
+ return Number.isInteger(value) && value >= 0;
210
+ }
211
+ //#endregion
125
212
  //#region src/retry.ts
126
213
  const baseDelay = 100;
127
214
  const maximumDelay = 5e3;
@@ -188,4 +275,558 @@ function isTransientStatus(status) {
188
275
  return status === 408 || status === 429 || status >= 500 && status <= 599;
189
276
  }
190
277
  //#endregion
191
- export { StorageError, capabilityNames, errorCodeForStatus, invalidKeyReason, isStorageError, isTransientStatus, withRetry };
278
+ //#region src/part-reader.ts
279
+ /** Where a buffer starts before the stream has shown whether it fills a part. */
280
+ const initialCapacity = 65536;
281
+ /**
282
+ * A stream read into parts of one size (spec 4.13). A part is known to be the last only
283
+ * once the stream has ended behind it, so a full part looks one chunk ahead; that chunk
284
+ * is the stream's own and becomes the start of the next part.
285
+ *
286
+ * A part handed back through `recycle` lends its buffer to a later one. ADR 0016 bounds
287
+ * an upload at `partSize × concurrency` of buffers, and a fresh buffer per part would
288
+ * keep the settled ones alive until the collector noticed them.
289
+ *
290
+ * Until a part has filled, the buffer grows by doubling, because ADR 0009 has a body
291
+ * shorter than a part allocate only what it needs. Once one has, the stream is known to
292
+ * go as a multipart upload, and every buffer after it starts at the full part size.
293
+ */
294
+ var PartReader = class {
295
+ #reader;
296
+ #partSize;
297
+ #signal;
298
+ #cancelOnAbort = () => {
299
+ this.#reader.cancel(this.#signal?.reason).catch(() => {});
300
+ };
301
+ #free = [];
302
+ #ahead;
303
+ #filledOnce = false;
304
+ constructor(stream, partSize, signal) {
305
+ this.#reader = stream.getReader();
306
+ this.#partSize = partSize;
307
+ this.#signal = signal;
308
+ signal?.addEventListener("abort", this.#cancelOnAbort, { once: true });
309
+ }
310
+ async next() {
311
+ let bytes = this.#freshBuffer();
312
+ let filled = 0;
313
+ while (filled < this.#partSize) {
314
+ const chunk = this.#ahead ?? await this.#read();
315
+ this.#ahead = void 0;
316
+ if (chunk === void 0) return {
317
+ bytes: bytes.subarray(0, filled),
318
+ last: true
319
+ };
320
+ const taken = Math.min(chunk.byteLength, this.#partSize - filled);
321
+ if (filled + taken > bytes.byteLength) bytes = this.#grown(bytes, filled + taken);
322
+ bytes.set(chunk.subarray(0, taken), filled);
323
+ filled += taken;
324
+ if (taken < chunk.byteLength) this.#ahead = chunk.subarray(taken);
325
+ }
326
+ this.#filledOnce = true;
327
+ this.#ahead ??= await this.#read();
328
+ return {
329
+ bytes,
330
+ last: this.#ahead === void 0
331
+ };
332
+ }
333
+ /** The part's request settled, so nothing reads its bytes any more. */
334
+ recycle(part) {
335
+ if (part.bytes.buffer.byteLength === this.#partSize) this.#free.push(part.bytes.buffer);
336
+ }
337
+ /** Spec 4.2 leaves the stream canceled where `put` rejects before reading it to its end. */
338
+ async cancel(reason) {
339
+ await this.#reader.cancel(reason).catch(() => {});
340
+ }
341
+ release() {
342
+ this.#signal?.removeEventListener("abort", this.#cancelOnAbort);
343
+ this.#reader.releaseLock();
344
+ }
345
+ #freshBuffer() {
346
+ const pooled = this.#free.pop();
347
+ if (pooled !== void 0) return new Uint8Array(pooled);
348
+ return new Uint8Array(this.#filledOnce ? this.#partSize : Math.min(this.#partSize, initialCapacity));
349
+ }
350
+ #grown(bytes, needed) {
351
+ let capacity = bytes.byteLength;
352
+ while (capacity < needed) capacity *= 2;
353
+ const larger = new Uint8Array(Math.min(capacity, this.#partSize));
354
+ larger.set(bytes);
355
+ return larger;
356
+ }
357
+ async #read() {
358
+ for (;;) {
359
+ const { done, value } = await this.#reader.read();
360
+ this.#signal?.throwIfAborted();
361
+ if (done) return void 0;
362
+ if (value.byteLength > 0) return value;
363
+ }
364
+ }
365
+ };
366
+ //#endregion
367
+ //#region src/upload-stream.ts
368
+ /**
369
+ * Reads the stream into parts of `partSize` and hands a stream that ends within the first
370
+ * to `whole`, any other to `multipart`. The reader, its buffers and the cancellation of
371
+ * the source stay here, so the spec promises one call rather than the order of five
372
+ * (ADR 0030).
373
+ *
374
+ * The source is canceled wherever the upload settles before the stream ended, as spec 4.2
375
+ * has it for a `put` that rejects before reading its body to the end.
376
+ */
377
+ async function uploadStream(stream, options, upload) {
378
+ const parts = new PartReader(stream, options.partSize, options.signal);
379
+ const uploadSettled = new AbortController();
380
+ let readToEnd = false;
381
+ let cancelReason = /* @__PURE__ */ new Error("The upload settled before the stream ended");
382
+ try {
383
+ const first = await parts.next();
384
+ if (first.last) {
385
+ readToEnd = true;
386
+ return await upload.whole(first.bytes);
387
+ }
388
+ let called = false;
389
+ const sendParts = async (send) => {
390
+ uploadSettled.signal.throwIfAborted();
391
+ if (called) throw new Error("`sendParts` sends the parts of one upload once");
392
+ called = true;
393
+ const sent = await sendAll(parts, first, options, uploadSettled.signal, send);
394
+ readToEnd = true;
395
+ return sent;
396
+ };
397
+ return await upload.multipart(sendParts);
398
+ } catch (failure) {
399
+ cancelReason = failure;
400
+ throw failure;
401
+ } finally {
402
+ uploadSettled.abort(cancelReason);
403
+ if (!readToEnd) await parts.cancel(cancelReason);
404
+ parts.release();
405
+ }
406
+ }
407
+ /**
408
+ * `concurrency` parts in flight, and the next part read only once one of them settled,
409
+ * so the part buffers never outnumber the parts in flight (ADR 0016). The first failure
410
+ * stops the parts still in flight, and is what `sendParts` rejects with once they settled.
411
+ */
412
+ async function sendAll(parts, first, options, uploadSettled, send) {
413
+ const stop = new AbortController();
414
+ const partSignal = options.signal === void 0 ? stop.signal : AbortSignal.any([options.signal, stop.signal]);
415
+ const inFlight = /* @__PURE__ */ new Set();
416
+ const results = [];
417
+ let failure;
418
+ let size = 0;
419
+ const fail = (reason) => {
420
+ failure ??= { reason };
421
+ stop.abort();
422
+ parts.cancel(reason);
423
+ };
424
+ const sendPart = async (index, part) => {
425
+ try {
426
+ results[index] = await send(index, part.bytes, partSignal);
427
+ } catch (reason) {
428
+ fail(reason);
429
+ } finally {
430
+ parts.recycle(part);
431
+ }
432
+ };
433
+ const stopWithUpload = () => {
434
+ fail(uploadSettled.reason);
435
+ };
436
+ uploadSettled.addEventListener("abort", stopWithUpload, { once: true });
437
+ try {
438
+ for (let index = 0, part = first; !stop.signal.aborted; index += 1) {
439
+ if (index === options.maxParts - 1 && !part.last) throw tooManyParts(options);
440
+ const sending = sendPart(index, part);
441
+ inFlight.add(sending);
442
+ sending.finally(() => inFlight.delete(sending));
443
+ size += part.bytes.byteLength;
444
+ if (part.last) break;
445
+ while (inFlight.size >= options.concurrency) await Promise.race(inFlight);
446
+ part = await parts.next();
447
+ }
448
+ } catch (reason) {
449
+ fail(reason);
450
+ }
451
+ await Promise.all(inFlight);
452
+ uploadSettled.removeEventListener("abort", stopWithUpload);
453
+ if (failure !== void 0) {
454
+ await parts.cancel(failure.reason);
455
+ throw failure.reason;
456
+ }
457
+ return {
458
+ results,
459
+ size
460
+ };
461
+ }
462
+ /**
463
+ * Known once the last part the provider takes is full and the stream goes on. ADR 0016
464
+ * fixes the part size before the first part, so the way past it is a larger one.
465
+ */
466
+ function tooManyParts(options) {
467
+ return new StorageError({
468
+ code: "InvalidRequest",
469
+ message: `The stream needs more than ${options.maxParts} parts of the configured \`partSize\` of ${options.partSize} bytes; a larger \`multipart.partSize\` carries it`,
470
+ operation: "put",
471
+ bucket: options.bucket,
472
+ provider: options.provider,
473
+ key: options.key,
474
+ attempts: 0
475
+ });
476
+ }
477
+ //#endregion
478
+ //#region src/user-metadata.ts
479
+ const keyPatterns = {
480
+ /** The characters RFC 9110 allows in a field name, which is what a header carries. */
481
+ token: /^[!#$%&'*+.^_`|~\dA-Za-z-]+$/u,
482
+ /** An ASCII identifier, which Azure requires of a metadata name (ADR 0020). */
483
+ identifier: /^[A-Za-z_][A-Za-z\d_]*$/u
484
+ };
485
+ function isUserMetadataKey(name, rule) {
486
+ return keyPatterns[rule].test(name);
487
+ }
488
+ /**
489
+ * What survives a header field as it stands: printable ASCII with no space at either end,
490
+ * which HTTP trims, and no `=?`, which the reader would take for the start of an encoded
491
+ * word.
492
+ */
493
+ const travelsAsWritten = /^(?:[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?)?$/u;
494
+ const encodedWordStart = "=?UTF-8?B?";
495
+ const encodedWordEnd = "?=";
496
+ /**
497
+ * RFC 2047 bounds an encoded word at 75 characters; 45 bytes are 60 characters of base64,
498
+ * which leaves room for the 12 the word's frame costs.
499
+ */
500
+ const bytesPerEncodedWord = 45;
501
+ const utf8 = new TextEncoder();
502
+ /** The value as a header carries it: as written where it travels so, else as encoded words. */
503
+ function encodeUserMetadataValue(value, options) {
504
+ if (value === "") return value;
505
+ if (options?.always !== true && travelsAsWritten.test(value) && !value.includes("=?")) return value;
506
+ return utf8PiecesOf(value).map((piece) => `${encodedWordStart}${btoa(String.fromCharCode(...piece))}${encodedWordEnd}`).join(" ");
507
+ }
508
+ /**
509
+ * What spec 4.3 bounds at 2 KB: every key and its value as `encodeUserMetadataValue` writes
510
+ * it, without `always`, so the bound does not depend on what an adapter encodes beyond the rule.
511
+ */
512
+ function userMetadataByteLength(userMetadata) {
513
+ let bytes = 0;
514
+ for (const [name, value] of Object.entries(userMetadata)) bytes += utf8.encode(name).length + encodeUserMetadataValue(value).length;
515
+ return bytes;
516
+ }
517
+ /** Spec 4.3 bounds the set at 2 KB of the header bytes it costs once it is encoded. */
518
+ const headerByteLimit = 2048;
519
+ const noUserMetadata = { held: Object.freeze(Object.create(null)) };
520
+ /**
521
+ * The user metadata as a storage holds it, keys folded to lower case as a header field name
522
+ * is, or the first refusal of spec 4.3, read off what the storage declares.
523
+ */
524
+ function checkUserMetadata(userMetadata, capabilities) {
525
+ const entries = Object.entries(userMetadata ?? {});
526
+ if (entries.length === 0) return noUserMetadata;
527
+ if (!capabilities.includes("userMetadata")) return unsupported("userMetadata", "This storage holds no user metadata");
528
+ const held = Object.create(null);
529
+ for (const [name, value] of entries) {
530
+ if (!isUserMetadataKey(name, "token")) return refused(`The user metadata key ${JSON.stringify(name)} is no ASCII HTTP token`);
531
+ const folded = name.toLowerCase();
532
+ if (folded in held) return refused(`The user metadata key ${JSON.stringify(folded)} is given more than once`);
533
+ held[folded] = value;
534
+ }
535
+ const malformed = Object.entries(held).find(([, value]) => !value.isWellFormed());
536
+ if (malformed !== void 0) return refused(`The user metadata value of ${JSON.stringify(malformed[0])} holds a lone surrogate, which has no UTF-8 form`);
537
+ const headerBytes = userMetadataByteLength(held);
538
+ if (headerBytes > headerByteLimit) return refused(`The user metadata is ${headerBytes} encoded header bytes, above the limit of ${headerByteLimit}`);
539
+ const beyondIdentifiers = entries.find(([name]) => !isUserMetadataKey(name, "identifier"));
540
+ if (beyondIdentifiers !== void 0 && !capabilities.includes("userMetadataTokenKeys")) return unsupported("userMetadataTokenKeys", `This storage holds no user metadata key beyond identifiers, such as ${JSON.stringify(beyondIdentifiers[0])}`);
541
+ return { held: Object.freeze(held) };
542
+ }
543
+ function refused(message) {
544
+ return { refusal: {
545
+ code: "InvalidRequest",
546
+ message
547
+ } };
548
+ }
549
+ function unsupported(capability, message) {
550
+ return { refusal: {
551
+ code: "Unsupported",
552
+ message,
553
+ capability
554
+ } };
555
+ }
556
+ /** The UTF-8 bytes in pieces of at most one encoded word, each ending on a character. */
557
+ function utf8PiecesOf(value) {
558
+ const pieces = [];
559
+ let pending = [];
560
+ for (const character of value) {
561
+ const bytes = utf8.encode(character);
562
+ if (pending.length + bytes.length > bytesPerEncodedWord) {
563
+ pieces.push(Uint8Array.from(pending));
564
+ pending = [];
565
+ }
566
+ pending.push(...bytes);
567
+ }
568
+ pieces.push(Uint8Array.from(pending));
569
+ return pieces;
570
+ }
571
+ const encodedWord = /=\?([^?\s]+)\?([BbQq])\?([^?\s]*)\?=/gu;
572
+ const whitespaceOnly = /^[ \t]+$/u;
573
+ const hexPair = /^[\dA-Fa-f]{2}$/u;
574
+ /** A header value read back, with every form of encoded word RFC 2047 allows decoded. */
575
+ function decodeUserMetadataValue(value) {
576
+ let decoded = "";
577
+ let last = 0;
578
+ let previousWasEncoded = false;
579
+ for (const match of value.matchAll(encodedWord)) {
580
+ const between = value.slice(last, match.index);
581
+ const word = decodeWord(match[1] ?? "", match[2] ?? "", match[3] ?? "");
582
+ if (!(previousWasEncoded && word !== void 0 && whitespaceOnly.test(between))) decoded += between;
583
+ decoded += word ?? match[0];
584
+ previousWasEncoded = word !== void 0;
585
+ last = match.index + match[0].length;
586
+ }
587
+ return decoded + value.slice(last);
588
+ }
589
+ /** The text of one encoded word, or `undefined` where it names what cannot be read. */
590
+ function decodeWord(charset, encoding, text) {
591
+ const bytes = encoding.toUpperCase() === "B" ? base64Bytes(text) : quotedBytes(text);
592
+ if (bytes === void 0) return void 0;
593
+ try {
594
+ return new TextDecoder(charset, { fatal: true }).decode(bytes);
595
+ } catch {
596
+ return;
597
+ }
598
+ }
599
+ function base64Bytes(text) {
600
+ try {
601
+ return Uint8Array.from(atob(text), (character) => character.charCodeAt(0));
602
+ } catch {
603
+ return;
604
+ }
605
+ }
606
+ /** The `Q` encoding: `_` is a space and `=XX` one byte in hex. */
607
+ function quotedBytes(text) {
608
+ const bytes = [];
609
+ for (let index = 0; index < text.length; index += 1) {
610
+ const character = text[index];
611
+ if (character === "_") bytes.push(32);
612
+ else if (character === "=") {
613
+ const pair = text.slice(index + 1, index + 3);
614
+ if (!hexPair.test(pair)) return void 0;
615
+ bytes.push(Number.parseInt(pair, 16));
616
+ index += 2;
617
+ } else bytes.push(text.charCodeAt(index));
618
+ }
619
+ return Uint8Array.from(bytes);
620
+ }
621
+ //#endregion
622
+ //#region src/xml.ts
623
+ /** A document outside the subset `parseXml` reads, reported rather than read around (ADR 0003). */
624
+ var XmlSyntaxError = class extends Error {
625
+ name = "XmlSyntaxError";
626
+ };
627
+ /**
628
+ * The root element of an XML document of the subset the providers answer with: elements,
629
+ * attributes, text, comments, the five named entities and a numeric character reference to
630
+ * any Unicode scalar value but `U+0000`, under one optional declaration (spec 4.13).
631
+ */
632
+ function parseXml(document) {
633
+ const scanner = new Scanner(document);
634
+ scanner.skipDeclaration();
635
+ scanner.skipMisc();
636
+ const root = scanner.readElement();
637
+ scanner.skipMisc();
638
+ scanner.requireEnd();
639
+ return root;
640
+ }
641
+ const namePattern = /[:A-Z_a-z\u00C0-\uFFFF][:A-Z_a-z\u00C0-\uFFFF.\d-]*/uy;
642
+ const whitespacePattern = /[ \t\r\n]*/y;
643
+ const entityPattern = /&(?:#x([\da-fA-F]+)|#(\d+)|([A-Za-z]\w*));/uy;
644
+ /**
645
+ * What a character reference may name: any Unicode scalar value but `U+0000`. That is
646
+ * wider than `Char` of XML 1.0 on purpose, because S3 writes a key character XML cannot
647
+ * carry, such as `U+FFFE`, as a reference, and the key has to read back byte for byte
648
+ * (ADR 0027).
649
+ */
650
+ function isReferableCharacter(codePoint) {
651
+ return codePoint >= 1 && codePoint <= 55295 || codePoint >= 57344 && codePoint <= 1114111;
652
+ }
653
+ /** The five entities XML defines without a DTD, which is every one a document here has. */
654
+ const predefinedEntities = /* @__PURE__ */ new Map([
655
+ ["amp", "&"],
656
+ ["lt", "<"],
657
+ ["gt", ">"],
658
+ ["quot", "\""],
659
+ ["apos", "'"]
660
+ ]);
661
+ var Scanner = class {
662
+ #document;
663
+ #position = 0;
664
+ constructor(document) {
665
+ this.#document = document;
666
+ }
667
+ skipDeclaration() {
668
+ if (!this.#startsWith("<?xml")) return;
669
+ this.#position = this.#positionPast("?>", "The XML declaration is never closed");
670
+ }
671
+ /** Whitespace and comments, which are all that may stand beside the root element. */
672
+ skipMisc() {
673
+ for (;;) {
674
+ this.#skipWhitespace();
675
+ if (this.#startsWith("<!DOCTYPE")) throw this.#error("A DTD is refused");
676
+ if (!this.#startsWith("<!--")) return;
677
+ this.#skipComment();
678
+ }
679
+ }
680
+ requireEnd() {
681
+ if (this.#position < this.#document.length) throw this.#error("Something follows the root element");
682
+ }
683
+ readElement() {
684
+ this.#expect("<");
685
+ const name = this.#readName();
686
+ const attributes = Object.create(null);
687
+ if (this.#readStartTagEnd(attributes) === "empty") return {
688
+ name,
689
+ attributes,
690
+ children: [],
691
+ text: ""
692
+ };
693
+ const children = [];
694
+ let text = "";
695
+ for (;;) {
696
+ if (this.#position >= this.#document.length) throw this.#error(`The element <${name}> is never closed`);
697
+ if (this.#startsWith("</")) {
698
+ this.#position += 2;
699
+ this.#closeElement(name);
700
+ return {
701
+ name,
702
+ attributes,
703
+ children,
704
+ text
705
+ };
706
+ }
707
+ if (this.#startsWith("<!--")) this.#skipComment();
708
+ else if (this.#startsWith("<![CDATA[")) throw this.#error("A CDATA section is refused");
709
+ else if (this.#startsWith("<")) children.push(this.readElement());
710
+ else text += this.#readText();
711
+ }
712
+ }
713
+ #closeElement(name) {
714
+ const closing = this.#readName();
715
+ if (closing !== name) throw this.#error(`The element <${name}> is closed by </${closing}>`);
716
+ this.#skipWhitespace();
717
+ this.#expect(">");
718
+ }
719
+ #readStartTagEnd(attributes) {
720
+ for (;;) {
721
+ const before = this.#position;
722
+ this.#skipWhitespace();
723
+ if (this.#startsWith("/>")) {
724
+ this.#position += 2;
725
+ return "empty";
726
+ }
727
+ if (this.#startsWith(">")) {
728
+ this.#position += 1;
729
+ return "open";
730
+ }
731
+ if (this.#position === before) throw this.#error("An attribute follows no whitespace");
732
+ const attribute = this.#readName();
733
+ if (attribute in attributes) throw this.#error(`The attribute ${attribute} is given twice`);
734
+ this.#skipWhitespace();
735
+ this.#expect("=");
736
+ this.#skipWhitespace();
737
+ attributes[attribute] = this.#readAttributeValue();
738
+ }
739
+ }
740
+ /**
741
+ * XML 1.0, section 3.3.3: without a DTD every attribute is CDATA, so each whitespace
742
+ * character written into the value reads as a space, and a line break as one space. A
743
+ * reference to one stays the character it names.
744
+ */
745
+ #readAttributeValue() {
746
+ const quote = this.#document[this.#position];
747
+ if (quote !== "\"" && quote !== "'") throw this.#error("An attribute value is not quoted");
748
+ this.#position += 1;
749
+ let value = "";
750
+ for (;;) {
751
+ const character = this.#document[this.#position];
752
+ if (character === void 0) throw this.#error("An attribute value is never closed");
753
+ if (character === quote) {
754
+ this.#position += 1;
755
+ return value;
756
+ }
757
+ if (character === "<") throw this.#error("A `<` stands inside an attribute value");
758
+ if (character === "&") {
759
+ value += this.#readEntity();
760
+ continue;
761
+ }
762
+ this.#position += this.#startsWith("\r\n") ? 2 : 1;
763
+ value += character === " " || character === "\n" || character === "\r" ? " " : character;
764
+ }
765
+ }
766
+ /** Text up to the next markup, every `&` in it the start of an entity it decodes. */
767
+ #readText() {
768
+ const markup = this.#document.indexOf("<", this.#position);
769
+ const end = markup === -1 ? this.#document.length : markup;
770
+ let text = "";
771
+ while (this.#position < end) {
772
+ const ampersand = this.#document.indexOf("&", this.#position);
773
+ if (ampersand === -1 || ampersand >= end) {
774
+ text += this.#document.slice(this.#position, end);
775
+ this.#position = end;
776
+ break;
777
+ }
778
+ text += this.#document.slice(this.#position, ampersand);
779
+ this.#position = ampersand;
780
+ text += this.#readEntity();
781
+ }
782
+ return text;
783
+ }
784
+ #readEntity() {
785
+ entityPattern.lastIndex = this.#position;
786
+ const found = entityPattern.exec(this.#document);
787
+ if (found === null) throw this.#error("An `&` starts no entity");
788
+ const [entity, hex, decimal, name] = found;
789
+ if (name !== void 0) {
790
+ const character = predefinedEntities.get(name);
791
+ if (character === void 0) throw this.#error(`The entity ${entity} is not defined`);
792
+ this.#position = entityPattern.lastIndex;
793
+ return character;
794
+ }
795
+ const codePoint = hex === void 0 ? Number(decimal) : Number.parseInt(hex, 16);
796
+ if (!isReferableCharacter(codePoint)) throw this.#error(`The reference ${entity} names U+0000, a surrogate or no Unicode scalar value at all`);
797
+ this.#position = entityPattern.lastIndex;
798
+ return String.fromCodePoint(codePoint);
799
+ }
800
+ #skipComment() {
801
+ this.#position = this.#positionPast("-->", "A comment is never closed", 4);
802
+ }
803
+ #readName() {
804
+ namePattern.lastIndex = this.#position;
805
+ const found = namePattern.exec(this.#document);
806
+ if (found === null) throw this.#error("A name is missing where one belongs");
807
+ this.#position = namePattern.lastIndex;
808
+ return found[0];
809
+ }
810
+ #skipWhitespace() {
811
+ whitespacePattern.lastIndex = this.#position;
812
+ whitespacePattern.exec(this.#document);
813
+ this.#position = whitespacePattern.lastIndex;
814
+ }
815
+ #expect(literal) {
816
+ if (!this.#startsWith(literal)) throw this.#error(`\`${literal}\` is missing`);
817
+ this.#position += literal.length;
818
+ }
819
+ #positionPast(terminator, unterminated, offset = 0) {
820
+ const end = this.#document.indexOf(terminator, this.#position + offset);
821
+ if (end === -1) throw this.#error(unterminated);
822
+ return end + terminator.length;
823
+ }
824
+ #startsWith(literal) {
825
+ return this.#document.startsWith(literal, this.#position);
826
+ }
827
+ #error(message) {
828
+ return new XmlSyntaxError(`${message} (at character ${this.#position})`);
829
+ }
830
+ };
831
+ //#endregion
832
+ export { StorageError, XmlSyntaxError, capabilityNames, checkUserMetadata, decodeUserMetadataValue, encodeUserMetadataValue, errorCodeForStatus, invalidKeyReason, isStorageError, isTransientStatus, isUserMetadataKey, lastByteOf, parseXml, rangeBoundsRefusal, rangeCoversWhole, rangeHeader, rangeStartRefusal, readEnvironment, uploadStream, userMetadataByteLength, wholeSizeOf, withRetry };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stowage/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "One typed API for object storage: the types every stowage adapter implements, StorageError, and the utilities an adapter calls.",
5
5
  "keywords": [
6
6
  "adapter",