@world-engines/project-format 0.1.0-alpha.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/LICENSE +46 -0
- package/dist/account-project-root.d.ts +5 -0
- package/dist/account-project-root.js +12 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +8 -0
- package/dist/file-tree.d.ts +5 -0
- package/dist/file-tree.js +55 -0
- package/dist/gallery-migration.d.ts +28 -0
- package/dist/gallery-migration.js +65 -0
- package/dist/gallery.d.ts +16 -0
- package/dist/gallery.js +86 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/integrity.d.ts +6 -0
- package/dist/integrity.js +26 -0
- package/dist/lossless-blob.d.ts +49 -0
- package/dist/lossless-blob.js +270 -0
- package/dist/manifest.d.ts +89 -0
- package/dist/manifest.js +206 -0
- package/dist/neutral-upload.d.ts +48 -0
- package/dist/neutral-upload.js +102 -0
- package/dist/scenario-part.d.ts +8 -0
- package/dist/scenario-part.js +193 -0
- package/dist/tar.d.ts +7 -0
- package/dist/tar.js +169 -0
- package/dist/wesp.d.ts +7 -0
- package/dist/wesp.js +57 -0
- package/dist/windows-executable.d.ts +4 -0
- package/dist/windows-executable.js +68 -0
- package/package.json +51 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export const NEUTRAL_UPLOAD_RAW_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
2
|
+
export const NEUTRAL_UPLOAD_MAX_ENCODED_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
3
|
+
export const NEUTRAL_UPLOAD_MAX_OBJECT_BYTES = 512 * 1024 * 1024;
|
|
4
|
+
export const NEUTRAL_UPLOAD_MAX_CHUNKS = 128;
|
|
5
|
+
export const NEUTRAL_UPLOAD_MAX_OBJECTS = 64;
|
|
6
|
+
export const NEUTRAL_UPLOAD_MAX_PLAN_BYTES = 1024 * 1024 * 1024;
|
|
7
|
+
/** 每个 raw chunk 独立压缩,禁止跨 chunk gzip 状态。descriptor 仅在全部 writer 成功后返回。 */
|
|
8
|
+
export async function encodeNeutralUploadLogicalObject(input, writer, options) {
|
|
9
|
+
assertDescriptor(input);
|
|
10
|
+
const codec = options.codec ?? "gzip-member-v1";
|
|
11
|
+
if (codec !== "identity-v1" && codec !== "gzip-member-v1")
|
|
12
|
+
throw new TypeError("neutral upload codec 无效");
|
|
13
|
+
const objectHash = options.create_sha256();
|
|
14
|
+
const refs = [];
|
|
15
|
+
let rawLength = 0;
|
|
16
|
+
let pending = new Uint8Array(NEUTRAL_UPLOAD_RAW_CHUNK_BYTES);
|
|
17
|
+
let pendingLength = 0;
|
|
18
|
+
const flush = async () => {
|
|
19
|
+
if (pendingLength === 0)
|
|
20
|
+
return;
|
|
21
|
+
if (refs.length >= NEUTRAL_UPLOAD_MAX_CHUNKS)
|
|
22
|
+
throw new RangeError("neutral upload chunk 数超过 128");
|
|
23
|
+
const raw = pending.slice(0, pendingLength);
|
|
24
|
+
const encoded = codec === "identity-v1" ? raw : await gzipMember(raw);
|
|
25
|
+
if (encoded.byteLength > NEUTRAL_UPLOAD_MAX_ENCODED_CHUNK_BYTES)
|
|
26
|
+
throw new RangeError("neutral upload encoded chunk 超过 8 MiB");
|
|
27
|
+
const ref = Object.freeze({
|
|
28
|
+
index: refs.length,
|
|
29
|
+
raw_offset: rawLength - raw.byteLength,
|
|
30
|
+
raw_byte_length: raw.byteLength,
|
|
31
|
+
raw_sha256: options.create_sha256().update(raw).digestHex(),
|
|
32
|
+
codec,
|
|
33
|
+
encoded_byte_length: encoded.byteLength,
|
|
34
|
+
encoded_sha256: options.create_sha256().update(encoded).digestHex(),
|
|
35
|
+
});
|
|
36
|
+
await writer({ object_id: input.object_id, ref, bytes: encoded });
|
|
37
|
+
refs.push(ref);
|
|
38
|
+
pending = new Uint8Array(NEUTRAL_UPLOAD_RAW_CHUNK_BYTES);
|
|
39
|
+
pendingLength = 0;
|
|
40
|
+
};
|
|
41
|
+
for await (const value of input.bytes) {
|
|
42
|
+
if (!(value instanceof Uint8Array))
|
|
43
|
+
throw new TypeError("neutral upload source 只能产生 Uint8Array");
|
|
44
|
+
if (rawLength + value.byteLength > NEUTRAL_UPLOAD_MAX_OBJECT_BYTES)
|
|
45
|
+
throw new RangeError("neutral upload object 超过 512 MiB");
|
|
46
|
+
objectHash.update(value);
|
|
47
|
+
let offset = 0;
|
|
48
|
+
while (offset < value.byteLength) {
|
|
49
|
+
const copied = Math.min(pending.byteLength - pendingLength, value.byteLength - offset);
|
|
50
|
+
pending.set(value.subarray(offset, offset + copied), pendingLength);
|
|
51
|
+
pendingLength += copied;
|
|
52
|
+
offset += copied;
|
|
53
|
+
rawLength += copied;
|
|
54
|
+
if (pendingLength === pending.byteLength)
|
|
55
|
+
await flush();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
await flush();
|
|
59
|
+
if (rawLength === 0)
|
|
60
|
+
throw new RangeError("neutral upload 不允许空 logical object");
|
|
61
|
+
return Object.freeze({
|
|
62
|
+
object_id: input.object_id,
|
|
63
|
+
object_kind: input.object_kind,
|
|
64
|
+
...(input.section_key === undefined ? {} : { section_key: input.section_key }),
|
|
65
|
+
...(input.media_type === undefined ? {} : { media_type: input.media_type }),
|
|
66
|
+
...(input.canonical_encoding === undefined ? {} : { canonical_encoding: input.canonical_encoding }),
|
|
67
|
+
raw_byte_length: rawLength,
|
|
68
|
+
raw_sha256: objectHash.digestHex(),
|
|
69
|
+
chunks: Object.freeze(refs),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export function assertNeutralUploadPlanLimits(objects) {
|
|
73
|
+
if (objects.length === 0 || objects.length > NEUTRAL_UPLOAD_MAX_OBJECTS)
|
|
74
|
+
throw new RangeError("neutral upload plan object 数无效");
|
|
75
|
+
if (new Set(objects.map((object) => object.object_id)).size !== objects.length)
|
|
76
|
+
throw new TypeError("neutral upload object_id 重复");
|
|
77
|
+
let raw = 0;
|
|
78
|
+
let encoded = 0;
|
|
79
|
+
for (const object of objects) {
|
|
80
|
+
raw += object.raw_byte_length;
|
|
81
|
+
encoded += object.chunks.reduce((sum, chunk) => sum + chunk.encoded_byte_length, 0);
|
|
82
|
+
if (!Number.isSafeInteger(raw) || !Number.isSafeInteger(encoded) || raw > NEUTRAL_UPLOAD_MAX_PLAN_BYTES || encoded > NEUTRAL_UPLOAD_MAX_PLAN_BYTES) {
|
|
83
|
+
throw new RangeError("neutral upload plan aggregate 超过 1 GiB");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function assertDescriptor(input) {
|
|
88
|
+
for (const [field, value] of [["object_id", input.object_id], ["object_kind", input.object_kind]]) {
|
|
89
|
+
if (value.length === 0 || value !== value.trim().normalize("NFC"))
|
|
90
|
+
throw new TypeError(`${field} 无效`);
|
|
91
|
+
}
|
|
92
|
+
if (!["manifest", "revision_index", "section", "local_project_wesp", "view"].includes(input.object_kind))
|
|
93
|
+
throw new TypeError("object_kind 无效");
|
|
94
|
+
const isSection = input.object_kind === "section" || input.object_kind === "local_project_wesp" || input.object_kind === "view";
|
|
95
|
+
if (isSection !== (input.section_key !== undefined && input.media_type !== undefined && input.canonical_encoding !== undefined)) {
|
|
96
|
+
throw new TypeError("section descriptor 不闭合");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function gzipMember(raw) {
|
|
100
|
+
const stream = new Blob([raw]).stream().pipeThrough(new CompressionStream("gzip"));
|
|
101
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
102
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* protobuf wire: field 1 (schema_version), varint 1。
|
|
3
|
+
* 所有 author sections 使用 protobuf 默认空值;initializer 不制造第二套 JSON authority。
|
|
4
|
+
*/
|
|
5
|
+
export declare const EMPTY_SCENARIO_PART_SOURCE_V1_PROTOBUF: Uint8Array;
|
|
6
|
+
export declare function createEmptyScenarioPartWesp(subtle?: SubtleCrypto): Promise<Uint8Array>;
|
|
7
|
+
export declare function validateScenarioPartSourceV1Protobuf(payload: Uint8Array): Uint8Array;
|
|
8
|
+
export declare function decodeScenarioPartWesp(encoded: Uint8Array, subtle?: SubtleCrypto): Promise<Uint8Array>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { ProjectFormatError } from "./errors.js";
|
|
2
|
+
import { decodeWesp, encodeWesp } from "./wesp.js";
|
|
3
|
+
/**
|
|
4
|
+
* protobuf wire: field 1 (schema_version), varint 1。
|
|
5
|
+
* 所有 author sections 使用 protobuf 默认空值;initializer 不制造第二套 JSON authority。
|
|
6
|
+
*/
|
|
7
|
+
export const EMPTY_SCENARIO_PART_SOURCE_V1_PROTOBUF = new Uint8Array([0x08, 0x01]);
|
|
8
|
+
export function createEmptyScenarioPartWesp(subtle) {
|
|
9
|
+
return encodeWesp(EMPTY_SCENARIO_PART_SOURCE_V1_PROTOBUF, subtle);
|
|
10
|
+
}
|
|
11
|
+
function protobufInvalid(message) {
|
|
12
|
+
throw new ProjectFormatError("E_SCENARIO_PROTOBUF_INVALID", message);
|
|
13
|
+
}
|
|
14
|
+
function readVarint(bytes, offset) {
|
|
15
|
+
let value = 0n;
|
|
16
|
+
for (let index = 0; index < 10; index += 1) {
|
|
17
|
+
const byte = bytes[offset + index];
|
|
18
|
+
if (byte === undefined)
|
|
19
|
+
protobufInvalid("protobuf varint 被截断");
|
|
20
|
+
if (index === 9 && byte > 1)
|
|
21
|
+
protobufInvalid("protobuf varint 超过 uint64");
|
|
22
|
+
value |= BigInt(byte & 0x7f) << BigInt(index * 7);
|
|
23
|
+
if ((byte & 0x80) === 0)
|
|
24
|
+
return { value, next_offset: offset + index + 1 };
|
|
25
|
+
}
|
|
26
|
+
return protobufInvalid("protobuf varint 超过 10 bytes");
|
|
27
|
+
}
|
|
28
|
+
function readLengthDelimited(bytes, offset) {
|
|
29
|
+
const length = readVarint(bytes, offset);
|
|
30
|
+
if (length.value > BigInt(Number.MAX_SAFE_INTEGER))
|
|
31
|
+
protobufInvalid("protobuf length 超过安全整数");
|
|
32
|
+
const end = length.next_offset + Number(length.value);
|
|
33
|
+
if (end > bytes.byteLength)
|
|
34
|
+
protobufInvalid("protobuf length-delimited field 被截断");
|
|
35
|
+
return { payload: bytes.slice(length.next_offset, end), next_offset: end };
|
|
36
|
+
}
|
|
37
|
+
function skipUnknownField(bytes, wireType, offset) {
|
|
38
|
+
if (wireType === 0)
|
|
39
|
+
return readVarint(bytes, offset).next_offset;
|
|
40
|
+
if (wireType === 1) {
|
|
41
|
+
if (offset + 8 > bytes.byteLength)
|
|
42
|
+
protobufInvalid("protobuf fixed64 被截断");
|
|
43
|
+
return offset + 8;
|
|
44
|
+
}
|
|
45
|
+
if (wireType === 2)
|
|
46
|
+
return readLengthDelimited(bytes, offset).next_offset;
|
|
47
|
+
if (wireType === 5) {
|
|
48
|
+
if (offset + 4 > bytes.byteLength)
|
|
49
|
+
protobufInvalid("protobuf fixed32 被截断");
|
|
50
|
+
return offset + 4;
|
|
51
|
+
}
|
|
52
|
+
return protobufInvalid(`不支持 protobuf wire_type ${wireType}`);
|
|
53
|
+
}
|
|
54
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
55
|
+
function validateUtf8(payload, label) {
|
|
56
|
+
try {
|
|
57
|
+
UTF8_DECODER.decode(payload);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
protobufInvalid(`${label} 不是合法 UTF-8`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function singularFields(messageName) {
|
|
64
|
+
if (messageName === "ScenarioPartSourceV1")
|
|
65
|
+
return new Set([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
66
|
+
if (messageName === "SpatialWorldSourceV1")
|
|
67
|
+
return new Set([1, 2, 3, 4]);
|
|
68
|
+
if (messageName === "SpatialTopologyV1")
|
|
69
|
+
return new Set([1, 2]);
|
|
70
|
+
if (messageName === "SpatialObjectV1")
|
|
71
|
+
return new Set([1, 2, 3, 4]);
|
|
72
|
+
return new Set([1, 2]);
|
|
73
|
+
}
|
|
74
|
+
function maxKnownField(messageName) {
|
|
75
|
+
if (messageName === "ScenarioPartSourceV1")
|
|
76
|
+
return 8;
|
|
77
|
+
if (messageName === "SpatialWorldSourceV1")
|
|
78
|
+
return 13;
|
|
79
|
+
if (messageName === "SpatialTopologyV1")
|
|
80
|
+
return 2;
|
|
81
|
+
if (messageName === "SpatialObjectV1")
|
|
82
|
+
return 5;
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
function readKnownLengthDelimited(bytes, offset, wireType, label) {
|
|
86
|
+
if (wireType !== 2)
|
|
87
|
+
protobufInvalid(`${label} wire type 错误`);
|
|
88
|
+
return readLengthDelimited(bytes, offset);
|
|
89
|
+
}
|
|
90
|
+
function validateMessage(bytes, messageName) {
|
|
91
|
+
let offset = 0;
|
|
92
|
+
let schemaVersion;
|
|
93
|
+
const seenSingularFields = new Set();
|
|
94
|
+
const singular = singularFields(messageName);
|
|
95
|
+
while (offset < bytes.byteLength) {
|
|
96
|
+
const tag = readVarint(bytes, offset);
|
|
97
|
+
offset = tag.next_offset;
|
|
98
|
+
const fieldNumberValue = tag.value >> 3n;
|
|
99
|
+
if (fieldNumberValue > 0x1fffffffn)
|
|
100
|
+
protobufInvalid(`${messageName} field_number 超过 protobuf 上限`);
|
|
101
|
+
const fieldNumber = Number(fieldNumberValue);
|
|
102
|
+
const wireType = Number(tag.value & 0x07n);
|
|
103
|
+
if (fieldNumber === 0)
|
|
104
|
+
protobufInvalid(`${messageName} 包含 field_number 0`);
|
|
105
|
+
const known = fieldNumber <= maxKnownField(messageName);
|
|
106
|
+
if (known && singular.has(fieldNumber)) {
|
|
107
|
+
if (seenSingularFields.has(fieldNumber))
|
|
108
|
+
protobufInvalid(`${messageName} field ${fieldNumber} 重复`);
|
|
109
|
+
seenSingularFields.add(fieldNumber);
|
|
110
|
+
}
|
|
111
|
+
if (fieldNumber === 1
|
|
112
|
+
&& (messageName === "ScenarioPartSourceV1" || messageName === "SpatialWorldSourceV1")) {
|
|
113
|
+
if (wireType !== 0)
|
|
114
|
+
protobufInvalid(`${messageName}.schema_version wire type 错误`);
|
|
115
|
+
const version = readVarint(bytes, offset);
|
|
116
|
+
if (version.value > 0xffffffffn)
|
|
117
|
+
protobufInvalid(`${messageName}.schema_version 超过 uint32`);
|
|
118
|
+
schemaVersion = version.value;
|
|
119
|
+
offset = version.next_offset;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (messageName === "ScenarioPartSourceV1" && fieldNumber >= 2 && fieldNumber <= 8) {
|
|
123
|
+
const field = readKnownLengthDelimited(bytes, offset, wireType, `${messageName} field ${fieldNumber}`);
|
|
124
|
+
if (fieldNumber === 8)
|
|
125
|
+
validateMessage(field.payload, "SpatialWorldSourceV1");
|
|
126
|
+
offset = field.next_offset;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (messageName === "SpatialWorldSourceV1" && fieldNumber >= 2 && fieldNumber <= 13) {
|
|
130
|
+
const field = readKnownLengthDelimited(bytes, offset, wireType, `${messageName} field ${fieldNumber}`);
|
|
131
|
+
if (fieldNumber === 2 || fieldNumber === 13) {
|
|
132
|
+
validateUtf8(field.payload, `${messageName} field ${fieldNumber}`);
|
|
133
|
+
}
|
|
134
|
+
else if (fieldNumber === 3) {
|
|
135
|
+
validateMessage(field.payload, "SpatialTopologyV1");
|
|
136
|
+
}
|
|
137
|
+
else if (fieldNumber >= 7 && fieldNumber <= 11) {
|
|
138
|
+
validateMessage(field.payload, "SpatialObjectV1");
|
|
139
|
+
}
|
|
140
|
+
else if (fieldNumber === 12) {
|
|
141
|
+
validateMessage(field.payload, "SpatialPresetV1");
|
|
142
|
+
}
|
|
143
|
+
offset = field.next_offset;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (messageName === "SpatialTopologyV1" && fieldNumber >= 1 && fieldNumber <= 2) {
|
|
147
|
+
if (fieldNumber === 1) {
|
|
148
|
+
if (wireType !== 0)
|
|
149
|
+
protobufInvalid(`${messageName}.kind wire type 错误`);
|
|
150
|
+
const kind = readVarint(bytes, offset);
|
|
151
|
+
if (kind.value > 3n)
|
|
152
|
+
protobufInvalid(`${messageName}.kind 不受支持`);
|
|
153
|
+
offset = kind.next_offset;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
offset = readKnownLengthDelimited(bytes, offset, wireType, `${messageName}.settings`).next_offset;
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (messageName === "SpatialObjectV1" && fieldNumber >= 1 && fieldNumber <= 5) {
|
|
161
|
+
const field = readKnownLengthDelimited(bytes, offset, wireType, `${messageName} field ${fieldNumber}`);
|
|
162
|
+
if (fieldNumber <= 3 || fieldNumber === 5) {
|
|
163
|
+
validateUtf8(field.payload, `${messageName} field ${fieldNumber}`);
|
|
164
|
+
}
|
|
165
|
+
offset = field.next_offset;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (messageName === "SpatialPresetV1" && fieldNumber >= 1 && fieldNumber <= 2) {
|
|
169
|
+
const field = readKnownLengthDelimited(bytes, offset, wireType, `${messageName} field ${fieldNumber}`);
|
|
170
|
+
if (fieldNumber === 1)
|
|
171
|
+
validateUtf8(field.payload, `${messageName}.preset_id`);
|
|
172
|
+
offset = field.next_offset;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
offset = skipUnknownField(bytes, wireType, offset);
|
|
176
|
+
}
|
|
177
|
+
if (messageName === "ScenarioPartSourceV1" || messageName === "SpatialWorldSourceV1") {
|
|
178
|
+
if (schemaVersion === undefined)
|
|
179
|
+
protobufInvalid(`${messageName}.schema_version 缺失`);
|
|
180
|
+
if (schemaVersion === 1n)
|
|
181
|
+
return;
|
|
182
|
+
throw new ProjectFormatError("E_SCENARIO_SCHEMA_VERSION_UNSUPPORTED", `${messageName}.schema_version=${schemaVersion.toString()} 不受支持`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export function validateScenarioPartSourceV1Protobuf(payload) {
|
|
186
|
+
if (!(payload instanceof Uint8Array))
|
|
187
|
+
throw new TypeError("ScenarioPart protobuf 必须是 Uint8Array");
|
|
188
|
+
validateMessage(payload, "ScenarioPartSourceV1");
|
|
189
|
+
return payload;
|
|
190
|
+
}
|
|
191
|
+
export async function decodeScenarioPartWesp(encoded, subtle) {
|
|
192
|
+
return validateScenarioPartSourceV1Protobuf(await decodeWesp(encoded, subtle));
|
|
193
|
+
}
|
package/dist/tar.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Canonical ustar file archive used by Creator and sandbox Node build workers. */
|
|
2
|
+
export interface TarEntry {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly bytes: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
export declare const packTar: (entries: readonly TarEntry[]) => Uint8Array;
|
|
7
|
+
export declare const unpackTar: (buf: Uint8Array) => readonly TarEntry[];
|
package/dist/tar.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const BLOCK_SIZE = 512;
|
|
2
|
+
const NAME_MAX = 100;
|
|
3
|
+
const PREFIX_MAX = 155;
|
|
4
|
+
const USTAR_MAGIC = "ustar\0";
|
|
5
|
+
const USTAR_VERSION = "00";
|
|
6
|
+
const DEFAULT_MODE = "0000644";
|
|
7
|
+
const TYPEFLAG_FILE = "0";
|
|
8
|
+
const hasControlCharacter = (value) => [...value].some((character) => {
|
|
9
|
+
const code = character.charCodeAt(0);
|
|
10
|
+
return code < 0x20 || code === 0x7f;
|
|
11
|
+
});
|
|
12
|
+
const assertSafePath = (path) => {
|
|
13
|
+
if (path.length === 0 || path !== path.normalize("NFC") || path.startsWith("/")
|
|
14
|
+
|| path.includes("\\") || path.includes("\0") || /^[A-Za-z]:/u.test(path)) {
|
|
15
|
+
throw new Error("tar_path_invalid");
|
|
16
|
+
}
|
|
17
|
+
for (const part of path.split("/")) {
|
|
18
|
+
if (part.length === 0 || part === "." || part === ".." || hasControlCharacter(part)) {
|
|
19
|
+
throw new Error("tar_path_invalid");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const padOctal = (value, width) => {
|
|
24
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
25
|
+
throw new Error("octal_overflow");
|
|
26
|
+
const octal = value.toString(8);
|
|
27
|
+
if (octal.length > width)
|
|
28
|
+
throw new Error("octal_overflow");
|
|
29
|
+
return octal.padStart(width, "0");
|
|
30
|
+
};
|
|
31
|
+
const writeString = (buf, offset, value, width) => {
|
|
32
|
+
const encoded = new TextEncoder().encode(value);
|
|
33
|
+
if (encoded.length > width)
|
|
34
|
+
throw new Error("field_too_long");
|
|
35
|
+
buf.set(encoded, offset);
|
|
36
|
+
};
|
|
37
|
+
const readString = (buf, offset, width) => {
|
|
38
|
+
let end = offset;
|
|
39
|
+
const limit = offset + width;
|
|
40
|
+
while (end < limit && buf[end] !== 0)
|
|
41
|
+
end += 1;
|
|
42
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buf.subarray(offset, end));
|
|
43
|
+
};
|
|
44
|
+
const readOctal = (buf, offset, width) => {
|
|
45
|
+
const raw = readString(buf, offset, width).trim();
|
|
46
|
+
if (raw.length === 0)
|
|
47
|
+
return 0;
|
|
48
|
+
if (!/^[0-7]+$/u.test(raw))
|
|
49
|
+
throw new Error("invalid_octal");
|
|
50
|
+
const parsed = Number.parseInt(raw, 8);
|
|
51
|
+
if (!Number.isSafeInteger(parsed))
|
|
52
|
+
throw new Error("invalid_octal");
|
|
53
|
+
return parsed;
|
|
54
|
+
};
|
|
55
|
+
const splitName = (path) => {
|
|
56
|
+
const encoder = new TextEncoder();
|
|
57
|
+
if (encoder.encode(path).length <= NAME_MAX)
|
|
58
|
+
return { name: path, prefix: "" };
|
|
59
|
+
for (let index = path.length - 1; index > 0; index -= 1) {
|
|
60
|
+
if (path.charCodeAt(index) !== 0x2f)
|
|
61
|
+
continue;
|
|
62
|
+
const prefix = path.slice(0, index);
|
|
63
|
+
const name = path.slice(index + 1);
|
|
64
|
+
if (encoder.encode(prefix).length <= PREFIX_MAX && encoder.encode(name).length <= NAME_MAX && name.length > 0) {
|
|
65
|
+
return { name, prefix };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw new Error("path_too_long");
|
|
69
|
+
};
|
|
70
|
+
const computeChecksum = (header) => {
|
|
71
|
+
let sum = 0;
|
|
72
|
+
for (let index = 0; index < BLOCK_SIZE; index += 1)
|
|
73
|
+
sum += header[index] ?? 0;
|
|
74
|
+
return sum;
|
|
75
|
+
};
|
|
76
|
+
const buildHeader = (entry) => {
|
|
77
|
+
assertSafePath(entry.path);
|
|
78
|
+
if (!(entry.bytes instanceof Uint8Array))
|
|
79
|
+
throw new Error("tar_entry_bytes_invalid");
|
|
80
|
+
const header = new Uint8Array(BLOCK_SIZE);
|
|
81
|
+
const { name, prefix } = splitName(entry.path);
|
|
82
|
+
writeString(header, 0, name, NAME_MAX);
|
|
83
|
+
writeString(header, 100, `${DEFAULT_MODE}\0`, 8);
|
|
84
|
+
writeString(header, 108, `${padOctal(0, 7)}\0`, 8);
|
|
85
|
+
writeString(header, 116, `${padOctal(0, 7)}\0`, 8);
|
|
86
|
+
writeString(header, 124, `${padOctal(entry.bytes.length, 11)}\0`, 12);
|
|
87
|
+
writeString(header, 136, `${padOctal(0, 11)}\0`, 12);
|
|
88
|
+
header.fill(0x20, 148, 156);
|
|
89
|
+
writeString(header, 156, TYPEFLAG_FILE, 1);
|
|
90
|
+
writeString(header, 257, USTAR_MAGIC, 6);
|
|
91
|
+
writeString(header, 263, USTAR_VERSION, 2);
|
|
92
|
+
if (prefix.length > 0)
|
|
93
|
+
writeString(header, 345, prefix, PREFIX_MAX);
|
|
94
|
+
writeString(header, 148, `${computeChecksum(header).toString(8).padStart(6, "0")}\0 `, 8);
|
|
95
|
+
return header;
|
|
96
|
+
};
|
|
97
|
+
const isZeroBlock = (buf, offset) => {
|
|
98
|
+
for (let index = 0; index < BLOCK_SIZE; index += 1)
|
|
99
|
+
if (buf[offset + index] !== 0)
|
|
100
|
+
return false;
|
|
101
|
+
return true;
|
|
102
|
+
};
|
|
103
|
+
export const packTar = (entries) => {
|
|
104
|
+
const paths = new Set();
|
|
105
|
+
const parts = [];
|
|
106
|
+
let total = BLOCK_SIZE * 2;
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
assertSafePath(entry.path);
|
|
109
|
+
if (paths.has(entry.path))
|
|
110
|
+
throw new Error("tar_path_duplicate");
|
|
111
|
+
paths.add(entry.path);
|
|
112
|
+
const header = buildHeader(entry);
|
|
113
|
+
const padding = new Uint8Array((BLOCK_SIZE - (entry.bytes.length % BLOCK_SIZE)) % BLOCK_SIZE);
|
|
114
|
+
total += header.length + entry.bytes.length + padding.length;
|
|
115
|
+
if (!Number.isSafeInteger(total))
|
|
116
|
+
throw new Error("tar_size_overflow");
|
|
117
|
+
parts.push(header, entry.bytes.slice(), padding);
|
|
118
|
+
}
|
|
119
|
+
const result = new Uint8Array(total);
|
|
120
|
+
let cursor = 0;
|
|
121
|
+
for (const part of [...parts, new Uint8Array(BLOCK_SIZE * 2)]) {
|
|
122
|
+
result.set(part, cursor);
|
|
123
|
+
cursor += part.length;
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
};
|
|
127
|
+
export const unpackTar = (buf) => {
|
|
128
|
+
if (!(buf instanceof Uint8Array) || buf.byteLength < BLOCK_SIZE * 2)
|
|
129
|
+
throw new Error("tar_truncated");
|
|
130
|
+
const entries = [];
|
|
131
|
+
const paths = new Set();
|
|
132
|
+
let offset = 0;
|
|
133
|
+
let terminated = false;
|
|
134
|
+
while (offset + BLOCK_SIZE <= buf.length) {
|
|
135
|
+
if (isZeroBlock(buf, offset)) {
|
|
136
|
+
if (offset + BLOCK_SIZE * 2 > buf.length || !isZeroBlock(buf, offset + BLOCK_SIZE))
|
|
137
|
+
throw new Error("tar_trailer_invalid");
|
|
138
|
+
terminated = true;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
const header = buf.subarray(offset, offset + BLOCK_SIZE);
|
|
142
|
+
const headerCopy = header.slice();
|
|
143
|
+
const storedChecksum = readOctal(headerCopy, 148, 8);
|
|
144
|
+
headerCopy.fill(0x20, 148, 156);
|
|
145
|
+
if (storedChecksum !== computeChecksum(headerCopy))
|
|
146
|
+
throw new Error("tar_checksum_invalid");
|
|
147
|
+
const name = readString(header, 0, NAME_MAX);
|
|
148
|
+
const prefix = readString(header, 345, PREFIX_MAX);
|
|
149
|
+
const path = prefix.length > 0 ? `${prefix}/${name}` : name;
|
|
150
|
+
assertSafePath(path);
|
|
151
|
+
if (paths.has(path))
|
|
152
|
+
throw new Error("tar_path_duplicate");
|
|
153
|
+
const size = readOctal(header, 124, 12);
|
|
154
|
+
const typeflag = readString(header, 156, 1) || TYPEFLAG_FILE;
|
|
155
|
+
if (typeflag !== TYPEFLAG_FILE)
|
|
156
|
+
throw new Error("tar_type_unsupported");
|
|
157
|
+
const dataStart = offset + BLOCK_SIZE;
|
|
158
|
+
const padding = (BLOCK_SIZE - (size % BLOCK_SIZE)) % BLOCK_SIZE;
|
|
159
|
+
const next = dataStart + size + padding;
|
|
160
|
+
if (!Number.isSafeInteger(next) || next > buf.length)
|
|
161
|
+
throw new Error("tar_truncated");
|
|
162
|
+
paths.add(path);
|
|
163
|
+
entries.push(Object.freeze({ path, bytes: buf.slice(dataStart, dataStart + size) }));
|
|
164
|
+
offset = next;
|
|
165
|
+
}
|
|
166
|
+
if (!terminated)
|
|
167
|
+
throw new Error("tar_trailer_missing");
|
|
168
|
+
return Object.freeze(entries);
|
|
169
|
+
};
|
package/dist/wesp.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function encodeWesp(protobufPayload: Uint8Array, subtle?: SubtleCrypto): Promise<Uint8Array>;
|
|
2
|
+
export declare function decodeWesp(encoded: Uint8Array, subtle?: SubtleCrypto): Promise<Uint8Array>;
|
|
3
|
+
export declare const WESP_V1: Readonly<{
|
|
4
|
+
magic: "WESP";
|
|
5
|
+
format_version: 1;
|
|
6
|
+
header_size: number;
|
|
7
|
+
}>;
|
package/dist/wesp.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ProjectFormatError } from "./errors.js";
|
|
2
|
+
import { sha256Bytes } from "./integrity.js";
|
|
3
|
+
const WESP_MAGIC = new Uint8Array([0x57, 0x45, 0x53, 0x50]);
|
|
4
|
+
const WESP_FORMAT_VERSION = 1;
|
|
5
|
+
const WESP_HEADER_SIZE = WESP_MAGIC.byteLength + 1 + 8 + 32;
|
|
6
|
+
function hasMagic(bytes) {
|
|
7
|
+
return WESP_MAGIC.every((value, index) => bytes[index] === value);
|
|
8
|
+
}
|
|
9
|
+
function equalBytes(left, right) {
|
|
10
|
+
if (left.byteLength !== right.byteLength)
|
|
11
|
+
return false;
|
|
12
|
+
let difference = 0;
|
|
13
|
+
for (let index = 0; index < left.byteLength; index += 1) {
|
|
14
|
+
difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
|
|
15
|
+
}
|
|
16
|
+
return difference === 0;
|
|
17
|
+
}
|
|
18
|
+
export async function encodeWesp(protobufPayload, subtle) {
|
|
19
|
+
if (!(protobufPayload instanceof Uint8Array)) {
|
|
20
|
+
throw new TypeError("protobufPayload 必须是 Uint8Array");
|
|
21
|
+
}
|
|
22
|
+
const encoded = new Uint8Array(WESP_HEADER_SIZE + protobufPayload.byteLength);
|
|
23
|
+
encoded.set(WESP_MAGIC, 0);
|
|
24
|
+
encoded[4] = WESP_FORMAT_VERSION;
|
|
25
|
+
new DataView(encoded.buffer, encoded.byteOffset + 5, 8).setBigUint64(0, BigInt(protobufPayload.byteLength));
|
|
26
|
+
encoded.set(await sha256Bytes(protobufPayload, subtle), 13);
|
|
27
|
+
encoded.set(protobufPayload, WESP_HEADER_SIZE);
|
|
28
|
+
return encoded;
|
|
29
|
+
}
|
|
30
|
+
export async function decodeWesp(encoded, subtle) {
|
|
31
|
+
if (!(encoded instanceof Uint8Array) || encoded.byteLength < WESP_HEADER_SIZE) {
|
|
32
|
+
throw new ProjectFormatError("E_WESP_SIZE_MISMATCH", "WESP header 不完整");
|
|
33
|
+
}
|
|
34
|
+
if (!hasMagic(encoded)) {
|
|
35
|
+
throw new ProjectFormatError("E_WESP_MAGIC_INVALID", "WESP magic 不匹配");
|
|
36
|
+
}
|
|
37
|
+
if (encoded[4] !== WESP_FORMAT_VERSION) {
|
|
38
|
+
throw new ProjectFormatError("E_WESP_VERSION_UNSUPPORTED", `不支持 WESP format_version ${String(encoded[4])}`);
|
|
39
|
+
}
|
|
40
|
+
const declaredSize = new DataView(encoded.buffer, encoded.byteOffset + 5, 8).getBigUint64(0);
|
|
41
|
+
const actualSize = BigInt(encoded.byteLength - WESP_HEADER_SIZE);
|
|
42
|
+
if (declaredSize !== actualSize) {
|
|
43
|
+
throw new ProjectFormatError("E_WESP_SIZE_MISMATCH", `WESP payload 长度不匹配:声明 ${declaredSize.toString()},实际 ${actualSize.toString()}`);
|
|
44
|
+
}
|
|
45
|
+
const expectedDigest = encoded.slice(13, WESP_HEADER_SIZE);
|
|
46
|
+
const payload = encoded.slice(WESP_HEADER_SIZE);
|
|
47
|
+
const actualDigest = await sha256Bytes(payload, subtle);
|
|
48
|
+
if (!equalBytes(expectedDigest, actualDigest)) {
|
|
49
|
+
throw new ProjectFormatError("E_WESP_DIGEST_MISMATCH", "WESP payload SHA-256 不匹配");
|
|
50
|
+
}
|
|
51
|
+
return payload;
|
|
52
|
+
}
|
|
53
|
+
export const WESP_V1 = Object.freeze({
|
|
54
|
+
magic: "WESP",
|
|
55
|
+
format_version: WESP_FORMAT_VERSION,
|
|
56
|
+
header_size: WESP_HEADER_SIZE,
|
|
57
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const IMAGE_FILE_MACHINE_AMD64 = 0x8664;
|
|
2
|
+
const IMAGE_NT_OPTIONAL_HDR64_MAGIC = 0x20b;
|
|
3
|
+
const IMAGE_SECTION_HEADER_SIZE = 40;
|
|
4
|
+
const MAX_SECTION_COUNT = 96;
|
|
5
|
+
export class WindowsExecutableValidationError extends Error {
|
|
6
|
+
}
|
|
7
|
+
function fail(message) {
|
|
8
|
+
throw new WindowsExecutableValidationError(message);
|
|
9
|
+
}
|
|
10
|
+
function requireRange(bytes, offset, length, label) {
|
|
11
|
+
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0 || offset + length > bytes.byteLength) {
|
|
12
|
+
fail(`${label} 越过 PE 文件边界`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function asciiEquals(bytes, offset, expected) {
|
|
16
|
+
return expected.every((value, index) => bytes[offset + index] === value);
|
|
17
|
+
}
|
|
18
|
+
/** 验证 Windows x64 PE32+ 的结构边界;不依赖 Node Buffer,可由 initializer 与构建脚本共同消费。 */
|
|
19
|
+
export function assertWindowsX64Executable(bytes) {
|
|
20
|
+
requireRange(bytes, 0, 0x40, "DOS header");
|
|
21
|
+
if (!asciiEquals(bytes, 0, [0x4d, 0x5a]))
|
|
22
|
+
fail("缺少 DOS MZ header");
|
|
23
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
24
|
+
const peOffset = view.getUint32(0x3c, true);
|
|
25
|
+
if (peOffset < 0x40)
|
|
26
|
+
fail("DOS e_lfanew 不合理");
|
|
27
|
+
requireRange(bytes, peOffset, 24, "PE/COFF header");
|
|
28
|
+
if (!asciiEquals(bytes, peOffset, [0x50, 0x45, 0, 0]))
|
|
29
|
+
fail("缺少 PE\\0\\0 signature");
|
|
30
|
+
const coffOffset = peOffset + 4;
|
|
31
|
+
const machine = view.getUint16(coffOffset, true);
|
|
32
|
+
const sectionCount = view.getUint16(coffOffset + 2, true);
|
|
33
|
+
const optionalHeaderSize = view.getUint16(coffOffset + 16, true);
|
|
34
|
+
if (machine !== IMAGE_FILE_MACHINE_AMD64)
|
|
35
|
+
fail(`不是 x64 PE(Machine=0x${machine.toString(16)})`);
|
|
36
|
+
if (sectionCount < 1 || sectionCount > MAX_SECTION_COUNT)
|
|
37
|
+
fail(`section 数量不合理: ${sectionCount}`);
|
|
38
|
+
if (optionalHeaderSize < 112)
|
|
39
|
+
fail(`x64 optional header 过短: ${optionalHeaderSize}`);
|
|
40
|
+
const optionalOffset = coffOffset + 20;
|
|
41
|
+
requireRange(bytes, optionalOffset, optionalHeaderSize, "optional header");
|
|
42
|
+
if (view.getUint16(optionalOffset, true) !== IMAGE_NT_OPTIONAL_HDR64_MAGIC)
|
|
43
|
+
fail("不是 PE32+ x64 optional header");
|
|
44
|
+
const sectionTableOffset = optionalOffset + optionalHeaderSize;
|
|
45
|
+
const sectionTableSize = sectionCount * IMAGE_SECTION_HEADER_SIZE;
|
|
46
|
+
requireRange(bytes, sectionTableOffset, sectionTableSize, "section table");
|
|
47
|
+
const sizeOfImage = view.getUint32(optionalOffset + 56, true);
|
|
48
|
+
const sizeOfHeaders = view.getUint32(optionalOffset + 60, true);
|
|
49
|
+
const numberOfRvaAndSizes = view.getUint32(optionalOffset + 108, true);
|
|
50
|
+
if (sizeOfImage < 0x1000 || sizeOfHeaders < sectionTableOffset + sectionTableSize || sizeOfHeaders > bytes.byteLength)
|
|
51
|
+
fail("optional header image/header 边界不合理");
|
|
52
|
+
if (numberOfRvaAndSizes > 16)
|
|
53
|
+
fail(`data-directory 数量不合理: ${numberOfRvaAndSizes}`);
|
|
54
|
+
let hasFileBackedSection = false;
|
|
55
|
+
for (let index = 0; index < sectionCount; index += 1) {
|
|
56
|
+
const sectionOffset = sectionTableOffset + index * IMAGE_SECTION_HEADER_SIZE;
|
|
57
|
+
const rawSize = view.getUint32(sectionOffset + 16, true);
|
|
58
|
+
const rawOffset = view.getUint32(sectionOffset + 20, true);
|
|
59
|
+
if (rawSize === 0)
|
|
60
|
+
continue;
|
|
61
|
+
if (rawOffset < sizeOfHeaders)
|
|
62
|
+
fail(`section ${index} raw data 与 PE headers 重叠`);
|
|
63
|
+
requireRange(bytes, rawOffset, rawSize, `section ${index} raw data`);
|
|
64
|
+
hasFileBackedSection = true;
|
|
65
|
+
}
|
|
66
|
+
if (!hasFileBackedSection)
|
|
67
|
+
fail("没有 file-backed section");
|
|
68
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@world-engines/project-format",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./tar": {
|
|
12
|
+
"types": "./dist/tar.d.ts",
|
|
13
|
+
"import": "./dist/tar.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"worldengine_source_sha256": "70618ef316bd1a14b6ae3bb2ecbad26814daa3b2c41ad82cba377757d0e42924",
|
|
23
|
+
"worldengine_source_identity_provenance": {
|
|
24
|
+
"schema": "worldengine-public-namespace-projection/v1",
|
|
25
|
+
"kind": "composite-source-identity; namespace projection; not-full-source-recompile",
|
|
26
|
+
"original_name": "@worldengine/project-format",
|
|
27
|
+
"original_archive_sha256": "59259ac05463944bec2021382d98202462c7e245614a7f5a56c23b814ed989ed",
|
|
28
|
+
"original_source_sha256": "d484ac0a7b8122190d770bb42224e0c089ef9e05784744e8a8c529b2016f9c63",
|
|
29
|
+
"projected_name": "@world-engines/project-format",
|
|
30
|
+
"package_name_mapping": {
|
|
31
|
+
"@chat/blocks-editor": "@world-engines/blocks-editor",
|
|
32
|
+
"@chat/ladybug-bridge": "@world-engines/ladybug-bridge",
|
|
33
|
+
"@chat/monaco-host": "@world-engines/monaco-host",
|
|
34
|
+
"@chat/protocol-ts": "@world-engines/protocol-ts",
|
|
35
|
+
"@chat/scenario-author-source": "@world-engines/scenario-author-source",
|
|
36
|
+
"@chat/scenario-review-snapshot": "@world-engines/scenario-review-snapshot",
|
|
37
|
+
"@chat/tmw": "@world-engines/tmw",
|
|
38
|
+
"@chat/view-exposure": "@world-engines/view-exposure",
|
|
39
|
+
"@world-engines/chatplay-vite-plugin": "@world-engines/chatplay-vite-plugin",
|
|
40
|
+
"@worldengine/agent-kit": "@world-engines/agent-kit",
|
|
41
|
+
"@worldengine/authoring-bridge": "@world-engines/authoring-bridge",
|
|
42
|
+
"@worldengine/authoring-ui": "@world-engines/authoring-ui",
|
|
43
|
+
"@worldengine/create-project": "@world-engines/create-project",
|
|
44
|
+
"@worldengine/project-format": "@world-engines/project-format",
|
|
45
|
+
"@worldengine/project-host": "@world-engines/project-host",
|
|
46
|
+
"@worldengine/project-setup": "@world-engines/project-setup",
|
|
47
|
+
"@worldengine/spatial-authoring": "@world-engines/spatial-authoring",
|
|
48
|
+
"@worldengine/view-sdk": "@world-engines/view-sdk"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|