@onlyworlds/sdk 4.48.0 → 4.49.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/AGENTS.md +5 -2
- package/CHANGELOG.md +21 -0
- package/README.md +18 -3
- package/SCHEMA.md +1 -1
- package/dist/index.d.ts +60 -30
- package/dist/index.js +361 -72
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# For AI agents using @onlyworlds/sdk
|
|
2
2
|
|
|
3
|
-
**Current as of**: SDK **4.x** · schema-dist **v0.32.2-dist.
|
|
3
|
+
**Current as of**: SDK **4.x** · schema-dist **v0.32.2-dist.37** (canonical 00.32.02).
|
|
4
4
|
This line is asserted by `codegen:check` in CI — if the pin moves and this file is not
|
|
5
5
|
re-read against it, the check fails rather than letting this document rot quietly.
|
|
6
6
|
|
|
@@ -79,7 +79,10 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
|
|
|
79
79
|
- World files (no account needed; since 4.10.0): `readWorldFile(bytes)` opens a `.zip` holding one
|
|
80
80
|
world folder and returns `{ worldId, name, world, files: [{ path, data }] }`;
|
|
81
81
|
`writeWorldFile(files, topDir)` makes one. Refusals throw `OwWorldFileError` with a `reason`.
|
|
82
|
-
`world.json`'s `api` block (link and key) is removed both ways.
|
|
82
|
+
`world.json`'s `api` block (link and key) is removed both ways. Both follow Folder Format
|
|
83
|
+
§4.7 (`.owworld`): the reader checks the central directory before inflating
|
|
84
|
+
and ignores files beside the world's folder; the writer's output is byte-stable (marker
|
|
85
|
+
first, UTF-8 name order, 1980 stamps) and holds only `world.json` and the folder's parts.
|
|
83
86
|
- World calendars (since 4.20.0): `worldCalendars(elements)` finds the elements carrying
|
|
84
87
|
`atlas_calendar` (Atlas owns the field: read it, never write it); `resolveCalendarLens` picks
|
|
85
88
|
one; `formatStep(config, step)` names a point on the world's integer time line, `parseDate`
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ earlier history lives in git log only.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [4.49.0] - 2026-10-22
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **The world file follows Folder Format §4.7, the `.owworld` archive** (OW Council #522), and gives the spec's verdict and cause on every one of the 74 fixtures in its conformance set (Quillon's, `owworld-fixtures`), plus its three on-demand floor cases.
|
|
13
|
+
- The reader parses the central directory itself (zip64 included) and runs every check on names, kinds, methods and sizes before it inflates anything.
|
|
14
|
+
- Six new refusal causes join `OwWorldFileReason`: `duplicate` (OW-17), `collision` (OW-18: NFC, then simple case folding), `link` (OW-20), `method` (OW-21), `name` (OW-22) and `marker` (OW-6). A `Record<OwWorldFileReason, …>` in a caller needs the six new keys.
|
|
15
|
+
- The OW-18 fold is the C and S rows of Unicode's CaseFolding.txt 15.1, built in as a table, never the runtime's casing, so a fold a newer Unicode adds stays out until the pin moves (Skeld's ruling, OW Council #9830). `simpleCaseFold(text)` exports it.
|
|
16
|
+
- Files beside the world's folder are ignored, no longer `more_than_one` (OW-4). A `world.json` under `records/` is a producer's data, not a second world (OW-5). A spanned archive is `not_openable`. An entry that inflates past the size it declares is `too_big` at any budget (OW-14).
|
|
17
|
+
- The default budget is the spec's floor: `WORLD_FILE_MAX_BYTES` 2 GiB (was 1 GiB), `WORLD_FILE_MAX_ENTRIES` 500,000 (was 200,000) (OW-15).
|
|
18
|
+
- A `mimetype` marker is never required. One with other bytes than `WORLD_FILE_MEDIA_TYPE` (`application/vnd.onlyworlds.world+zip`, newly exported) is refused, and the marker is never returned as a file.
|
|
19
|
+
- `files` come back in code point order (the UTF-8 bytes of the name), not UTF-16 order.
|
|
20
|
+
- The writer gives one set of bytes for one input (OW-12, OW-13). It places the `mimetype` marker first and stored, orders entries by the UTF-8 bytes of their names, and stamps every entry 1980-01-01 00:00 (was the clock).
|
|
21
|
+
- The writer writes only `world.json`, `elements/`, `spatial/`, `media/`, `records/` and a tool's dot-folder it is handed (OW-7). It leaves out any other file, and a dot-name or `.meta` inside a content folder. It refuses a `name` or a `collision` as the reader would.
|
|
22
|
+
|
|
23
|
+
## [4.48.1] - 2026-10-22
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Schema repinned to schema-dist `v0.32.2-dist.37` (canonical 00.32.02 unchanged, so no type, field or constant moved): the dist added `profiles/` (identity, calendar with its fixture, packet, review), its folder lint reads known-of stubs and never `records/`, and its export example carries `source` and `exported_as`. The generated headers, `SCHEMA.md` and `AGENTS.md` name the new pin.
|
|
28
|
+
|
|
8
29
|
## [4.48.0] - 2026-10-22
|
|
9
30
|
|
|
10
31
|
### Added
|
package/README.md
CHANGED
|
@@ -240,19 +240,34 @@ const { token_rating, tokens_remaining, resets_at } = await account.allowance();
|
|
|
240
240
|
|
|
241
241
|
## World files
|
|
242
242
|
|
|
243
|
-
A world file
|
|
243
|
+
A world file (`.owworld`, a `.zip` by another name) is one archive holding a whole world folder (`world.json` and `elements/<type>/*.json`, as Atlas keeps a world on disk), the way Atlas downloads and opens one. It needs no account and works in the browser and in Node. Both functions follow the Folder Format spec's §4.7 and pass its fixture set.
|
|
244
244
|
|
|
245
245
|
```typescript
|
|
246
246
|
import { readWorldFile, writeWorldFile, OwWorldFileError } from '@onlyworlds/sdk';
|
|
247
247
|
|
|
248
248
|
const world = readWorldFile(bytes); // a Uint8Array
|
|
249
249
|
world.worldId; world.name; world.world; // world.json, parsed
|
|
250
|
-
world.files; // [{ path, data }], relative to the world's folder,
|
|
250
|
+
world.files; // [{ path, data }], relative to the world's folder, in code point order
|
|
251
251
|
|
|
252
252
|
const zip = writeWorldFile(world.files, 'hollow-reach-0192a000'); // one top-level folder
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
-
The reader
|
|
255
|
+
The reader checks every entry before it inflates anything, and refuses the file before returning anything. Check `err.reason` on the `OwWorldFileError` it throws:
|
|
256
|
+
|
|
257
|
+
| `reason` | what the file holds |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `not_openable` | not a zip, a spanned one, or a broken entry |
|
|
260
|
+
| `unsafe_path` | a path that could land outside the folder, or a path that is both a file and a folder |
|
|
261
|
+
| `name` | a name some disk cannot hold: `CON` and the other device names, a trailing dot or space, `<>:"\|?*`, a control character, a non-ASCII name without the UTF-8 flag |
|
|
262
|
+
| `duplicate`, `collision` | two entries with one name, or two names that differ only in case or in how an accent is written |
|
|
263
|
+
| `link`, `method` | a link or special file, an encrypted entry, or a method other than stored or deflate |
|
|
264
|
+
| `too_big`, `too_many` | more than 2 GiB or 500,000 files uncompressed (`maxBytes`, `maxEntries` to change them), or an entry bigger than it says |
|
|
265
|
+
| `marker` | a `mimetype` entry with other bytes than `WORLD_FILE_MEDIA_TYPE` (a marker is never required) |
|
|
266
|
+
| `not_a_world`, `more_than_one` | no `world.json` with an `id`, two worlds, or a second `world.json` inside the world (one under `records/` is data) |
|
|
267
|
+
|
|
268
|
+
Files beside the world's folder and archive junk (`__MACOSX`, `.DS_Store`) are ignored. The world's OnlyWorlds link and key (`world.json`'s `api`) never travel: both functions remove them. A tool's own folder (`.atlas/` and the like) comes back like any other file; pass `skip: path => …` to leave files out.
|
|
269
|
+
|
|
270
|
+
The writer gives the same bytes for the same files: the `mimetype` marker first, then every entry in the UTF-8 order of its name, stamped 1980-01-01 00:00. It writes `world.json`, `elements/`, `spatial/`, `media/` and `records/`, and leaves any other file out. A tool's dot-folder you hand it travels too, which makes the file a private copy rather than a portable `.owworld`.
|
|
256
271
|
|
|
257
272
|
## World calendars
|
|
258
273
|
|
package/SCHEMA.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OnlyWorlds Schema Reference
|
|
2
2
|
|
|
3
|
-
**Source**: https://github.com/OnlyWorlds/schema-dist @ **v0.32.2-dist.
|
|
3
|
+
**Source**: https://github.com/OnlyWorlds/schema-dist @ **v0.32.2-dist.37** — canonical schema **00.32.02**, published 2026-10-21.
|
|
4
4
|
|
|
5
5
|
GENERATED from the canonical schema YAML — do not hand-edit (regenerate: `python codegen/generate_types.py`).
|
|
6
6
|
Written for both humans and AI agents reading this package locally.
|
package/dist/index.d.ts
CHANGED
|
@@ -3842,33 +3842,52 @@ declare function getElementIcon(type: string): string;
|
|
|
3842
3842
|
declare function getElementLabel(elementType: ElementType): string;
|
|
3843
3843
|
|
|
3844
3844
|
/**
|
|
3845
|
-
* The world file
|
|
3846
|
-
*
|
|
3847
|
-
* back. Absorbed from Atlas's
|
|
3848
|
-
* 1.17), so Atlas, Lantern and any
|
|
3849
|
-
*
|
|
3850
|
-
*
|
|
3851
|
-
* on disk, except the OnlyWorlds link and its key (`world.json` without `api`).
|
|
3852
|
-
* JSON is deflated, everything else stored. A world.json at the root, with no
|
|
3853
|
-
* folder, reads the same.
|
|
3845
|
+
* The world file, `.owworld` (Folder Format spec §4.7; a `.zip` by another
|
|
3846
|
+
* name): one archive holding one whole world as a portable copy (§4.1), and
|
|
3847
|
+
* the reader that opens one back. Absorbed from Atlas's
|
|
3848
|
+
* `src/store/world-file.ts` (Temper's, Atlas 1.17), so Atlas, Lantern and any
|
|
3849
|
+
* script read one file the same way; brought to §4.7's checks OW-1 to OW-22
|
|
3850
|
+
* and their fixture set (OW Council #522).
|
|
3854
3851
|
*
|
|
3855
3852
|
* THE READER takes bytes and returns bytes: no DOM, `File` or file system, so
|
|
3856
|
-
* it runs in a browser and in Node alike. It
|
|
3857
|
-
*
|
|
3858
|
-
*
|
|
3859
|
-
*
|
|
3860
|
-
*
|
|
3861
|
-
*
|
|
3862
|
-
*
|
|
3863
|
-
*
|
|
3853
|
+
* it runs in a browser and in Node alike. It reads the central directory
|
|
3854
|
+
* itself (zip64 included) and runs every check on names, kinds, methods and
|
|
3855
|
+
* sizes before it inflates anything. Each refusal names one cause
|
|
3856
|
+
* (`OwWorldFileReason`), checked in the reference checker's order:
|
|
3857
|
+
* - `not_openable`: not a zip, a spanned one, a broken entry or CRC.
|
|
3858
|
+
* - `unsafe_path` (OW-16, OW-19): a name that is not a plain relative path
|
|
3859
|
+
* (`\`, an empty, `.` or `..` segment, absolute, a drive), or a path that is
|
|
3860
|
+
* both a file and a folder.
|
|
3861
|
+
* - `link` (OW-20): a Unix mode that is not a regular file or a directory.
|
|
3862
|
+
* - `method` (OW-21): encrypted, or a method other than stored or deflate.
|
|
3863
|
+
* - `name` (OW-22): a Windows device name, a trailing dot or space, one of
|
|
3864
|
+
* `<>:"|?*` or a control character, a non-ASCII name without the UTF-8 flag.
|
|
3865
|
+
* - `duplicate` (OW-17), `collision` (OW-18: NFC, then simple case folding).
|
|
3866
|
+
* - `too_many`, `too_big` (OW-14, OW-15): past the budget by count or declared
|
|
3867
|
+
* size, or an entry that inflates past the size it declares.
|
|
3868
|
+
* - `marker` (OW-6): a root `mimetype` entry with other bytes than the media
|
|
3869
|
+
* type. A marker is never required.
|
|
3870
|
+
* - `not_a_world`, `more_than_one` (OW-2, OW-3, OW-5): no world, two worlds, a
|
|
3871
|
+
* second `world.json` inside the world (one under `records/` is a
|
|
3872
|
+
* producer's data), a `world.json` that will not parse or has no string id.
|
|
3873
|
+
* Files beside the world's folder and archive junk (`__MACOSX`, `.DS_Store`,
|
|
3874
|
+
* `Thumbs.db`) are ignored (OW-4), still checked and counted.
|
|
3875
|
+
*
|
|
3876
|
+
* THE WRITER emits a portable copy (OW-7, OW-12, OW-13): the `mimetype` marker
|
|
3877
|
+
* first and stored, then only the folder's parts, every entry stamped
|
|
3878
|
+
* 1980-01-01 00:00, in the UTF-8 byte order of its name, JSON deflated and
|
|
3879
|
+
* everything else stored.
|
|
3864
3880
|
*
|
|
3865
|
-
* A tool's own `.<tool>/` folder is the tool's (spec §4):
|
|
3866
|
-
*
|
|
3881
|
+
* A tool's own `.<tool>/` folder is the tool's (spec §4): the reader returns
|
|
3882
|
+
* it like any file (a caller leaves files out with `skip`), and the writer
|
|
3883
|
+
* carries one it is handed, which makes the file a private copy (OW-7, OW-9).
|
|
3867
3884
|
*/
|
|
3868
|
-
/**
|
|
3885
|
+
/** The `mimetype` marker's exact bytes (spec §4.7, OW-6): ASCII, no newline. */
|
|
3886
|
+
declare const WORLD_FILE_MEDIA_TYPE = "application/vnd.onlyworlds.world+zip";
|
|
3887
|
+
/** The budget, uncompressed: the floor every reader accepts (spec §4.7, OW-15). */
|
|
3869
3888
|
declare const WORLD_FILE_MAX_BYTES: number;
|
|
3870
|
-
declare const WORLD_FILE_MAX_ENTRIES =
|
|
3871
|
-
type OwWorldFileReason = 'unsafe_path' | 'too_big' | 'too_many' | 'not_a_world' | 'more_than_one' | 'not_openable';
|
|
3889
|
+
declare const WORLD_FILE_MAX_ENTRIES = 500000;
|
|
3890
|
+
type OwWorldFileReason = 'unsafe_path' | 'too_big' | 'too_many' | 'not_a_world' | 'more_than_one' | 'not_openable' | 'duplicate' | 'collision' | 'link' | 'method' | 'name' | 'marker';
|
|
3872
3891
|
/** A world file was refused; nothing was returned. `reason` says why, for a tool to word its own message. */
|
|
3873
3892
|
declare class OwWorldFileError extends Error {
|
|
3874
3893
|
readonly reason: OwWorldFileReason;
|
|
@@ -3885,26 +3904,37 @@ interface OwReadWorldFile {
|
|
|
3885
3904
|
name: string;
|
|
3886
3905
|
/** `world.json`, parsed, without `api`. */
|
|
3887
3906
|
world: Record<string, unknown>;
|
|
3888
|
-
/** The world's files, relative to its folder, `world.json` included (without `api`),
|
|
3907
|
+
/** The world's files, relative to its folder, `world.json` included (without `api`), in code point order. */
|
|
3889
3908
|
files: OwWorldFileEntry[];
|
|
3890
3909
|
}
|
|
3891
3910
|
interface OwReadWorldFileOptions {
|
|
3892
3911
|
maxBytes?: number;
|
|
3893
3912
|
maxEntries?: number;
|
|
3894
|
-
/** Leave a file out of `files` (a path relative to the world's folder). It still
|
|
3913
|
+
/** Leave a file out of `files` (a path relative to the world's folder). It is still checked and counted. */
|
|
3895
3914
|
skip?: (path: string) => boolean;
|
|
3896
3915
|
}
|
|
3916
|
+
/**
|
|
3917
|
+
* Simple case folding (Unicode 15.1, CaseFolding.txt C and S), code point by
|
|
3918
|
+
* code point: what OW-18 applies after NFC. A code point the table does not
|
|
3919
|
+
* map folds to itself.
|
|
3920
|
+
*/
|
|
3921
|
+
declare function simpleCaseFold(text: string): string;
|
|
3897
3922
|
/**
|
|
3898
3923
|
* Read a world file. Throws `OwWorldFileError` saying why, having returned
|
|
3899
3924
|
* nothing; the caller writes only what this returns.
|
|
3900
3925
|
*/
|
|
3901
3926
|
declare function readWorldFile(bytes: Uint8Array, opts?: OwReadWorldFileOptions): OwReadWorldFile;
|
|
3902
3927
|
/**
|
|
3903
|
-
* Write a world file
|
|
3904
|
-
*
|
|
3905
|
-
*
|
|
3906
|
-
*
|
|
3907
|
-
*
|
|
3928
|
+
* Write a world file under one top-level folder `topDir`: the `mimetype`
|
|
3929
|
+
* marker first and stored, then `world.json` (without `api`) and the folder's
|
|
3930
|
+
* parts, in UTF-8 byte order, every entry stamped 1980-01-01 00:00, JSON
|
|
3931
|
+
* deflated and everything else stored (spec §4.7, OW-6, OW-7, OW-12, OW-13).
|
|
3932
|
+
* Any other file is left out. A tool's dot-folder in `files` travels, and
|
|
3933
|
+
* makes the result a private copy, not a portable `.owworld` (OW-7): leave it
|
|
3934
|
+
* out of `files` for a portable one. Refuses (`OwWorldFileError`) an unsafe
|
|
3935
|
+
* path or `topDir`, a duplicate path, a name some disk cannot hold, two names
|
|
3936
|
+
* that fold to one, and a world.json that is missing, will not parse (it may
|
|
3937
|
+
* hold a key, so it is never zipped raw) or has no `id`.
|
|
3908
3938
|
*/
|
|
3909
3939
|
declare function writeWorldFile(files: OwWorldFileEntry[], topDir: string): Uint8Array;
|
|
3910
3940
|
|
|
@@ -4717,4 +4747,4 @@ declare class TokenResource {
|
|
|
4717
4747
|
getEncryptionInfo(): Promise<EncryptionInfo>;
|
|
4718
4748
|
}
|
|
4719
4749
|
|
|
4720
|
-
export { AGENT_DOORS, ASK_LIMITS, type AccessKeyResponse, CALENDAR_FIELD, CALENDAR_SUPERTYPE, type CalendarConfig, type CalendarEra, type CalendarIntercalary, type CalendarMonths, type CalendarWeek, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, type EraDomain, FAMILY_COLORS, FAMILY_ORDER, FIELD_ACRONYMS, FIELD_SCHEMA, FILE_EXTENSIONS, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, type OwAccountWorld, type OwAgentJoined, type OwAllowance, OwApiError, type OwAskAnswer, type OwAskContext, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwCalendarElement, type OwChange, type OwChangeDelete, type OwChangeUpsert, type OwChangesPage, type OwClientConfig, type OwCreatedWorldLink, type OwDeleteReason, type OwDraught, type OwDraughtComponent, type OwDraughtItem, type OwDraughtPolicy, type OwElement, type OwElementBase, type OwErrorBody, type OwFileAddedBy, type OwFileDownload, type OwFileKind, type OwFileRow, type OwFileTicket, type OwHiddenRow, type OwJoinPreview, type OwJoined, type OwJoinedMembership, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMe, type OwMediaTicket, type OwMember, type OwMemberRole, type OwMintedKey, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, type OwPlayerJoined, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwResyncReason, type OwSeat, type OwSeatCreated, type OwSignInBegin, type OwSignInBegun, type OwSignInComplete, OwSignInError, type OwSignInReason, type OwSignInResult, type OwSignInStorage, type OwSignedIn, type OwSortAddToRow, type OwSortAnswer, type OwSortAskRow, type OwSortLinkRow, type OwSortMakeRow, type OwSortNote, type OwSortRef, type OwSortRow, type OwUploadedFile, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldFileRow, type OwWorldFiles, type OwWorldIndex, type OwWorldLink, type OwWorldMeta, type OwWriteDoor, PHOTO_MAX_BYTES, PHOTO_TYPES, PUBLIC_FIELDS, type ParseError, type ParseOptions, type ParseResult, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type StepBreakdown, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, type WorldCalendar, absYearForEraYear, absoluteYear, approxYearLength, base64url, breakdownStep, buildAskContext, buildWorldIndex, detectKeyKind, displayYear, elementColor, eraForStep, errorFromResponse, familyOf, fieldLabel, fileKindOf, floorDiv, floorMod, formatDuration, formatMidLabel, formatStep, formatYearLabel, getElementIcon, getElementLabel, isAgentDoor, isDemoKey, isKnownOf, isLeapYear, isParseError, kindCanWrite, lookupWorld, monthStartSteps, parseDate, parseErrorEnvelope, pinExpectation, pkceChallenge, planPhoto, preparePhoto, readCalendarConfig, readCalendarValue, readWorldFile, renderElement, resolveCalendarLens, resolveEraDomains, validateCalendarConfig, weekdayForStep, worldCalendars, writeWorldFile, yearLength, yearStartStep };
|
|
4750
|
+
export { AGENT_DOORS, ASK_LIMITS, type AccessKeyResponse, CALENDAR_FIELD, CALENDAR_SUPERTYPE, type CalendarConfig, type CalendarEra, type CalendarIntercalary, type CalendarMonths, type CalendarWeek, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, type EraDomain, FAMILY_COLORS, FAMILY_ORDER, FIELD_ACRONYMS, FIELD_SCHEMA, FILE_EXTENSIONS, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, type OwAccountWorld, type OwAgentJoined, type OwAllowance, OwApiError, type OwAskAnswer, type OwAskContext, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwCalendarElement, type OwChange, type OwChangeDelete, type OwChangeUpsert, type OwChangesPage, type OwClientConfig, type OwCreatedWorldLink, type OwDeleteReason, type OwDraught, type OwDraughtComponent, type OwDraughtItem, type OwDraughtPolicy, type OwElement, type OwElementBase, type OwErrorBody, type OwFileAddedBy, type OwFileDownload, type OwFileKind, type OwFileRow, type OwFileTicket, type OwHiddenRow, type OwJoinPreview, type OwJoined, type OwJoinedMembership, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMe, type OwMediaTicket, type OwMember, type OwMemberRole, type OwMintedKey, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, type OwPlayerJoined, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwResyncReason, type OwSeat, type OwSeatCreated, type OwSignInBegin, type OwSignInBegun, type OwSignInComplete, OwSignInError, type OwSignInReason, type OwSignInResult, type OwSignInStorage, type OwSignedIn, type OwSortAddToRow, type OwSortAnswer, type OwSortAskRow, type OwSortLinkRow, type OwSortMakeRow, type OwSortNote, type OwSortRef, type OwSortRow, type OwUploadedFile, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldFileRow, type OwWorldFiles, type OwWorldIndex, type OwWorldLink, type OwWorldMeta, type OwWriteDoor, PHOTO_MAX_BYTES, PHOTO_TYPES, PUBLIC_FIELDS, type ParseError, type ParseOptions, type ParseResult, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type StepBreakdown, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, WORLD_FILE_MEDIA_TYPE, type WorldCalendar, absYearForEraYear, absoluteYear, approxYearLength, base64url, breakdownStep, buildAskContext, buildWorldIndex, detectKeyKind, displayYear, elementColor, eraForStep, errorFromResponse, familyOf, fieldLabel, fileKindOf, floorDiv, floorMod, formatDuration, formatMidLabel, formatStep, formatYearLabel, getElementIcon, getElementLabel, isAgentDoor, isDemoKey, isKnownOf, isLeapYear, isParseError, kindCanWrite, lookupWorld, monthStartSteps, parseDate, parseErrorEnvelope, pinExpectation, pkceChallenge, planPhoto, preparePhoto, readCalendarConfig, readCalendarValue, readWorldFile, renderElement, resolveCalendarLens, resolveEraDomains, simpleCaseFold, validateCalendarConfig, weekdayForStep, worldCalendars, writeWorldFile, yearLength, yearStartStep };
|
package/dist/index.js
CHANGED
|
@@ -2514,17 +2514,25 @@ function getElementLabel(elementType) {
|
|
|
2514
2514
|
}
|
|
2515
2515
|
|
|
2516
2516
|
// src/world-file.ts
|
|
2517
|
-
import {
|
|
2517
|
+
import { Inflate, inflateSync, zipSync } from "fflate";
|
|
2518
2518
|
var WORLD_FILE = "world.json";
|
|
2519
|
-
var
|
|
2520
|
-
var
|
|
2519
|
+
var MARKER_NAME = "mimetype";
|
|
2520
|
+
var WORLD_FILE_MEDIA_TYPE = "application/vnd.onlyworlds.world+zip";
|
|
2521
|
+
var WORLD_FILE_MAX_BYTES = 2 * 1024 ** 3;
|
|
2522
|
+
var WORLD_FILE_MAX_ENTRIES = 5e5;
|
|
2521
2523
|
var MESSAGES = {
|
|
2522
2524
|
unsafe_path: "The world file holds a path that cannot be written safely.",
|
|
2523
|
-
too_big: "The world file is bigger than the budget allows.",
|
|
2525
|
+
too_big: "The world file is bigger than the budget allows, or a file in it is bigger than it says.",
|
|
2524
2526
|
too_many: "The world file holds more files than the budget allows.",
|
|
2525
2527
|
not_a_world: "The file is not a world: it has no readable world.json with an id.",
|
|
2526
|
-
more_than_one: "The file holds more than one world
|
|
2527
|
-
not_openable: "The file is not a zip that can be read."
|
|
2528
|
+
more_than_one: "The file holds more than one world.",
|
|
2529
|
+
not_openable: "The file is not a zip that can be read.",
|
|
2530
|
+
duplicate: "The world file holds two entries with the same name.",
|
|
2531
|
+
collision: "The world file holds two names that differ only in case or in how an accent is written.",
|
|
2532
|
+
link: "The world file holds a link or a special file.",
|
|
2533
|
+
method: "The world file holds an encrypted entry, or one compressed other than stored or deflate.",
|
|
2534
|
+
name: "The world file holds a name that some disk cannot hold.",
|
|
2535
|
+
marker: "The world file's mimetype entry is not the OnlyWorlds media type."
|
|
2528
2536
|
};
|
|
2529
2537
|
var OwWorldFileError = class extends Error {
|
|
2530
2538
|
constructor(reason, message = MESSAGES[reason]) {
|
|
@@ -2533,13 +2541,228 @@ var OwWorldFileError = class extends Error {
|
|
|
2533
2541
|
this.reason = reason;
|
|
2534
2542
|
}
|
|
2535
2543
|
};
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2544
|
+
var refuse = (reason, message) => {
|
|
2545
|
+
throw new OwWorldFileError(reason, message);
|
|
2546
|
+
};
|
|
2547
|
+
function pathFault(name) {
|
|
2548
|
+
if (name.includes("\\")) return "holds a backslash";
|
|
2549
|
+
if (name.startsWith("/")) return "is absolute";
|
|
2550
|
+
if (/^[A-Za-z]:/.test(name)) return "names a drive";
|
|
2551
|
+
if (name.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) return "has an empty, . or .. segment";
|
|
2552
|
+
return null;
|
|
2553
|
+
}
|
|
2554
|
+
var RESERVED = /* @__PURE__ */ new Set([
|
|
2555
|
+
"CON",
|
|
2556
|
+
"PRN",
|
|
2557
|
+
"AUX",
|
|
2558
|
+
"NUL",
|
|
2559
|
+
...[1, 2, 3, 4, 5, 6, 7, 8, 9].flatMap((i) => [`COM${i}`, `LPT${i}`])
|
|
2560
|
+
]);
|
|
2561
|
+
var BAD_CHAR = /[<>:"|?*\p{Cc}]/u;
|
|
2562
|
+
function nameFault(name) {
|
|
2563
|
+
for (const seg of name.split("/")) {
|
|
2564
|
+
if (BAD_CHAR.test(seg)) return `segment ${JSON.stringify(seg)} holds a reserved or control character`;
|
|
2565
|
+
if (seg.endsWith(".") || seg.endsWith(" ")) return `segment ${JSON.stringify(seg)} ends in a dot or a space`;
|
|
2566
|
+
if (RESERVED.has(seg.split(".")[0].toUpperCase())) return `segment ${JSON.stringify(seg)} is a Windows device name`;
|
|
2567
|
+
}
|
|
2568
|
+
return null;
|
|
2539
2569
|
}
|
|
2540
2570
|
function isJunk(path) {
|
|
2541
2571
|
return path.split("/").some((seg) => seg === "__MACOSX" || seg === ".DS_Store" || seg === "Thumbs.db");
|
|
2542
2572
|
}
|
|
2573
|
+
var FOLD_RUNS = "1t.q..w,51...lj,5c.n..w,60.7..w,74.o.2.1,8i.3.2.1,8p.8.2.1,96.n.2.1,ag...-3d,ah.3.2.1,an...-7g,ap...5u,aq.2.2.1,au...5q,av...1,ax.2..5p,az...1,b2...27,b3...5m,b4...5n,b5...1,b7...5p,b8...5r,ba...5v,bb...5t,bc...1,bg...5v,bh...5x,bj...5y,bk.3.2.1,bq...62,br...1,bt...62,bw...1,by...62,bz...1,c1.2..61,c3.2.2.1,c7...63,c8...1,cc...1,ck...2,cl...1,cn...2,co...1,cq...2,cr.9.2.1,da.9.2.1,dt...2,du.2.2.1,dy...-2p,dz...-1k,e0.k.2.1,f4...-3m,f6.9.2.1,fu...8bv,fv...1,fx...-4j,fy...8bs,g1...1,g3...-5f,g4...1x,g5...1z,g6.5.2.1,n9...38,og.2.2.1,om...1,ov...38,p2...12,p4.3..11,p8...1s,pa.2..1r,pd.h..w,pv.9..w,qq...1,r3...8,r4...-u,r5...-p,r9...-f,ra...-m,rc.c.2.1,s0...-1i,s1...-1c,s4...-1o,s5...-1s,s7...1,s9...-7,sa...1,sd.3..-3m,sg.g..28,sw.w..w,v4.h.2.1,wa.r.2.1,xs...f,xt.7.2.1,y8.1c.2.1,10x.12..1c,3a8.12..5ls,3bb...5ls,3bh...5ls,3y0.6..-8,5mo...-4su,5mp...-4st,5mq...-4sk,5mr.2..-4si,5mt...-4sj,5mu...-4sc,5mv...-4ro,5mw...r7n,5n4.17..-2bk,5od.3..-2bk,5xc.23.2.1,61n...-1m,61q...-5vj,61s.1c.2.1,64o.8..-8,654.6..-8,65k.8..-8,660.8..-8,66g.6..-8,66x.4.2.-8,67c.8..-8,688.8..-8,68o.8..-8,694.8..-8,69k.2..-8,69m.2..-22,69o...-9,69q...-5j9,6a0.4..-2e,6a4...-9,6ab...-5kz,6ag.2..-8,6ai.2..-2s,6ar...-5kj,6aw.2..-8,6ay.2..-34,6b0...-7,6bc.2..-3k,6be.2..-3i,6bg...-9,6jq...-5st,6ju...-6gv,6jv...-6di,6k2...s,6lc.g..g,6mb...1,792.q..q,8ow.1c..1c,8rk...1,8rm...-8af,8rn...-2xy,8ro...-89z,8rr.3.2.1,8rx...-8bg,8ry...-8al,8rz...-8bj,8s0...-8bi,8s2...1,8s5...1,8se.2..-8cf,8sg.1e.2.1,8vf.2.2.1,8vm...1,wu8.n.2.1,ww0.e.2.1,x0i.7.2.1,x0y.v.2.1,x2x.2.2.1,x31...-r9g,x32.5.2.1,x3f...1,x3h...-wmg,x3k.2.2.1,x3q.a.2.1,x4a...-wn8,x4b...-wnj,x4c...-wnf,x4d...-wn5,x4e...-wn8,x4g...-wlu,x4h...-wmi,x4i...-wlx,x4j...ps,x4k.8.2.1,x50...-1c,x51...-wn7,x52...-raw,x53.2.2.1,x5c...1,x5i.2.2.1,x6d...1,xv4.28..-tzk,1dl1...1,1ee9.q..w,1fcw.14..14,1fhs.10..14,1fn4.b..13,1fng.f..13,1fnw.7..13,1fo4.2..13,1h1c.1f..1s,1jfk.w..w,20cg.w..w,2olc.y..y";
|
|
2574
|
+
var foldTable = null;
|
|
2575
|
+
function simpleCaseFold(text) {
|
|
2576
|
+
if (!foldTable) {
|
|
2577
|
+
foldTable = /* @__PURE__ */ new Map();
|
|
2578
|
+
for (const run of FOLD_RUNS.split(",")) {
|
|
2579
|
+
const [start, count, stride, delta] = run.split(".");
|
|
2580
|
+
const s = parseInt(start, 36);
|
|
2581
|
+
const n = count ? parseInt(count, 36) : 1;
|
|
2582
|
+
const step = stride ? parseInt(stride, 36) : 1;
|
|
2583
|
+
const d = parseInt(delta, 36);
|
|
2584
|
+
for (let k = 0; k < n; k++) foldTable.set(s + k * step, s + k * step + d);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
let out = "";
|
|
2588
|
+
for (const c of text) {
|
|
2589
|
+
const to = foldTable.get(c.codePointAt(0));
|
|
2590
|
+
out += to === void 0 ? c : String.fromCodePoint(to);
|
|
2591
|
+
}
|
|
2592
|
+
return out;
|
|
2593
|
+
}
|
|
2594
|
+
function foldName(name) {
|
|
2595
|
+
return simpleCaseFold(name.normalize("NFC"));
|
|
2596
|
+
}
|
|
2597
|
+
function byCodePoint(a, b) {
|
|
2598
|
+
const n = Math.min(a.length, b.length);
|
|
2599
|
+
for (let i = 0; i < n; i++) {
|
|
2600
|
+
const x = a.codePointAt(i);
|
|
2601
|
+
const y = b.codePointAt(i);
|
|
2602
|
+
if (x !== y) return x - y;
|
|
2603
|
+
if (x > 65535) i++;
|
|
2604
|
+
}
|
|
2605
|
+
return a.length - b.length;
|
|
2606
|
+
}
|
|
2607
|
+
var u16 = (b, o) => b[o] | b[o + 1] << 8;
|
|
2608
|
+
var u32 = (b, o) => (b[o] | b[o + 1] << 8 | b[o + 2] << 16) + b[o + 3] * 16777216;
|
|
2609
|
+
var u64 = (b, o) => u32(b, o) + u32(b, o + 4) * 4294967296;
|
|
2610
|
+
var SIG_LOCAL = 67324752;
|
|
2611
|
+
var SIG_CENTRAL = 33639248;
|
|
2612
|
+
var SIG_END = 101010256;
|
|
2613
|
+
var SIG_END64 = 101075792;
|
|
2614
|
+
var SIG_LOCATOR64 = 117853008;
|
|
2615
|
+
var MAX32 = 4294967295;
|
|
2616
|
+
var utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
2617
|
+
var latin1 = (b) => {
|
|
2618
|
+
let s = "";
|
|
2619
|
+
for (const x of b) s += String.fromCharCode(x);
|
|
2620
|
+
return s;
|
|
2621
|
+
};
|
|
2622
|
+
function centralDirectory(bytes) {
|
|
2623
|
+
const len = bytes.length;
|
|
2624
|
+
let end = -1;
|
|
2625
|
+
for (let i = len - 22; i >= Math.max(0, len - 22 - 65535); i--) {
|
|
2626
|
+
if (u32(bytes, i) === SIG_END) {
|
|
2627
|
+
end = i;
|
|
2628
|
+
break;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
if (end < 0) refuse("not_openable", "The file is not a zip: it has no end of central directory.");
|
|
2632
|
+
const thisDisk = u16(bytes, end + 4);
|
|
2633
|
+
const cdDisk = u16(bytes, end + 6);
|
|
2634
|
+
if (!(thisDisk === 0 && cdDisk === 0 || thisDisk === 65535 && cdDisk === 65535)) {
|
|
2635
|
+
refuse("not_openable", `The zip spans disks (its end record says disk ${thisDisk}, the directory on disk ${cdDisk}).`);
|
|
2636
|
+
}
|
|
2637
|
+
let cdSize = u32(bytes, end + 12);
|
|
2638
|
+
let cdOffset = u32(bytes, end + 16);
|
|
2639
|
+
const loc = end - 20;
|
|
2640
|
+
if (loc >= 0 && u32(bytes, loc) === SIG_LOCATOR64) {
|
|
2641
|
+
if (u32(bytes, loc + 4) !== 0 || u32(bytes, loc + 16) > 1) refuse("not_openable", "The zip spans disks (zip64).");
|
|
2642
|
+
const rec = u64(bytes, loc + 8);
|
|
2643
|
+
if (rec + 56 > len || u32(bytes, rec) !== SIG_END64) refuse("not_openable", "The zip64 end record is missing.");
|
|
2644
|
+
if (u32(bytes, rec + 16) !== 0 || u32(bytes, rec + 20) !== 0) refuse("not_openable", "The zip spans disks (zip64).");
|
|
2645
|
+
cdSize = u64(bytes, rec + 40);
|
|
2646
|
+
cdOffset = u64(bytes, rec + 48);
|
|
2647
|
+
}
|
|
2648
|
+
const cdEnd = cdOffset + cdSize;
|
|
2649
|
+
if (cdEnd > len) refuse("not_openable", "The central directory lies past the end of the file.");
|
|
2650
|
+
const out = [];
|
|
2651
|
+
let p = cdOffset;
|
|
2652
|
+
while (p < cdEnd) {
|
|
2653
|
+
if (p + 46 > cdEnd || u32(bytes, p) !== SIG_CENTRAL) refuse("not_openable", "The central directory is broken.");
|
|
2654
|
+
const flags = u16(bytes, p + 8);
|
|
2655
|
+
const nlen = u16(bytes, p + 28);
|
|
2656
|
+
const elen = u16(bytes, p + 30);
|
|
2657
|
+
const clen = u16(bytes, p + 32);
|
|
2658
|
+
const nameBytes = bytes.subarray(p + 46, p + 46 + nlen);
|
|
2659
|
+
const extra = bytes.subarray(p + 46 + nlen, p + 46 + nlen + elen);
|
|
2660
|
+
let usize = u32(bytes, p + 24);
|
|
2661
|
+
let csize = u32(bytes, p + 20);
|
|
2662
|
+
let offset = u32(bytes, p + 42);
|
|
2663
|
+
if (usize === MAX32 || csize === MAX32 || offset === MAX32) {
|
|
2664
|
+
for (let q = 0; q + 4 <= extra.length; q += 4 + u16(extra, q + 2)) {
|
|
2665
|
+
if (u16(extra, q) !== 1) continue;
|
|
2666
|
+
let r = q + 4;
|
|
2667
|
+
const stop = r + u16(extra, q + 2);
|
|
2668
|
+
const next = () => {
|
|
2669
|
+
if (r + 8 > stop) refuse("not_openable", "A zip64 extra field is short.");
|
|
2670
|
+
const v = u64(extra, r);
|
|
2671
|
+
r += 8;
|
|
2672
|
+
return v;
|
|
2673
|
+
};
|
|
2674
|
+
if (usize === MAX32) usize = next();
|
|
2675
|
+
if (csize === MAX32) csize = next();
|
|
2676
|
+
if (offset === MAX32) offset = next();
|
|
2677
|
+
break;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
const nonAscii = nameBytes.some((x) => x > 127);
|
|
2681
|
+
let name;
|
|
2682
|
+
let badUtf8 = false;
|
|
2683
|
+
if (flags & 2048) {
|
|
2684
|
+
try {
|
|
2685
|
+
name = utf8.decode(nameBytes);
|
|
2686
|
+
} catch {
|
|
2687
|
+
name = latin1(nameBytes);
|
|
2688
|
+
badUtf8 = true;
|
|
2689
|
+
}
|
|
2690
|
+
} else {
|
|
2691
|
+
name = latin1(nameBytes);
|
|
2692
|
+
}
|
|
2693
|
+
out.push({
|
|
2694
|
+
name,
|
|
2695
|
+
flags,
|
|
2696
|
+
method: u16(bytes, p + 10),
|
|
2697
|
+
crc: u32(bytes, p + 16),
|
|
2698
|
+
csize,
|
|
2699
|
+
usize,
|
|
2700
|
+
offset,
|
|
2701
|
+
external: u32(bytes, p + 38),
|
|
2702
|
+
nonAscii,
|
|
2703
|
+
badUtf8
|
|
2704
|
+
});
|
|
2705
|
+
p += 46 + nlen + elen + clen;
|
|
2706
|
+
}
|
|
2707
|
+
if (p !== cdEnd) refuse("not_openable", "The central directory is broken.");
|
|
2708
|
+
return out;
|
|
2709
|
+
}
|
|
2710
|
+
var crcTable = null;
|
|
2711
|
+
function crc32(data) {
|
|
2712
|
+
if (!crcTable) {
|
|
2713
|
+
crcTable = new Int32Array(256);
|
|
2714
|
+
for (let n = 0; n < 256; n++) {
|
|
2715
|
+
let c2 = n;
|
|
2716
|
+
for (let k = 0; k < 8; k++) c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
|
|
2717
|
+
crcTable[n] = c2;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
let c = -1;
|
|
2721
|
+
for (let i = 0; i < data.length; i++) c = crcTable[(c ^ data[i]) & 255] ^ c >>> 8;
|
|
2722
|
+
return (c ^ -1) >>> 0;
|
|
2723
|
+
}
|
|
2724
|
+
var CHUNK = 64 * 1024;
|
|
2725
|
+
function inflateEntry(bytes, e) {
|
|
2726
|
+
const off = e.offset;
|
|
2727
|
+
if (off + 30 > bytes.length || u32(bytes, off) !== SIG_LOCAL) refuse("not_openable", `${e.name}: no local header at its offset.`);
|
|
2728
|
+
const start = off + 30 + u16(bytes, off + 26) + u16(bytes, off + 28);
|
|
2729
|
+
const payload = bytes.subarray(start, start + e.csize);
|
|
2730
|
+
let data;
|
|
2731
|
+
if (e.method === 0) {
|
|
2732
|
+
data = payload;
|
|
2733
|
+
} else if (payload.length <= CHUNK) {
|
|
2734
|
+
try {
|
|
2735
|
+
data = inflateSync(payload);
|
|
2736
|
+
} catch {
|
|
2737
|
+
return refuse("not_openable", `${e.name}: its deflate stream is broken.`);
|
|
2738
|
+
}
|
|
2739
|
+
} else {
|
|
2740
|
+
const out = new Uint8Array(e.usize);
|
|
2741
|
+
let n = 0;
|
|
2742
|
+
let over = false;
|
|
2743
|
+
const inf = new Inflate((chunk) => {
|
|
2744
|
+
if (n + chunk.length > e.usize) {
|
|
2745
|
+
over = true;
|
|
2746
|
+
throw new Error("past its size");
|
|
2747
|
+
}
|
|
2748
|
+
out.set(chunk, n);
|
|
2749
|
+
n += chunk.length;
|
|
2750
|
+
});
|
|
2751
|
+
try {
|
|
2752
|
+
for (let i = 0; i < payload.length; i += CHUNK) {
|
|
2753
|
+
const stop = Math.min(i + CHUNK, payload.length);
|
|
2754
|
+
inf.push(payload.subarray(i, stop), stop === payload.length);
|
|
2755
|
+
}
|
|
2756
|
+
} catch {
|
|
2757
|
+
if (!over) refuse("not_openable", `${e.name}: its deflate stream is broken.`);
|
|
2758
|
+
}
|
|
2759
|
+
data = over ? new Uint8Array(e.usize + 1) : out.subarray(0, n);
|
|
2760
|
+
}
|
|
2761
|
+
if (data.length > e.usize) refuse("too_big", `${e.name} holds more than the ${e.usize} bytes it declares.`);
|
|
2762
|
+
if (data.length !== e.usize) refuse("not_openable", `${e.name} holds ${data.length} bytes and declares ${e.usize}.`);
|
|
2763
|
+
if (crc32(data) !== e.crc) refuse("not_openable", `${e.name} fails its CRC.`);
|
|
2764
|
+
return e.method === 0 ? data.slice() : data;
|
|
2765
|
+
}
|
|
2543
2766
|
function parseWorld(data) {
|
|
2544
2767
|
let meta;
|
|
2545
2768
|
try {
|
|
@@ -2557,93 +2780,157 @@ function withoutLink(data, meta) {
|
|
|
2557
2780
|
const { api: _link, ...rest } = meta;
|
|
2558
2781
|
return { data: new TextEncoder().encode(JSON.stringify(rest, null, 2) + "\n"), world: rest };
|
|
2559
2782
|
}
|
|
2560
|
-
function
|
|
2561
|
-
const
|
|
2562
|
-
const
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
refusal = "unsafe_path";
|
|
2574
|
-
return false;
|
|
2575
|
-
}
|
|
2576
|
-
if (isJunk(file.name)) return false;
|
|
2577
|
-
count++;
|
|
2578
|
-
declared += file.originalSize;
|
|
2579
|
-
if (count > maxEntries) {
|
|
2580
|
-
refusal = "too_many";
|
|
2581
|
-
return false;
|
|
2582
|
-
}
|
|
2583
|
-
if (declared > maxBytes) {
|
|
2584
|
-
refusal = "too_big";
|
|
2585
|
-
return false;
|
|
2586
|
-
}
|
|
2587
|
-
return true;
|
|
2783
|
+
function checkCollisions(names) {
|
|
2784
|
+
const byFold = /* @__PURE__ */ new Map();
|
|
2785
|
+
const segFold = /* @__PURE__ */ new Map();
|
|
2786
|
+
const done = /* @__PURE__ */ new Set();
|
|
2787
|
+
for (const raw of names) {
|
|
2788
|
+
const segs = (raw.endsWith("/") ? raw.slice(0, -1) : raw).split("/");
|
|
2789
|
+
let prefix = "";
|
|
2790
|
+
let folded = "";
|
|
2791
|
+
for (let i = 0; i < segs.length; i++) {
|
|
2792
|
+
let f = segFold.get(segs[i]);
|
|
2793
|
+
if (f === void 0) {
|
|
2794
|
+
f = foldName(segs[i]);
|
|
2795
|
+
segFold.set(segs[i], f);
|
|
2588
2796
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2797
|
+
prefix = i ? `${prefix}/${segs[i]}` : segs[i];
|
|
2798
|
+
folded = i ? `${folded}/${f}` : f;
|
|
2799
|
+
if (done.has(prefix)) continue;
|
|
2800
|
+
done.add(prefix);
|
|
2801
|
+
const other = byFold.get(folded);
|
|
2802
|
+
if (other !== void 0) {
|
|
2803
|
+
refuse("collision", `The names ${JSON.stringify(other)} and ${JSON.stringify(prefix)} are one name after NFC and case folding.`);
|
|
2804
|
+
}
|
|
2805
|
+
byFold.set(folded, prefix);
|
|
2806
|
+
}
|
|
2592
2807
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
const
|
|
2597
|
-
for (const n of names) {
|
|
2808
|
+
}
|
|
2809
|
+
function checkFileAndFolder(files) {
|
|
2810
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2811
|
+
for (const n of files) {
|
|
2598
2812
|
const segs = n.split("/");
|
|
2599
|
-
|
|
2600
|
-
|
|
2813
|
+
let prefix = "";
|
|
2814
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
2815
|
+
prefix = i ? `${prefix}/${segs[i]}` : segs[i];
|
|
2816
|
+
if (seen.has(prefix)) continue;
|
|
2817
|
+
seen.add(prefix);
|
|
2818
|
+
if (files.has(prefix)) refuse("unsafe_path", `${JSON.stringify(prefix)} is a file and a folder.`);
|
|
2601
2819
|
}
|
|
2602
2820
|
}
|
|
2821
|
+
}
|
|
2822
|
+
function readWorldFile(bytes, opts = {}) {
|
|
2823
|
+
const maxBytes = opts.maxBytes ?? WORLD_FILE_MAX_BYTES;
|
|
2824
|
+
const maxEntries = opts.maxEntries ?? WORLD_FILE_MAX_ENTRIES;
|
|
2825
|
+
const entries = centralDirectory(bytes);
|
|
2826
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2827
|
+
const files = [];
|
|
2828
|
+
let declared = 0;
|
|
2829
|
+
for (const e of entries) {
|
|
2830
|
+
const name2 = e.name;
|
|
2831
|
+
const isDir = name2.endsWith("/");
|
|
2832
|
+
const bare = isDir ? name2.slice(0, -1) : name2;
|
|
2833
|
+
const quoted = JSON.stringify(name2);
|
|
2834
|
+
const fault = pathFault(bare);
|
|
2835
|
+
if (fault) refuse("unsafe_path", `The entry ${quoted} ${fault}.`);
|
|
2836
|
+
const kind = e.external >>> 16 & 61440;
|
|
2837
|
+
if (kind !== 0 && kind !== 32768 && kind !== 16384) refuse("link", `The entry ${quoted} is a link or a special file.`);
|
|
2838
|
+
if (e.flags & 65) refuse("method", `The entry ${quoted} is encrypted.`);
|
|
2839
|
+
if (e.method !== 0 && e.method !== 8) refuse("method", `The entry ${quoted} uses method ${e.method} (only stored and deflate).`);
|
|
2840
|
+
if (e.badUtf8) refuse("name", `The entry ${quoted} is flagged UTF-8 and is not.`);
|
|
2841
|
+
if (e.nonAscii && !(e.flags & 2048)) refuse("name", `The entry ${quoted} is not ASCII and lacks the UTF-8 flag.`);
|
|
2842
|
+
const bad = nameFault(bare);
|
|
2843
|
+
if (bad) refuse("name", `The entry ${quoted}: ${bad}.`);
|
|
2844
|
+
if (seen.has(name2)) refuse("duplicate", `The entry ${quoted} appears twice.`);
|
|
2845
|
+
seen.add(name2);
|
|
2846
|
+
if (isDir) continue;
|
|
2847
|
+
files.push(e);
|
|
2848
|
+
if (files.length > maxEntries) refuse("too_many", `The world file holds more than ${maxEntries} files.`);
|
|
2849
|
+
declared += e.usize;
|
|
2850
|
+
if (declared > maxBytes) refuse("too_big", `The world file declares more than ${maxBytes} bytes.`);
|
|
2851
|
+
}
|
|
2852
|
+
checkCollisions(seen);
|
|
2853
|
+
const names = new Set(files.map((e) => e.name));
|
|
2854
|
+
checkFileAndFolder(names);
|
|
2855
|
+
const marker = files.find((e) => e.name === MARKER_NAME);
|
|
2856
|
+
if (marker && new TextDecoder().decode(inflateEntry(bytes, marker)) !== WORLD_FILE_MEDIA_TYPE) {
|
|
2857
|
+
refuse("marker", "The mimetype entry holds other bytes than the OnlyWorlds media type.");
|
|
2858
|
+
}
|
|
2859
|
+
const candidates = [...names].filter((n) => n !== MARKER_NAME && !isJunk(n));
|
|
2603
2860
|
let prefix;
|
|
2604
|
-
if (
|
|
2861
|
+
if (candidates.includes(WORLD_FILE)) {
|
|
2605
2862
|
prefix = "";
|
|
2606
2863
|
} else {
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2609
|
-
if (
|
|
2610
|
-
|
|
2611
|
-
|
|
2864
|
+
const roots = new Set(candidates.filter((n) => n.split("/").length === 2 && n.endsWith(`/${WORLD_FILE}`) && !n.startsWith(".")).map((n) => n.slice(0, -WORLD_FILE.length)));
|
|
2865
|
+
if (roots.size === 0) refuse("not_a_world", "There is no world.json at the root or in one top-level folder.");
|
|
2866
|
+
if (roots.size > 1) refuse("more_than_one", `The file holds worlds in ${[...roots].join(", ")}.`);
|
|
2867
|
+
prefix = [...roots][0];
|
|
2868
|
+
}
|
|
2869
|
+
const inside = new Set(candidates.filter((n) => n.startsWith(prefix)));
|
|
2870
|
+
for (const n of inside) {
|
|
2871
|
+
const rel = n.slice(prefix.length);
|
|
2872
|
+
if (rel !== WORLD_FILE && rel.endsWith(`/${WORLD_FILE}`) && !rel.startsWith("records/")) {
|
|
2873
|
+
refuse("more_than_one", `A second world.json sits inside the world: ${n}.`);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
const content = /* @__PURE__ */ new Map();
|
|
2877
|
+
for (const e of files) {
|
|
2878
|
+
const data = inflateEntry(bytes, e);
|
|
2879
|
+
if (inside.has(e.name)) content.set(e.name, data);
|
|
2612
2880
|
}
|
|
2613
2881
|
const worldName = `${prefix}${WORLD_FILE}`;
|
|
2614
|
-
|
|
2615
|
-
const
|
|
2616
|
-
const
|
|
2617
|
-
for (const n of names) {
|
|
2882
|
+
const parsed = withoutLink(content.get(worldName), parseWorld(content.get(worldName)));
|
|
2883
|
+
const out = [];
|
|
2884
|
+
for (const [n, data] of content) {
|
|
2618
2885
|
const path = n.slice(prefix.length);
|
|
2619
2886
|
if (opts.skip?.(path)) continue;
|
|
2620
|
-
|
|
2887
|
+
out.push({ path, data: n === worldName ? parsed.data : data });
|
|
2621
2888
|
}
|
|
2622
|
-
|
|
2889
|
+
out.sort((a, b) => byCodePoint(a.path, b.path));
|
|
2623
2890
|
const name = parsed.world.name;
|
|
2624
2891
|
return {
|
|
2625
2892
|
worldId: parsed.world.id,
|
|
2626
2893
|
name: typeof name === "string" ? name : "",
|
|
2627
2894
|
world: parsed.world,
|
|
2628
|
-
files
|
|
2895
|
+
files: out
|
|
2629
2896
|
};
|
|
2630
2897
|
}
|
|
2898
|
+
var PARTS = /* @__PURE__ */ new Set(["elements", "spatial", "media", "records"]);
|
|
2899
|
+
function carried(path) {
|
|
2900
|
+
if (path === WORLD_FILE) return true;
|
|
2901
|
+
const segs = path.split("/");
|
|
2902
|
+
if (segs.length < 2) return false;
|
|
2903
|
+
if (segs[0].startsWith(".")) return true;
|
|
2904
|
+
if (!PARTS.has(segs[0])) return false;
|
|
2905
|
+
if (segs[0] === "records") return true;
|
|
2906
|
+
return !segs.some((s) => s.startsWith(".")) && !path.endsWith(".meta");
|
|
2907
|
+
}
|
|
2908
|
+
var STAMP = new Date(1980, 0, 1, 0, 0, 0);
|
|
2631
2909
|
function writeWorldFile(files, topDir) {
|
|
2632
|
-
if (
|
|
2633
|
-
const
|
|
2634
|
-
let sawWorld = false;
|
|
2910
|
+
if (pathFault(topDir) || topDir.includes("/")) throw new OwWorldFileError("unsafe_path");
|
|
2911
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2635
2912
|
for (const f of files) {
|
|
2636
|
-
if (
|
|
2637
|
-
|
|
2638
|
-
|
|
2913
|
+
if (pathFault(f.path)) throw new OwWorldFileError("unsafe_path");
|
|
2914
|
+
if (seen.has(f.path)) throw new OwWorldFileError("unsafe_path", `The path ${f.path} is listed twice.`);
|
|
2915
|
+
seen.add(f.path);
|
|
2916
|
+
}
|
|
2917
|
+
const kept = files.filter((f) => carried(f.path));
|
|
2918
|
+
if (!kept.some((f) => f.path === WORLD_FILE)) throw new OwWorldFileError("not_a_world");
|
|
2919
|
+
const keys = kept.map((f) => `${topDir}/${f.path}`);
|
|
2920
|
+
for (const key of keys) {
|
|
2921
|
+
const bad = nameFault(key);
|
|
2922
|
+
if (bad) throw new OwWorldFileError("name", `The path ${key}: ${bad}.`);
|
|
2923
|
+
}
|
|
2924
|
+
checkCollisions([MARKER_NAME, ...keys]);
|
|
2925
|
+
checkFileAndFolder(/* @__PURE__ */ new Set([MARKER_NAME, ...keys]));
|
|
2926
|
+
const input = {
|
|
2927
|
+
[MARKER_NAME]: [new TextEncoder().encode(WORLD_FILE_MEDIA_TYPE), { level: 0, mtime: STAMP }]
|
|
2928
|
+
};
|
|
2929
|
+
for (const f of [...kept].sort((a, b) => byCodePoint(a.path, b.path))) {
|
|
2639
2930
|
let data = f.data;
|
|
2640
|
-
if (f.path === WORLD_FILE)
|
|
2641
|
-
|
|
2642
|
-
sawWorld = true;
|
|
2643
|
-
}
|
|
2644
|
-
input[key] = [data, { level: /\.json$/i.test(f.path) ? 6 : 0 }];
|
|
2931
|
+
if (f.path === WORLD_FILE) data = withoutLink(data, parseWorld(data)).data;
|
|
2932
|
+
input[`${topDir}/${f.path}`] = [data, { level: /\.json$/i.test(f.path) ? 6 : 0, mtime: STAMP }];
|
|
2645
2933
|
}
|
|
2646
|
-
if (!sawWorld) throw new OwWorldFileError("not_a_world");
|
|
2647
2934
|
return zipSync(input);
|
|
2648
2935
|
}
|
|
2649
2936
|
|
|
@@ -3885,6 +4172,7 @@ export {
|
|
|
3885
4172
|
TokenResource,
|
|
3886
4173
|
WORLD_FILE_MAX_BYTES,
|
|
3887
4174
|
WORLD_FILE_MAX_ENTRIES,
|
|
4175
|
+
WORLD_FILE_MEDIA_TYPE,
|
|
3888
4176
|
absYearForEraYear,
|
|
3889
4177
|
absoluteYear,
|
|
3890
4178
|
approxYearLength,
|
|
@@ -3928,6 +4216,7 @@ export {
|
|
|
3928
4216
|
renderElement,
|
|
3929
4217
|
resolveCalendarLens,
|
|
3930
4218
|
resolveEraDomains,
|
|
4219
|
+
simpleCaseFold,
|
|
3931
4220
|
validateCalendarConfig,
|
|
3932
4221
|
weekdayForStep,
|
|
3933
4222
|
worldCalendars,
|