@carbonenginejs/runtime-utils 0.1.5 → 0.1.6
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/package.json +2 -1
- package/src/index.js +1 -0
- package/src/resFile.js +149 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carbonenginejs/runtime-utils",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Browser-safe shared utilities, math, constants, Carbon types, schemas, documents, and runtime model primitives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"./tangent": "./src/tangent.js",
|
|
82
82
|
"./geometry": "./src/geometry/index.js",
|
|
83
83
|
"./path": "./src/path.js",
|
|
84
|
+
"./resfile": "./src/resFile.js",
|
|
84
85
|
"./text": "./src/text.js",
|
|
85
86
|
"./validation": "./src/validation.js"
|
|
86
87
|
},
|
package/src/index.js
CHANGED
package/src/resFile.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVE resource file addressing.
|
|
3
|
+
*
|
|
4
|
+
* A resource's stored name IS its identity: a hash of its logical path, then
|
|
5
|
+
* the md5 of its contents. Two builds either carry the same address for a path
|
|
6
|
+
* or they do not, which makes address comparison - not file hashing - the way
|
|
7
|
+
* to tell whether anything changed.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately no md5 here. Computing it is I/O, and the shape of that I/O
|
|
10
|
+
* differs per environment: a file stream in Node, an ArrayBuffer in a browser.
|
|
11
|
+
* The digest is passed in, so this module stays environment-neutral and every
|
|
12
|
+
* consumer shares one derivation instead of keeping its own.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ADDRESS = /^([a-f0-9]{2})\/([a-f0-9]{16})_([a-f0-9]{32})$/u;
|
|
16
|
+
|
|
17
|
+
/** Lazily built byte-to-hex table. */
|
|
18
|
+
let hex = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* FNV-1 (64-bit) over a resource's logical path.
|
|
22
|
+
*
|
|
23
|
+
* FNV-**1**, not 1a: the multiply happens before the XOR. Offset basis
|
|
24
|
+
* 0xcbf29ce484222325, prime 0x100000001b3, emitted as 16 lowercase hex digits.
|
|
25
|
+
*
|
|
26
|
+
* Verified against 1718 content-addressed entries from a real resfileindex.
|
|
27
|
+
*
|
|
28
|
+
* ONLY DEFINED FOR ASCII PATHS. Two implementations exist in the wild - one
|
|
29
|
+
* hashing UTF-8 bytes, one hashing UTF-16 code units - and they agree on every
|
|
30
|
+
* ASCII string and disagree beyond it. No real resource path has ever been
|
|
31
|
+
* non-ASCII, so no evidence exists for which is correct, and guessing would
|
|
32
|
+
* turn a dormant difference into a live wrong answer. A non-ASCII path
|
|
33
|
+
* therefore throws rather than returning something plausible.
|
|
34
|
+
*
|
|
35
|
+
* @param {String} logicalPath - the prefixed path, e.g. "res:/graphics/x.dds"
|
|
36
|
+
* @returns {String} 16 lowercase hex digits
|
|
37
|
+
*/
|
|
38
|
+
export function fnv164(logicalPath)
|
|
39
|
+
{
|
|
40
|
+
const value = String(logicalPath);
|
|
41
|
+
|
|
42
|
+
if (!hex)
|
|
43
|
+
{
|
|
44
|
+
hex = [];
|
|
45
|
+
for (let i = 0; i < 256; i++) hex[i] = ((i >> 4) & 15).toString(16) + (i & 15).toString(16);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 16-bit limbs, so the 64-bit multiply stays inside the safe integer range.
|
|
49
|
+
let v0 = 0x2325;
|
|
50
|
+
let v1 = 0x8422;
|
|
51
|
+
let v2 = 0x9ce4;
|
|
52
|
+
let v3 = 0xcbf2;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < value.length; i++)
|
|
55
|
+
{
|
|
56
|
+
const code = value.charCodeAt(i);
|
|
57
|
+
|
|
58
|
+
if (code > 0x7f)
|
|
59
|
+
{
|
|
60
|
+
throw new RangeError(
|
|
61
|
+
`Resource path hashing is only defined for ASCII: ${JSON.stringify(value)}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Multiply by 0x100000001b3: the low limb contributes 0x1b3 (435) to
|
|
66
|
+
// every limb, and the 0x100000000 term shifts two limbs left.
|
|
67
|
+
let t0 = v0 * 435;
|
|
68
|
+
let t1 = v1 * 435;
|
|
69
|
+
let t2 = v2 * 435 + (v0 << 8);
|
|
70
|
+
const t3 = v3 * 435 + (v1 << 8);
|
|
71
|
+
|
|
72
|
+
t1 += t0 >>> 16;
|
|
73
|
+
v0 = t0 & 0xffff;
|
|
74
|
+
t2 += t1 >>> 16;
|
|
75
|
+
v1 = t1 & 0xffff;
|
|
76
|
+
v3 = (t3 + (t2 >>> 16)) & 0xffff;
|
|
77
|
+
v2 = t2 & 0xffff;
|
|
78
|
+
|
|
79
|
+
v0 ^= code;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return hex[v3 >> 8] + hex[v3 & 255]
|
|
83
|
+
+ hex[v2 >> 8] + hex[v2 & 255]
|
|
84
|
+
+ hex[v1 >> 8] + hex[v1 & 255]
|
|
85
|
+
+ hex[v0 >> 8] + hex[v0 & 255];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Builds a resource's stored address from its path and its content digest.
|
|
90
|
+
*
|
|
91
|
+
* The shard directory is the first two characters of the path hash itself, not
|
|
92
|
+
* a separate value - a detail that is easy to reimplement wrongly.
|
|
93
|
+
*
|
|
94
|
+
* @param {String} logicalPath
|
|
95
|
+
* @param {String} md5 - 32 hex digits, the md5 of the file's contents
|
|
96
|
+
* @returns {String} "<shard>/<pathHash>_<md5>"
|
|
97
|
+
*/
|
|
98
|
+
export function resFileAddress(logicalPath, md5)
|
|
99
|
+
{
|
|
100
|
+
const digest = String(md5).toLowerCase();
|
|
101
|
+
|
|
102
|
+
if (!/^[a-f0-9]{32}$/u.test(digest))
|
|
103
|
+
{
|
|
104
|
+
throw new TypeError(`Resource content digest must be 32 hex digits: ${md5}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const pathHash = fnv164(logicalPath);
|
|
108
|
+
|
|
109
|
+
return `${pathHash.slice(0, 2)}/${pathHash}_${digest}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Splits a stored address into its parts, or null when it is not one.
|
|
114
|
+
*
|
|
115
|
+
* Null rather than throwing: an index legitimately carries plain paths for
|
|
116
|
+
* overlay entries alongside content-addressed ones, so "not an address" is an
|
|
117
|
+
* ordinary answer rather than an error.
|
|
118
|
+
*
|
|
119
|
+
* @param {String} address
|
|
120
|
+
* @returns {{shard: String, pathHash: String, checksum: String}|null}
|
|
121
|
+
*/
|
|
122
|
+
export function parseResFileAddress(address)
|
|
123
|
+
{
|
|
124
|
+
const match = String(address ?? "").toLowerCase().match(ADDRESS);
|
|
125
|
+
|
|
126
|
+
return match ? { shard: match[1], pathHash: match[2], checksum: match[3] } : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Checks a stored address against the path it claims to be for.
|
|
131
|
+
*
|
|
132
|
+
* Only the path half can be checked without reading the file; the content half
|
|
133
|
+
* is verifiable only against the bytes. Returns false rather than throwing so a
|
|
134
|
+
* caller can survey an index without exception handling - except for a
|
|
135
|
+
* non-ASCII path, where the answer is genuinely unknown.
|
|
136
|
+
*
|
|
137
|
+
* @param {String} address
|
|
138
|
+
* @param {String} logicalPath
|
|
139
|
+
* @returns {Boolean}
|
|
140
|
+
*/
|
|
141
|
+
export function isResFileAddressFor(address, logicalPath)
|
|
142
|
+
{
|
|
143
|
+
const parts = parseResFileAddress(address);
|
|
144
|
+
|
|
145
|
+
if (!parts) return false;
|
|
146
|
+
|
|
147
|
+
return parts.pathHash === fnv164(logicalPath)
|
|
148
|
+
&& parts.shard === parts.pathHash.slice(0, 2);
|
|
149
|
+
}
|