@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
package/LICENSE
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
WorldEngine 官方作者工具许可证
|
|
2
|
+
|
|
3
|
+
版权所有 (c) 2026 Nixdorfer。保留所有权利。
|
|
4
|
+
|
|
5
|
+
本仓库的源代码、构建材料及附带资源(统称“本作品”)不是开源软件,
|
|
6
|
+
不适用任何 OSI 认证的开源许可证。
|
|
7
|
+
|
|
8
|
+
一、官方作者工具分发许可
|
|
9
|
+
|
|
10
|
+
版权所有者可以通过其官方网站、npm registry、签名安装包或其他官方渠道,
|
|
11
|
+
复制并分发由本作品构建的 WorldEngine 作者工具及其必要运行资源
|
|
12
|
+
(统称“官方作者工具”)。仅版权所有者或其书面指定的发布者享有此分发权。
|
|
13
|
+
|
|
14
|
+
二、作者用户许可
|
|
15
|
+
|
|
16
|
+
从官方渠道取得官方作者工具的作者用户,可以:
|
|
17
|
+
|
|
18
|
+
1. 安装和运行官方作者工具;
|
|
19
|
+
2. 使用官方作者工具创作、编辑、预览、导入、导出和提交其有权处理的内容;
|
|
20
|
+
3. 为上述使用目的制作合理必要的本地备份副本。
|
|
21
|
+
|
|
22
|
+
三、未授予的权利
|
|
23
|
+
|
|
24
|
+
除第二条明确允许的行为外,本许可证不授予作者用户或其他第三方以下权利:
|
|
25
|
+
|
|
26
|
+
1. 修改、改编、反编译、反汇编或制作官方作者工具的派生作品;
|
|
27
|
+
2. 复制、镜像、转发、再上传、出售、出租、再分发或再许可官方作者工具;
|
|
28
|
+
3. 使用本作品的源代码、构建材料或任何部分开发、提供或训练其他产品或服务;
|
|
29
|
+
4. 删除或规避版权、许可、签名、访问控制或其他权利管理信息。
|
|
30
|
+
|
|
31
|
+
法律强制允许且合同不得排除的权利不受上述限制影响。
|
|
32
|
+
|
|
33
|
+
四、源代码查看
|
|
34
|
+
|
|
35
|
+
第三方可以在已获合法访问权限的范围内查看本作品,用于审查、安全研究或学习参考;
|
|
36
|
+
查看不授予运行、复制、修改、分发、再许可或用于其他产品与服务的权利。
|
|
37
|
+
|
|
38
|
+
五、无担保与责任限制
|
|
39
|
+
|
|
40
|
+
本作品与官方作者工具均按“现状”提供,不附带任何明示或暗示的担保。
|
|
41
|
+
在适用法律允许的最大范围内,版权所有者不对因访问或使用本作品或官方作者工具
|
|
42
|
+
造成的任何损失承担责任。
|
|
43
|
+
|
|
44
|
+
六、终止
|
|
45
|
+
|
|
46
|
+
违反本许可证将立即终止相应的访问与使用权;终止不影响版权所有者已经产生的权利和救济。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const ACCOUNT_PROJECT_ROOT_DOMAIN = "worldengine-account-project-root-v1\0";
|
|
2
|
+
/**
|
|
3
|
+
* 账户模型代理的 project-root identity 唯一 canonical 输入。
|
|
4
|
+
* 调用方必须先解析 realpath;Windows 路径按文件系统语义折叠大小写。
|
|
5
|
+
*/
|
|
6
|
+
export function canonicalAccountProjectRootIdentityBytes(canonicalRoot, platform) {
|
|
7
|
+
if (typeof canonicalRoot !== "string" || canonicalRoot.length === 0) {
|
|
8
|
+
throw new TypeError("canonical project root 不能为空");
|
|
9
|
+
}
|
|
10
|
+
const normalized = platform === "win32" ? canonicalRoot.toLowerCase() : canonicalRoot;
|
|
11
|
+
return new TextEncoder().encode(`${ACCOUNT_PROJECT_ROOT_DOMAIN}${normalized}`);
|
|
12
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type ProjectFormatErrorCode = "E_PROJECT_MANIFEST_INVALID" | "E_PROJECT_PATH_INVALID" | "E_SCENARIO_PROTOBUF_INVALID" | "E_SCENARIO_SCHEMA_VERSION_UNSUPPORTED" | "E_WESP_DIGEST_MISMATCH" | "E_WESP_MAGIC_INVALID" | "E_WESP_SIZE_MISMATCH" | "E_WESP_VERSION_UNSUPPORTED";
|
|
2
|
+
export declare class ProjectFormatError extends Error {
|
|
3
|
+
readonly code: ProjectFormatErrorCode;
|
|
4
|
+
constructor(code: ProjectFormatErrorCode, message: string);
|
|
5
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ProjectFormatError } from "./errors.js";
|
|
2
|
+
import { sha256Hex } from "./integrity.js";
|
|
3
|
+
function compareBytes(left, right) {
|
|
4
|
+
const length = Math.min(left.length, right.length);
|
|
5
|
+
for (let index = 0; index < length; index += 1) {
|
|
6
|
+
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
7
|
+
if (difference !== 0)
|
|
8
|
+
return difference;
|
|
9
|
+
}
|
|
10
|
+
return left.length - right.length;
|
|
11
|
+
}
|
|
12
|
+
function validateRelativeCanonicalPath(path) {
|
|
13
|
+
if (path.length === 0 ||
|
|
14
|
+
path.includes("\\") ||
|
|
15
|
+
path.startsWith("/") ||
|
|
16
|
+
/^[A-Za-z]:/.test(path) ||
|
|
17
|
+
path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
18
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", `非法 canonical file-tree path:${path}`);
|
|
19
|
+
}
|
|
20
|
+
const normalized = path.normalize("NFC");
|
|
21
|
+
if (normalized !== path) {
|
|
22
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", `file-tree path 必须使用 NFC:${path}`);
|
|
23
|
+
}
|
|
24
|
+
return path;
|
|
25
|
+
}
|
|
26
|
+
export async function computeCanonicalFileTreeDigest(entries, subtle) {
|
|
27
|
+
const encoder = new TextEncoder();
|
|
28
|
+
const normalized = entries.map((entry) => {
|
|
29
|
+
if (!(entry.bytes instanceof Uint8Array))
|
|
30
|
+
throw new TypeError("file-tree bytes 必须是 Uint8Array");
|
|
31
|
+
const path = validateRelativeCanonicalPath(entry.path);
|
|
32
|
+
return { path, pathBytes: encoder.encode(path), bytes: entry.bytes };
|
|
33
|
+
});
|
|
34
|
+
normalized.sort((left, right) => compareBytes(left.pathBytes, right.pathBytes));
|
|
35
|
+
for (let index = 1; index < normalized.length; index += 1) {
|
|
36
|
+
if (normalized[index - 1]?.path === normalized[index]?.path) {
|
|
37
|
+
throw new ProjectFormatError("E_PROJECT_MANIFEST_INVALID", `file-tree path 重复:${normalized[index]?.path ?? ""}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const totalSize = normalized.reduce((sum, entry) => sum + 4 + entry.pathBytes.byteLength + 8 + entry.bytes.byteLength, 0);
|
|
41
|
+
const canonical = new Uint8Array(totalSize);
|
|
42
|
+
const view = new DataView(canonical.buffer);
|
|
43
|
+
let offset = 0;
|
|
44
|
+
for (const entry of normalized) {
|
|
45
|
+
view.setUint32(offset, entry.pathBytes.byteLength);
|
|
46
|
+
offset += 4;
|
|
47
|
+
canonical.set(entry.pathBytes, offset);
|
|
48
|
+
offset += entry.pathBytes.byteLength;
|
|
49
|
+
view.setBigUint64(offset, BigInt(entry.bytes.byteLength));
|
|
50
|
+
offset += 8;
|
|
51
|
+
canonical.set(entry.bytes, offset);
|
|
52
|
+
offset += entry.bytes.byteLength;
|
|
53
|
+
}
|
|
54
|
+
return sha256Hex(canonical, subtle);
|
|
55
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type CanonicalFileTreeEntry } from "./file-tree.js";
|
|
2
|
+
import { type GalleryScenarioEntityIdentity } from "./gallery.js";
|
|
3
|
+
import { type LocalAuthorProjectManifestV1 } from "./manifest.js";
|
|
4
|
+
export interface LegacyGalleryMigrationEntry extends CanonicalFileTreeEntry {
|
|
5
|
+
readonly scenario_entity?: GalleryScenarioEntityIdentity;
|
|
6
|
+
}
|
|
7
|
+
export interface LocalAuthorProjectGalleryMigrationMoveV1 {
|
|
8
|
+
readonly source_path: string;
|
|
9
|
+
readonly source_sha256: string;
|
|
10
|
+
readonly destination_path: string;
|
|
11
|
+
}
|
|
12
|
+
export interface LocalAuthorProjectGalleryMigrationPlanV1 {
|
|
13
|
+
readonly schema_version: 1;
|
|
14
|
+
readonly kind: "legacy-gallery-v1-to-v2";
|
|
15
|
+
readonly project_id: string;
|
|
16
|
+
readonly project_revision: number;
|
|
17
|
+
readonly source_manifest_canonical_digest: string;
|
|
18
|
+
readonly legacy_entry_count: number;
|
|
19
|
+
readonly legacy_tree_digest: string;
|
|
20
|
+
readonly gallery_content_tree_sha256: string;
|
|
21
|
+
readonly moves: readonly LocalAuthorProjectGalleryMigrationMoveV1[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 只生成可恢复计划,不触碰文件系统。调用者应用前必须重算 manifest 与 legacy_tree preimage;
|
|
25
|
+
* scenario legacy 文件没有明确 node/edge 绑定时 fail closed,禁止猜测归属。
|
|
26
|
+
*/
|
|
27
|
+
export declare function planLocalAuthorProjectGalleryMigration(manifest: LocalAuthorProjectManifestV1, entries: readonly LegacyGalleryMigrationEntry[], subtle?: SubtleCrypto): Promise<LocalAuthorProjectGalleryMigrationPlanV1>;
|
|
28
|
+
export declare function assertLocalAuthorProjectGalleryMigrationPreimage(manifest: LocalAuthorProjectManifestV1, entries: readonly LegacyGalleryMigrationEntry[], plan: LocalAuthorProjectGalleryMigrationPlanV1, subtle?: SubtleCrypto): Promise<void>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { computeCanonicalFileTreeDigest } from "./file-tree.js";
|
|
2
|
+
import { encodeGalleryLegacyAssetFileName, galleryScenarioAssetPath, galleryViewAssetPath } from "./gallery.js";
|
|
3
|
+
import { sha256Hex } from "./integrity.js";
|
|
4
|
+
import { computeCanonicalProjectDigest, validateLocalAuthorProjectManifest } from "./manifest.js";
|
|
5
|
+
import { ProjectFormatError } from "./errors.js";
|
|
6
|
+
const VIEW_IMAGE = /\.([a-z0-9]+)$/;
|
|
7
|
+
const ALLOWED_VIEW_IMAGE_EXTENSIONS = new Set(["avif", "gif", "jpeg", "jpg", "png", "svg", "webp"]);
|
|
8
|
+
function legacyRelativePath(path, root) {
|
|
9
|
+
return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 只生成可恢复计划,不触碰文件系统。调用者应用前必须重算 manifest 与 legacy_tree preimage;
|
|
13
|
+
* scenario legacy 文件没有明确 node/edge 绑定时 fail closed,禁止猜测归属。
|
|
14
|
+
*/
|
|
15
|
+
export async function planLocalAuthorProjectGalleryMigration(manifest, entries, subtle) {
|
|
16
|
+
validateLocalAuthorProjectManifest(manifest);
|
|
17
|
+
if (manifest.schema_version !== 1)
|
|
18
|
+
throw new ProjectFormatError("E_PROJECT_MANIFEST_INVALID", "gallery legacy migration 只接受 manifest v1");
|
|
19
|
+
const moves = await Promise.all(entries.map(async (entry) => {
|
|
20
|
+
const scenarioRelative = legacyRelativePath(entry.path, "scenario/assets");
|
|
21
|
+
const viewRelative = legacyRelativePath(entry.path, "view/public");
|
|
22
|
+
let destinationPath;
|
|
23
|
+
if (scenarioRelative !== undefined) {
|
|
24
|
+
if (entry.scenario_entity === undefined)
|
|
25
|
+
throw new ProjectFormatError("E_PROJECT_MANIFEST_INVALID", `legacy scenario asset 缺少 node/edge 绑定:${entry.path}`);
|
|
26
|
+
destinationPath = galleryScenarioAssetPath(entry.scenario_entity.kind, entry.scenario_entity.id, encodeGalleryLegacyAssetFileName(scenarioRelative));
|
|
27
|
+
}
|
|
28
|
+
else if (viewRelative !== undefined) {
|
|
29
|
+
const match = VIEW_IMAGE.exec(viewRelative.toLowerCase());
|
|
30
|
+
if (match === null || !ALLOWED_VIEW_IMAGE_EXTENSIONS.has(match[1] ?? ""))
|
|
31
|
+
throw new ProjectFormatError("E_PROJECT_MANIFEST_INVALID", `legacy view/public 含非图片或无扩展资源:${entry.path}`);
|
|
32
|
+
destinationPath = galleryViewAssetPath(await sha256Hex(entry.bytes, subtle), match[1]);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", `legacy gallery migration 路径不受支持:${entry.path}`);
|
|
36
|
+
}
|
|
37
|
+
return Object.freeze({ source_path: entry.path, source_sha256: await sha256Hex(entry.bytes, subtle), destination_path: destinationPath });
|
|
38
|
+
}));
|
|
39
|
+
const destinations = new Set();
|
|
40
|
+
for (const move of moves) {
|
|
41
|
+
if (destinations.has(move.destination_path))
|
|
42
|
+
throw new ProjectFormatError("E_PROJECT_MANIFEST_INVALID", `gallery migration destination 冲突:${move.destination_path}`);
|
|
43
|
+
destinations.add(move.destination_path);
|
|
44
|
+
}
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
schema_version: 1,
|
|
47
|
+
kind: "legacy-gallery-v1-to-v2",
|
|
48
|
+
project_id: manifest.project_id,
|
|
49
|
+
project_revision: manifest.project_revision,
|
|
50
|
+
source_manifest_canonical_digest: await computeCanonicalProjectDigest(manifest, subtle),
|
|
51
|
+
legacy_entry_count: entries.length,
|
|
52
|
+
legacy_tree_digest: await computeCanonicalFileTreeDigest(entries, subtle),
|
|
53
|
+
gallery_content_tree_sha256: await computeCanonicalFileTreeDigest(moves.map((move) => ({
|
|
54
|
+
path: move.destination_path.slice("gallery/".length),
|
|
55
|
+
bytes: entries.find((entry) => entry.path === move.source_path).bytes,
|
|
56
|
+
})), subtle),
|
|
57
|
+
moves: Object.freeze(moves.sort((left, right) => left.source_path.localeCompare(right.source_path, "en"))),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export async function assertLocalAuthorProjectGalleryMigrationPreimage(manifest, entries, plan, subtle) {
|
|
61
|
+
const actual = await planLocalAuthorProjectGalleryMigration(manifest, entries, subtle);
|
|
62
|
+
if (JSON.stringify(plan) !== JSON.stringify(actual)) {
|
|
63
|
+
throw new ProjectFormatError("E_PROJECT_MANIFEST_INVALID", "gallery migration preimage 已过期或不匹配");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type GalleryScenarioEntityKind = "node" | "edge";
|
|
2
|
+
export declare const LOCAL_AUTHOR_GALLERY_PATHS: Readonly<{
|
|
3
|
+
readonly root: "gallery";
|
|
4
|
+
readonly scenario: "gallery/scenario";
|
|
5
|
+
readonly view: "gallery/view";
|
|
6
|
+
}>;
|
|
7
|
+
export interface GalleryScenarioEntityIdentity {
|
|
8
|
+
readonly kind: GalleryScenarioEntityKind;
|
|
9
|
+
readonly id: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function encodeGalleryScenarioEntityDirectory(kind: GalleryScenarioEntityKind, id: string): string;
|
|
12
|
+
export declare function decodeGalleryScenarioEntityDirectory(directory: string): GalleryScenarioEntityIdentity;
|
|
13
|
+
export declare function galleryScenarioAssetPath(kind: GalleryScenarioEntityKind, id: string, fileName: string): string;
|
|
14
|
+
/** 将 legacy root 下的相对路径压成可逆单文件名,避免嵌套目录逃逸或扁平化碰撞。 */
|
|
15
|
+
export declare function encodeGalleryLegacyAssetFileName(relativePath: string): string;
|
|
16
|
+
export declare function galleryViewAssetPath(contentSha256: string, extension?: string): string;
|
package/dist/gallery.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ProjectFormatError } from "./errors.js";
|
|
2
|
+
export const LOCAL_AUTHOR_GALLERY_PATHS = Object.freeze({
|
|
3
|
+
root: "gallery",
|
|
4
|
+
scenario: "gallery/scenario",
|
|
5
|
+
view: "gallery/view",
|
|
6
|
+
});
|
|
7
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
8
|
+
const SAFE_LEAF = /^[^./\\][^/\\]*$/u;
|
|
9
|
+
const SAFE_EXTENSION = /^[a-z0-9]+$/;
|
|
10
|
+
function assertUnicodeScalarString(value) {
|
|
11
|
+
if (value.length === 0)
|
|
12
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", "gallery entity id 不能为空");
|
|
13
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
14
|
+
const code = value.charCodeAt(index);
|
|
15
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
16
|
+
const next = value.charCodeAt(index + 1);
|
|
17
|
+
if (next < 0xdc00 || next > 0xdfff)
|
|
18
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", "gallery entity id 含孤立 surrogate");
|
|
19
|
+
index += 1;
|
|
20
|
+
}
|
|
21
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
22
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", "gallery entity id 含孤立 surrogate");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (new TextEncoder().encode(value).byteLength > 100)
|
|
26
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", "gallery entity id UTF-8 超过 100 bytes");
|
|
27
|
+
}
|
|
28
|
+
function hex(bytes) {
|
|
29
|
+
return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
|
|
30
|
+
}
|
|
31
|
+
export function encodeGalleryScenarioEntityDirectory(kind, id) {
|
|
32
|
+
if (kind !== "node" && kind !== "edge")
|
|
33
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", `gallery entity kind 无效:${String(kind)}`);
|
|
34
|
+
assertUnicodeScalarString(id);
|
|
35
|
+
return `${kind}-${hex(new TextEncoder().encode(id))}`;
|
|
36
|
+
}
|
|
37
|
+
export function decodeGalleryScenarioEntityDirectory(directory) {
|
|
38
|
+
const match = /^(node|edge)-([a-f0-9]+)$/.exec(directory);
|
|
39
|
+
if (match === null || match[2].length % 2 !== 0)
|
|
40
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", `gallery entity directory 非 canonical:${directory}`);
|
|
41
|
+
const bytes = new Uint8Array(match[2].length / 2);
|
|
42
|
+
for (let index = 0; index < bytes.length; index += 1)
|
|
43
|
+
bytes[index] = Number.parseInt(match[2].slice(index * 2, index * 2 + 2), 16);
|
|
44
|
+
let id;
|
|
45
|
+
try {
|
|
46
|
+
id = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", `gallery entity directory 含非法 UTF-8:${directory}`);
|
|
50
|
+
}
|
|
51
|
+
const kind = match[1];
|
|
52
|
+
if (encodeGalleryScenarioEntityDirectory(kind, id) !== directory)
|
|
53
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", `gallery entity directory 非 canonical:${directory}`);
|
|
54
|
+
return Object.freeze({ kind, id });
|
|
55
|
+
}
|
|
56
|
+
function assertSafeLeaf(fileName) {
|
|
57
|
+
const stem = fileName.split(".")[0]?.toUpperCase() ?? "";
|
|
58
|
+
if (!SAFE_LEAF.test(fileName) || /[\u0000-\u001f<>:"|?*]/u.test(fileName) || fileName === "." || fileName === ".." || fileName.endsWith(".") || fileName.endsWith(" ") || /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/.test(stem) || fileName.normalize("NFC") !== fileName) {
|
|
59
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", `gallery 文件名非法:${fileName}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function galleryScenarioAssetPath(kind, id, fileName) {
|
|
63
|
+
assertSafeLeaf(fileName);
|
|
64
|
+
return `${LOCAL_AUTHOR_GALLERY_PATHS.scenario}/${encodeGalleryScenarioEntityDirectory(kind, id)}/${fileName}`;
|
|
65
|
+
}
|
|
66
|
+
/** 将 legacy root 下的相对路径压成可逆单文件名,避免嵌套目录逃逸或扁平化碰撞。 */
|
|
67
|
+
export function encodeGalleryLegacyAssetFileName(relativePath) {
|
|
68
|
+
if (relativePath.length === 0 || relativePath.includes("\\") || relativePath.startsWith("/") || relativePath.split("/").some((segment) => segment === "" || segment === "." || segment === "..") || relativePath.normalize("NFC") !== relativePath) {
|
|
69
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", `legacy gallery path 非 canonical:${relativePath}`);
|
|
70
|
+
}
|
|
71
|
+
assertUnicodeScalarString(relativePath);
|
|
72
|
+
const encoded = `legacy-${hex(new TextEncoder().encode(relativePath))}`;
|
|
73
|
+
if (encoded.length > 240)
|
|
74
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", "legacy gallery path 编码后超过安全文件名长度");
|
|
75
|
+
return encoded;
|
|
76
|
+
}
|
|
77
|
+
export function galleryViewAssetPath(contentSha256, extension) {
|
|
78
|
+
if (!SHA256.test(contentSha256))
|
|
79
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", "gallery view asset digest 必须是小写 SHA-256");
|
|
80
|
+
if (extension === undefined || extension === "")
|
|
81
|
+
return `${LOCAL_AUTHOR_GALLERY_PATHS.view}/${contentSha256}`;
|
|
82
|
+
const canonicalExtension = (extension.startsWith(".") ? extension.slice(1) : extension).toLowerCase();
|
|
83
|
+
if (!SAFE_EXTENSION.test(canonicalExtension))
|
|
84
|
+
throw new ProjectFormatError("E_PROJECT_PATH_INVALID", `gallery view asset 扩展名非法:${extension}`);
|
|
85
|
+
return `${LOCAL_AUTHOR_GALLERY_PATHS.view}/${contentSha256}.${canonicalExtension}`;
|
|
86
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./errors.js";
|
|
2
|
+
export * from "./account-project-root.js";
|
|
3
|
+
export * from "./file-tree.js";
|
|
4
|
+
export * from "./gallery.js";
|
|
5
|
+
export * from "./gallery-migration.js";
|
|
6
|
+
export * from "./integrity.js";
|
|
7
|
+
export * from "./lossless-blob.js";
|
|
8
|
+
export * from "./neutral-upload.js";
|
|
9
|
+
export * from "./manifest.js";
|
|
10
|
+
export * from "./scenario-part.js";
|
|
11
|
+
export * from "./tar.js";
|
|
12
|
+
export * from "./wesp.js";
|
|
13
|
+
export * from "./windows-executable.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./errors.js";
|
|
2
|
+
export * from "./account-project-root.js";
|
|
3
|
+
export * from "./file-tree.js";
|
|
4
|
+
export * from "./gallery.js";
|
|
5
|
+
export * from "./gallery-migration.js";
|
|
6
|
+
export * from "./integrity.js";
|
|
7
|
+
export * from "./lossless-blob.js";
|
|
8
|
+
export * from "./neutral-upload.js";
|
|
9
|
+
export * from "./manifest.js";
|
|
10
|
+
export * from "./scenario-part.js";
|
|
11
|
+
export * from "./tar.js";
|
|
12
|
+
export * from "./wesp.js";
|
|
13
|
+
export * from "./windows-executable.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function bytesToHex(bytes: Uint8Array): string;
|
|
2
|
+
/** 比较固定格式 SHA-256 hex,避免提前退出造成不必要的差异行为。 */
|
|
3
|
+
export declare function sha256HexEquals(left: string, right: string): boolean;
|
|
4
|
+
export declare function sha256Bytes(bytes: Uint8Array, subtle?: SubtleCrypto): Promise<Uint8Array>;
|
|
5
|
+
export declare function sha256Hex(bytes: Uint8Array, subtle?: SubtleCrypto): Promise<string>;
|
|
6
|
+
export declare function assertSha256Hex(value: unknown, field: string): asserts value is string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ProjectFormatError } from "./errors.js";
|
|
2
|
+
export function bytesToHex(bytes) {
|
|
3
|
+
return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
|
|
4
|
+
}
|
|
5
|
+
/** 比较固定格式 SHA-256 hex,避免提前退出造成不必要的差异行为。 */
|
|
6
|
+
export function sha256HexEquals(left, right) {
|
|
7
|
+
if (left.length !== 64 || right.length !== 64)
|
|
8
|
+
return false;
|
|
9
|
+
let difference = 0;
|
|
10
|
+
for (let index = 0; index < 64; index += 1) {
|
|
11
|
+
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
12
|
+
}
|
|
13
|
+
return difference === 0;
|
|
14
|
+
}
|
|
15
|
+
export async function sha256Bytes(bytes, subtle = globalThis.crypto.subtle) {
|
|
16
|
+
const digest = await subtle.digest("SHA-256", bytes);
|
|
17
|
+
return new Uint8Array(digest);
|
|
18
|
+
}
|
|
19
|
+
export async function sha256Hex(bytes, subtle) {
|
|
20
|
+
return bytesToHex(await sha256Bytes(bytes, subtle));
|
|
21
|
+
}
|
|
22
|
+
export function assertSha256Hex(value, field) {
|
|
23
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
|
|
24
|
+
throw new ProjectFormatError("E_PROJECT_MANIFEST_INVALID", `${field} 必须是小写 SHA-256 hex`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Lossless blob descriptor v1:只描述传输对象,不授予 object_key 的访问权限。 */
|
|
2
|
+
export interface LosslessBlobDescriptorV1 {
|
|
3
|
+
readonly schema_version: 1;
|
|
4
|
+
readonly codec: "identity" | "gzip-v1";
|
|
5
|
+
readonly raw: LosslessBlobDigest;
|
|
6
|
+
readonly encoded: LosslessBlobDigest;
|
|
7
|
+
readonly chunks: readonly LosslessBlobChunkDescriptor[];
|
|
8
|
+
}
|
|
9
|
+
export interface LosslessBlobDigest {
|
|
10
|
+
readonly byte_length: number;
|
|
11
|
+
readonly sha256: string;
|
|
12
|
+
}
|
|
13
|
+
export interface LosslessBlobChunkDescriptor extends LosslessBlobDigest {
|
|
14
|
+
readonly index: number;
|
|
15
|
+
readonly object_key: string;
|
|
16
|
+
}
|
|
17
|
+
export type LosslessBlobByteSource = AsyncIterable<Uint8Array>;
|
|
18
|
+
export type LosslessBlobChunkWriter = (chunk: Readonly<{
|
|
19
|
+
index: number;
|
|
20
|
+
bytes: Uint8Array;
|
|
21
|
+
sha256: string;
|
|
22
|
+
}>) => Promise<string> | string;
|
|
23
|
+
export type LosslessBlobChunkReader = (chunk: LosslessBlobChunkDescriptor) => Promise<LosslessBlobByteSource> | LosslessBlobByteSource;
|
|
24
|
+
export interface LosslessBlobSha256 {
|
|
25
|
+
update(bytes: Uint8Array): LosslessBlobSha256;
|
|
26
|
+
digestHex(): string;
|
|
27
|
+
}
|
|
28
|
+
export interface LosslessBlobOptions {
|
|
29
|
+
readonly codec?: LosslessBlobDescriptorV1["codec"];
|
|
30
|
+
/** 运行时官方/受审计的增量 SHA-256 adapter;模块不自研密码学实现。 */
|
|
31
|
+
readonly createSha256: () => LosslessBlobSha256;
|
|
32
|
+
}
|
|
33
|
+
export declare const LOSSLESS_BLOB_CHUNK_BYTES: number;
|
|
34
|
+
export declare const LOSSLESS_BLOB_MAX_BYTES: number;
|
|
35
|
+
export declare const LOSSLESS_BLOB_MAX_CHUNKS = 128;
|
|
36
|
+
export declare class LosslessBlobError extends Error {
|
|
37
|
+
constructor(message: string);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 流式编码原始 bytes。writer 返回的 object_key 仅是不透明定位符;鉴权属于运输层。
|
|
41
|
+
* 失败时已写 chunk 不会出现在 descriptor 中,writer 应把它们留在临时 staging 中清理。
|
|
42
|
+
*/
|
|
43
|
+
export declare function encodeLosslessBlob(source: LosslessBlobByteSource, writer: LosslessBlobChunkWriter, options: LosslessBlobOptions): Promise<LosslessBlobDescriptorV1>;
|
|
44
|
+
/**
|
|
45
|
+
* 返回逐块验证后的原始 byte 流。只有迭代到 EOF 且未抛错,整个 blob 才 verified;
|
|
46
|
+
* 调用方必须写临时 staging,并在迭代成功结束后原子 commit,不能发布此前产出的前缀。
|
|
47
|
+
*/
|
|
48
|
+
export declare function decodeLosslessBlob(candidate: unknown, reader: LosslessBlobChunkReader, options: Pick<LosslessBlobOptions, "createSha256">): LosslessBlobByteSource;
|
|
49
|
+
export declare function validateLosslessBlobDescriptor(candidate: unknown): LosslessBlobDescriptorV1;
|