@atproto/lex-json 0.0.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/dist/blob.d.ts +5 -0
- package/dist/blob.d.ts.map +1 -0
- package/dist/blob.js +29 -0
- package/dist/blob.js.map +1 -0
- package/dist/bytes.d.ts +6 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +23 -0
- package/dist/bytes.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +8 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +3 -0
- package/dist/json.js.map +1 -0
- package/dist/lex-json.d.ts +14 -0
- package/dist/lex-json.d.ts.map +1 -0
- package/dist/lex-json.js +198 -0
- package/dist/lex-json.js.map +1 -0
- package/dist/link.d.ts +7 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +35 -0
- package/dist/link.js.map +1 -0
- package/package.json +47 -0
- package/src/blob.ts +31 -0
- package/src/bytes.ts +26 -0
- package/src/index.ts +4 -0
- package/src/json.ts +3 -0
- package/src/lex-json.test.ts +511 -0
- package/src/lex-json.ts +221 -0
- package/src/link.ts +37 -0
package/dist/blob.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob.d.ts","sourceRoot":"","sources":["../src/blob.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAa,MAAM,mBAAmB,CAAA;AAG9D,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7B,OAAO,GAAG,SAAS,CAwBrB"}
|
package/dist/blob.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseBlobRef = parseBlobRef;
|
|
4
|
+
const lex_data_1 = require("@atproto/lex-data");
|
|
5
|
+
const link_js_1 = require("./link.js");
|
|
6
|
+
function parseBlobRef(input, options) {
|
|
7
|
+
if (input.$type !== 'blob')
|
|
8
|
+
return undefined;
|
|
9
|
+
const ref = input?.ref;
|
|
10
|
+
if (!ref || typeof ref !== 'object')
|
|
11
|
+
return undefined;
|
|
12
|
+
// @NOTE Because json to lex conversion can be performed both in a depth-first
|
|
13
|
+
// manner (e.g. via lexParse) or in a breadth-first manner (e.g. via
|
|
14
|
+
// jsonToLex), the `ref` property may either be a LexMap with a $link
|
|
15
|
+
// property, or it may already be a CID instance.
|
|
16
|
+
if ('$link' in ref) {
|
|
17
|
+
const cid = (0, link_js_1.parseLexLink)(ref);
|
|
18
|
+
if (!cid)
|
|
19
|
+
return undefined;
|
|
20
|
+
const blob = { ...input, ref: cid };
|
|
21
|
+
if ((0, lex_data_1.isBlobRef)(blob, options))
|
|
22
|
+
return blob;
|
|
23
|
+
}
|
|
24
|
+
if ((0, lex_data_1.isBlobRef)(input)) {
|
|
25
|
+
return input;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=blob.js.map
|
package/dist/blob.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob.js","sourceRoot":"","sources":["../src/blob.ts"],"names":[],"mappings":";;AAGA,oCA2BC;AA9BD,gDAA8D;AAC9D,uCAAwC;AAExC,SAAgB,YAAY,CAC1B,KAAa,EACb,OAA8B;IAE9B,IAAI,KAAK,CAAC,KAAK,KAAK,MAAM;QAAE,OAAO,SAAS,CAAA;IAE5C,MAAM,GAAG,GAAG,KAAK,EAAE,GAAG,CAAA;IACtB,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IAErD,8EAA8E;IAC9E,oEAAoE;IACpE,qEAAqE;IACrE,iDAAiD;IAEjD,IAAI,OAAO,IAAI,GAAG,EAAE,CAAC;QACnB,MAAM,GAAG,GAAG,IAAA,sBAAY,EAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAA;QAE1B,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,CAAA;QACnC,IAAI,IAAA,oBAAS,EAAC,IAAI,EAAE,OAAO,CAAC;YAAE,OAAO,IAAI,CAAA;IAC3C,CAAC;IAED,IAAI,IAAA,oBAAS,EAAC,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC","sourcesContent":["import { BlobRef, LexMap, isBlobRef } from '@atproto/lex-data'\nimport { parseLexLink } from './link.js'\n\nexport function parseBlobRef(\n input: LexMap,\n options?: { strict?: boolean },\n): BlobRef | undefined {\n if (input.$type !== 'blob') return undefined\n\n const ref = input?.ref\n if (!ref || typeof ref !== 'object') return undefined\n\n // @NOTE Because json to lex conversion can be performed both in a depth-first\n // manner (e.g. via lexParse) or in a breadth-first manner (e.g. via\n // jsonToLex), the `ref` property may either be a LexMap with a $link\n // property, or it may already be a CID instance.\n\n if ('$link' in ref) {\n const cid = parseLexLink(ref)\n if (!cid) return undefined\n\n const blob = { ...input, ref: cid }\n if (isBlobRef(blob, options)) return blob\n }\n\n if (isBlobRef(input)) {\n return input\n }\n\n return undefined\n}\n"]}
|
package/dist/bytes.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE;IACpC,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,GAAG,UAAU,GAAG,SAAS,CAgBzB;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,CAE3D"}
|
package/dist/bytes.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseLexBytes = parseLexBytes;
|
|
4
|
+
exports.encodeLexBytes = encodeLexBytes;
|
|
5
|
+
const lex_data_1 = require("@atproto/lex-data");
|
|
6
|
+
function parseLexBytes(input) {
|
|
7
|
+
if (!input || !('$bytes' in input)) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
for (const key in input) {
|
|
11
|
+
if (key !== '$bytes') {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (typeof input.$bytes !== 'string') {
|
|
16
|
+
throw new TypeError('$bytes must be a base64-encoded string');
|
|
17
|
+
}
|
|
18
|
+
return (0, lex_data_1.fromBase64)(input.$bytes);
|
|
19
|
+
}
|
|
20
|
+
function encodeLexBytes(bytes) {
|
|
21
|
+
return { $bytes: (0, lex_data_1.toBase64)(bytes) };
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=bytes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bytes.js","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":";;AAGA,sCAkBC;AAED,wCAEC;AAzBD,gDAAwD;AAGxD,SAAgB,aAAa,CAAC,KAE7B;IACC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,QAAQ,IAAI,KAAK,CAAC,EAAE,CAAC;QACnC,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;YACrB,OAAO,SAAS,CAAA;QAClB,CAAC;IACH,CAAC;IAED,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,IAAI,SAAS,CAAC,wCAAwC,CAAC,CAAA;IAC/D,CAAC;IAED,OAAO,IAAA,qBAAU,EAAC,KAAK,CAAC,MAAM,CAAC,CAAA;AACjC,CAAC;AAED,SAAgB,cAAc,CAAC,KAAiB;IAC9C,OAAO,EAAE,MAAM,EAAE,IAAA,mBAAQ,EAAC,KAAK,CAAC,EAAE,CAAA;AACpC,CAAC","sourcesContent":["import { fromBase64, toBase64 } from '@atproto/lex-data'\nimport { JsonValue } from './json.js'\n\nexport function parseLexBytes(input?: {\n $bytes?: unknown\n}): Uint8Array | undefined {\n if (!input || !('$bytes' in input)) {\n return undefined\n }\n\n for (const key in input) {\n if (key !== '$bytes') {\n return undefined\n }\n }\n\n if (typeof input.$bytes !== 'string') {\n throw new TypeError('$bytes must be a base64-encoded string')\n }\n\n return fromBase64(input.$bytes)\n}\n\nexport function encodeLexBytes(bytes: Uint8Array): JsonValue {\n return { $bytes: toBase64(bytes) }\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,WAAW,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
tslib_1.__exportStar(require("./bytes.js"), exports);
|
|
5
|
+
tslib_1.__exportStar(require("./json.js"), exports);
|
|
6
|
+
tslib_1.__exportStar(require("./lex-json.js"), exports);
|
|
7
|
+
tslib_1.__exportStar(require("./link.js"), exports);
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAA0B;AAC1B,oDAAyB;AACzB,wDAA6B;AAC7B,oDAAyB","sourcesContent":["export * from './bytes.js'\nexport * from './json.js'\nexport * from './lex-json.js'\nexport * from './link.js'\n"]}
|
package/dist/json.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;AACzD,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,SAAS,EAAE,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,EAAE,SAAS;CAAE,CAAA;AAChF,MAAM,MAAM,UAAU,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,EAAE,SAAS;CAAE,CAAA"}
|
package/dist/json.js
ADDED
package/dist/json.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.js","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"","sourcesContent":["export type JsonScalar = number | string | boolean | null\nexport type JsonValue = JsonScalar | JsonValue[] | { [_ in string]?: JsonValue }\nexport type JsonObject = { [_ in string]?: JsonValue }\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LexValue } from '@atproto/lex-data';
|
|
2
|
+
import { JsonValue } from './json.js';
|
|
3
|
+
export declare function lexStringify(input: LexValue): string;
|
|
4
|
+
export type LexParseOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* Forbids the presence of invalid Lex values (e.g. non-integer numbers,
|
|
7
|
+
* malformed $link, $bytes, blob objects, etc.)
|
|
8
|
+
*/
|
|
9
|
+
strict?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function lexParse(input: string, options?: LexParseOptions): LexValue;
|
|
12
|
+
export declare function jsonToLex(value: JsonValue, options?: LexParseOptions): LexValue;
|
|
13
|
+
export declare function lexToJson(value: LexValue): JsonValue;
|
|
14
|
+
//# sourceMappingURL=lex-json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lex-json.d.ts","sourceRoot":"","sources":["../src/lex-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,QAAQ,EAET,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAc,SAAS,EAAE,MAAM,WAAW,CAAA;AAGjD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAKpD;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,wBAAgB,QAAQ,CACtB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,eAAmC,GAC3C,QAAQ,CAiBV;AAED,wBAAgB,SAAS,CACvB,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,eAAmC,GAC3C,QAAQ,CAsBV;AA+CD,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAqBpD"}
|
package/dist/lex-json.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.lexStringify = lexStringify;
|
|
4
|
+
exports.lexParse = lexParse;
|
|
5
|
+
exports.jsonToLex = jsonToLex;
|
|
6
|
+
exports.lexToJson = lexToJson;
|
|
7
|
+
const lex_data_1 = require("@atproto/lex-data");
|
|
8
|
+
const blob_js_1 = require("./blob.js");
|
|
9
|
+
const bytes_js_1 = require("./bytes.js");
|
|
10
|
+
const link_js_1 = require("./link.js");
|
|
11
|
+
function lexStringify(input) {
|
|
12
|
+
// @NOTE Because of the way the "replacer" works in JSON.stringify, it's
|
|
13
|
+
// simpler to convert Lex to JSON first rather than trying to do it
|
|
14
|
+
// on-the-fly.
|
|
15
|
+
return JSON.stringify(lexToJson(input));
|
|
16
|
+
}
|
|
17
|
+
function lexParse(input, options = { strict: false }) {
|
|
18
|
+
return JSON.parse(input, function (key, value) {
|
|
19
|
+
switch (typeof value) {
|
|
20
|
+
case 'object':
|
|
21
|
+
if (value === null)
|
|
22
|
+
return null;
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return value;
|
|
25
|
+
return parseSpecialJsonObject(value, options) ?? value;
|
|
26
|
+
case 'number':
|
|
27
|
+
if (Number.isInteger(value))
|
|
28
|
+
return value;
|
|
29
|
+
if (options.strict) {
|
|
30
|
+
throw new TypeError(`Invalid non-integer number: ${value}`);
|
|
31
|
+
}
|
|
32
|
+
// fallthrough
|
|
33
|
+
default:
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function jsonToLex(value, options = { strict: false }) {
|
|
39
|
+
switch (typeof value) {
|
|
40
|
+
case 'object': {
|
|
41
|
+
if (value === null)
|
|
42
|
+
return null;
|
|
43
|
+
if (Array.isArray(value))
|
|
44
|
+
return jsonArrayToLex(value, options);
|
|
45
|
+
return (parseSpecialJsonObject(value, options) ??
|
|
46
|
+
jsonObjectToLexMap(value, options));
|
|
47
|
+
}
|
|
48
|
+
case 'number':
|
|
49
|
+
if (Number.isInteger(value))
|
|
50
|
+
return value;
|
|
51
|
+
if (options.strict) {
|
|
52
|
+
throw new TypeError(`Invalid non-integer number: ${value}`);
|
|
53
|
+
}
|
|
54
|
+
// fallthrough
|
|
55
|
+
case 'boolean':
|
|
56
|
+
case 'string':
|
|
57
|
+
return value;
|
|
58
|
+
default:
|
|
59
|
+
throw new TypeError(`Invalid JSON value: ${typeof value}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function jsonArrayToLex(input, options) {
|
|
63
|
+
// Lazily copy value
|
|
64
|
+
let copy;
|
|
65
|
+
for (let i = 0; i < input.length; i++) {
|
|
66
|
+
const inputItem = input[i];
|
|
67
|
+
const item = jsonToLex(inputItem, options);
|
|
68
|
+
if (item !== inputItem) {
|
|
69
|
+
copy ?? (copy = Array.from(input));
|
|
70
|
+
copy[i] = item;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return copy ?? input;
|
|
74
|
+
}
|
|
75
|
+
function jsonObjectToLexMap(input, options) {
|
|
76
|
+
// Lazily copy value
|
|
77
|
+
let copy = undefined;
|
|
78
|
+
for (const [key, jsonValue] of Object.entries(input)) {
|
|
79
|
+
// Prevent prototype pollution
|
|
80
|
+
if (key === '__proto__') {
|
|
81
|
+
throw new TypeError('Invalid key: __proto__');
|
|
82
|
+
}
|
|
83
|
+
// Ignore (strip) undefined values
|
|
84
|
+
if (jsonValue === undefined) {
|
|
85
|
+
copy ?? (copy = { ...input });
|
|
86
|
+
delete copy[key];
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const value = jsonToLex(jsonValue, options);
|
|
90
|
+
if (value !== jsonValue) {
|
|
91
|
+
copy ?? (copy = { ...input });
|
|
92
|
+
copy[key] = value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return copy ?? input;
|
|
96
|
+
}
|
|
97
|
+
function lexToJson(value) {
|
|
98
|
+
switch (typeof value) {
|
|
99
|
+
case 'object':
|
|
100
|
+
if (value === null) {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
else if (Array.isArray(value)) {
|
|
104
|
+
return lexArrayToJson(value);
|
|
105
|
+
}
|
|
106
|
+
else if ((0, lex_data_1.isCid)(value)) {
|
|
107
|
+
return (0, link_js_1.encodeLexLink)(value);
|
|
108
|
+
}
|
|
109
|
+
else if (value instanceof Uint8Array) {
|
|
110
|
+
return (0, bytes_js_1.encodeLexBytes)(value);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
return encodeLexMap(value);
|
|
114
|
+
}
|
|
115
|
+
case 'boolean':
|
|
116
|
+
case 'string':
|
|
117
|
+
case 'number':
|
|
118
|
+
return value;
|
|
119
|
+
default:
|
|
120
|
+
throw new TypeError(`Invalid Lex value: ${typeof value}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function lexArrayToJson(input) {
|
|
124
|
+
// Lazily copy value
|
|
125
|
+
let copy;
|
|
126
|
+
for (let i = 0; i < input.length; i++) {
|
|
127
|
+
const inputItem = input[i];
|
|
128
|
+
const item = lexToJson(inputItem);
|
|
129
|
+
if (item !== inputItem) {
|
|
130
|
+
copy ?? (copy = Array.from(input));
|
|
131
|
+
copy[i] = item;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return copy ?? input;
|
|
135
|
+
}
|
|
136
|
+
function encodeLexMap(input) {
|
|
137
|
+
// Lazily copy value
|
|
138
|
+
let copy = undefined;
|
|
139
|
+
for (const [key, lexValue] of Object.entries(input)) {
|
|
140
|
+
// Prevent prototype pollution
|
|
141
|
+
if (key === '__proto__') {
|
|
142
|
+
throw new TypeError('Invalid key: __proto__');
|
|
143
|
+
}
|
|
144
|
+
// Ignore (strip) undefined values
|
|
145
|
+
if (lexValue === undefined) {
|
|
146
|
+
copy ?? (copy = { ...input });
|
|
147
|
+
delete copy[key];
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const jsonValue = lexToJson(lexValue);
|
|
151
|
+
if (jsonValue !== lexValue) {
|
|
152
|
+
copy ?? (copy = { ...input });
|
|
153
|
+
copy[key] = jsonValue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return copy ?? input;
|
|
157
|
+
}
|
|
158
|
+
function parseSpecialJsonObject(input, options) {
|
|
159
|
+
// Hot path: use hints to avoid parsing when possible
|
|
160
|
+
if (input.$link !== undefined) {
|
|
161
|
+
const cid = (0, link_js_1.parseLexLink)(input);
|
|
162
|
+
if (cid)
|
|
163
|
+
return cid;
|
|
164
|
+
if (options.strict)
|
|
165
|
+
throw new TypeError(`Invalid $link object`);
|
|
166
|
+
}
|
|
167
|
+
else if (input.$bytes !== undefined) {
|
|
168
|
+
const bytes = (0, bytes_js_1.parseLexBytes)(input);
|
|
169
|
+
if (bytes)
|
|
170
|
+
return bytes;
|
|
171
|
+
if (options.strict)
|
|
172
|
+
throw new TypeError(`Invalid $bytes object`);
|
|
173
|
+
}
|
|
174
|
+
else if (input.$type !== undefined) {
|
|
175
|
+
// @NOTE Since blobs are "just" regular lex objects with a special shape,
|
|
176
|
+
// and because an object that does not conform to the blob shape would still
|
|
177
|
+
// result in undefined being returned, we only attempt to parse blobs when
|
|
178
|
+
// the strict option is enabled.
|
|
179
|
+
if (options.strict) {
|
|
180
|
+
if (input.$type === 'blob') {
|
|
181
|
+
const blob = (0, blob_js_1.parseBlobRef)(input, options);
|
|
182
|
+
if (blob)
|
|
183
|
+
return blob;
|
|
184
|
+
throw new TypeError(`Invalid blob object`);
|
|
185
|
+
}
|
|
186
|
+
else if (typeof input.$type !== 'string') {
|
|
187
|
+
throw new TypeError(`Invalid $type property (${typeof input.$type})`);
|
|
188
|
+
}
|
|
189
|
+
else if (input.$type.length === 0) {
|
|
190
|
+
throw new TypeError(`Empty $type property`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// @NOTE We ignore legacy blob representation here. They can be handled at the
|
|
195
|
+
// application level if needed.
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=lex-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lex-json.js","sourceRoot":"","sources":["../src/lex-json.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BlobRef,\n CID,\n LexArray,\n LexMap,\n LexValue,\n isCid,\n} from '@atproto/lex-data'\nimport { parseBlobRef } from './blob.js'\nimport { encodeLexBytes, parseLexBytes } from './bytes.js'\nimport { JsonObject, JsonValue } from './json.js'\nimport { encodeLexLink, parseLexLink } from './link.js'\n\nexport function lexStringify(input: LexValue): string {\n // @NOTE Because of the way the \"replacer\" works in JSON.stringify, it's\n // simpler to convert Lex to JSON first rather than trying to do it\n // on-the-fly.\n return JSON.stringify(lexToJson(input))\n}\n\nexport type LexParseOptions = {\n /**\n * Forbids the presence of invalid Lex values (e.g. non-integer numbers,\n * malformed $link, $bytes, blob objects, etc.)\n */\n strict?: boolean\n}\n\nexport function lexParse(\n input: string,\n options: LexParseOptions = { strict: false },\n): LexValue {\n return JSON.parse(input, function (key: string, value: JsonValue): LexValue {\n switch (typeof value) {\n case 'object':\n if (value === null) return null\n if (Array.isArray(value)) return value\n return parseSpecialJsonObject(value, options) ?? value\n case 'number':\n if (Number.isInteger(value)) return value\n if (options.strict) {\n throw new TypeError(`Invalid non-integer number: ${value}`)\n }\n // fallthrough\n default:\n return value\n }\n })\n}\n\nexport function jsonToLex(\n value: JsonValue,\n options: LexParseOptions = { strict: false },\n): LexValue {\n switch (typeof value) {\n case 'object': {\n if (value === null) return null\n if (Array.isArray(value)) return jsonArrayToLex(value, options)\n return (\n parseSpecialJsonObject(value, options) ??\n jsonObjectToLexMap(value, options)\n )\n }\n case 'number':\n if (Number.isInteger(value)) return value\n if (options.strict) {\n throw new TypeError(`Invalid non-integer number: ${value}`)\n }\n // fallthrough\n case 'boolean':\n case 'string':\n return value\n default:\n throw new TypeError(`Invalid JSON value: ${typeof value}`)\n }\n}\n\nfunction jsonArrayToLex(\n input: JsonValue[],\n options: LexParseOptions,\n): LexValue[] {\n // Lazily copy value\n let copy: LexValue[] | undefined\n for (let i = 0; i < input.length; i++) {\n const inputItem = input[i]\n const item = jsonToLex(inputItem, options)\n if (item !== inputItem) {\n copy ??= Array.from(input)\n copy[i] = item\n }\n }\n return copy ?? input\n}\n\nfunction jsonObjectToLexMap(\n input: JsonObject,\n options: LexParseOptions,\n): LexMap {\n // Lazily copy value\n let copy: LexMap | undefined = undefined\n for (const [key, jsonValue] of Object.entries(input)) {\n // Prevent prototype pollution\n if (key === '__proto__') {\n throw new TypeError('Invalid key: __proto__')\n }\n\n // Ignore (strip) undefined values\n if (jsonValue === undefined) {\n copy ??= { ...input }\n delete copy[key]\n continue\n }\n\n const value = jsonToLex(jsonValue!, options)\n if (value !== jsonValue) {\n copy ??= { ...input }\n copy[key] = value\n }\n }\n return copy ?? input\n}\n\nexport function lexToJson(value: LexValue): JsonValue {\n switch (typeof value) {\n case 'object':\n if (value === null) {\n return value\n } else if (Array.isArray(value)) {\n return lexArrayToJson(value)\n } else if (isCid(value)) {\n return encodeLexLink(value)\n } else if (value instanceof Uint8Array) {\n return encodeLexBytes(value)\n } else {\n return encodeLexMap(value)\n }\n case 'boolean':\n case 'string':\n case 'number':\n return value\n default:\n throw new TypeError(`Invalid Lex value: ${typeof value}`)\n }\n}\n\nfunction lexArrayToJson(input: LexArray): JsonValue[] {\n // Lazily copy value\n let copy: JsonValue[] | undefined\n for (let i = 0; i < input.length; i++) {\n const inputItem = input[i]\n const item = lexToJson(inputItem)\n if (item !== inputItem) {\n copy ??= Array.from(input) as JsonValue[]\n copy[i] = item\n }\n }\n return copy ?? (input as JsonValue[])\n}\n\nfunction encodeLexMap(input: LexMap): JsonObject {\n // Lazily copy value\n let copy: JsonObject | undefined = undefined\n for (const [key, lexValue] of Object.entries(input)) {\n // Prevent prototype pollution\n if (key === '__proto__') {\n throw new TypeError('Invalid key: __proto__')\n }\n\n // Ignore (strip) undefined values\n if (lexValue === undefined) {\n copy ??= { ...input } as JsonObject\n delete copy[key]\n continue\n }\n\n const jsonValue = lexToJson(lexValue!)\n if (jsonValue !== lexValue) {\n copy ??= { ...input } as JsonObject\n copy[key] = jsonValue\n }\n }\n return copy ?? (input as JsonObject)\n}\n\nfunction parseSpecialJsonObject(\n input: LexMap,\n options: LexParseOptions,\n): CID | Uint8Array | BlobRef | undefined {\n // Hot path: use hints to avoid parsing when possible\n\n if (input.$link !== undefined) {\n const cid = parseLexLink(input)\n if (cid) return cid\n if (options.strict) throw new TypeError(`Invalid $link object`)\n } else if (input.$bytes !== undefined) {\n const bytes = parseLexBytes(input)\n if (bytes) return bytes\n if (options.strict) throw new TypeError(`Invalid $bytes object`)\n } else if (input.$type !== undefined) {\n // @NOTE Since blobs are \"just\" regular lex objects with a special shape,\n // and because an object that does not conform to the blob shape would still\n // result in undefined being returned, we only attempt to parse blobs when\n // the strict option is enabled.\n if (options.strict) {\n if (input.$type === 'blob') {\n const blob = parseBlobRef(input, options)\n if (blob) return blob\n throw new TypeError(`Invalid blob object`)\n } else if (typeof input.$type !== 'string') {\n throw new TypeError(`Invalid $type property (${typeof input.$type})`)\n } else if (input.$type.length === 0) {\n throw new TypeError(`Empty $type property`)\n }\n }\n }\n\n // @NOTE We ignore legacy blob representation here. They can be handled at the\n // application level if needed.\n\n return undefined\n}\n"]}
|
package/dist/link.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CID } from '@atproto/lex-data';
|
|
2
|
+
import { JsonValue } from './json.js';
|
|
3
|
+
export declare function parseLexLink(input?: {
|
|
4
|
+
$link?: unknown;
|
|
5
|
+
}): CID | undefined;
|
|
6
|
+
export declare function encodeLexLink(cid: CID): JsonValue;
|
|
7
|
+
//# sourceMappingURL=link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,GAAG,GAAG,SAAS,CA6BzE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,SAAS,CAEjD"}
|
package/dist/link.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseLexLink = parseLexLink;
|
|
4
|
+
exports.encodeLexLink = encodeLexLink;
|
|
5
|
+
const lex_data_1 = require("@atproto/lex-data");
|
|
6
|
+
function parseLexLink(input) {
|
|
7
|
+
if (!input || !('$link' in input)) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
for (const key in input) {
|
|
11
|
+
if (key !== '$link') {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (typeof input.$link !== 'string') {
|
|
16
|
+
throw new TypeError('$link must be a base32-encoded CID string');
|
|
17
|
+
}
|
|
18
|
+
if (input.$link.length === 0) {
|
|
19
|
+
throw new TypeError('CID string in $link cannot be empty');
|
|
20
|
+
}
|
|
21
|
+
// Arbitrary limit to prevent DoS via extremely long CIDs
|
|
22
|
+
if (input.$link.length > 2048) {
|
|
23
|
+
throw new TypeError('CID string in $link is too long');
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return lex_data_1.CID.parse(input.$link);
|
|
27
|
+
}
|
|
28
|
+
catch (cause) {
|
|
29
|
+
throw new TypeError('Invalid CID string in $link', { cause });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function encodeLexLink(cid) {
|
|
33
|
+
return { $link: cid.toString() };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=link.js.map
|
package/dist/link.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.js","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":";;AAGA,oCA6BC;AAED,sCAEC;AApCD,gDAAuC;AAGvC,SAAgB,YAAY,CAAC,KAA2B;IACtD,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,OAAO,IAAI,KAAK,CAAC,EAAE,CAAC;QAClC,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;YACpB,OAAO,SAAS,CAAA;QAClB,CAAC;IACH,CAAC;IAED,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,SAAS,CAAC,2CAA2C,CAAC,CAAA;IAClE,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,SAAS,CAAC,qCAAqC,CAAC,CAAA;IAC5D,CAAC;IAED,yDAAyD;IACzD,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QAC9B,MAAM,IAAI,SAAS,CAAC,iCAAiC,CAAC,CAAA;IACxD,CAAC;IAED,IAAI,CAAC;QACH,OAAO,cAAG,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,SAAS,CAAC,6BAA6B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IAC/D,CAAC;AACH,CAAC;AAED,SAAgB,aAAa,CAAC,GAAQ;IACpC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAA;AAClC,CAAC","sourcesContent":["import { CID } from '@atproto/lex-data'\nimport { JsonValue } from './json.js'\n\nexport function parseLexLink(input?: { $link?: unknown }): CID | undefined {\n if (!input || !('$link' in input)) {\n return undefined\n }\n\n for (const key in input) {\n if (key !== '$link') {\n return undefined\n }\n }\n\n if (typeof input.$link !== 'string') {\n throw new TypeError('$link must be a base32-encoded CID string')\n }\n\n if (input.$link.length === 0) {\n throw new TypeError('CID string in $link cannot be empty')\n }\n\n // Arbitrary limit to prevent DoS via extremely long CIDs\n if (input.$link.length > 2048) {\n throw new TypeError('CID string in $link is too long')\n }\n\n try {\n return CID.parse(input.$link)\n } catch (cause) {\n throw new TypeError('Invalid CID string in $link', { cause })\n }\n}\n\nexport function encodeLexLink(cid: CID): JsonValue {\n return { $link: cid.toString() }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atproto/lex-json",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Lexicon encoding utilities for AT Lexicon data in JSON format",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"atproto",
|
|
8
|
+
"lex",
|
|
9
|
+
"data",
|
|
10
|
+
"json",
|
|
11
|
+
"encoding",
|
|
12
|
+
"utilities"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://atproto.com",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/bluesky-social/atproto",
|
|
18
|
+
"directory": "packages/lex/lex-json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"./src",
|
|
22
|
+
"./dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"browser": "./dist/index.js",
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"require": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@atproto/lex-data": "workspace:*",
|
|
38
|
+
"tslib": "^2.8.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"jest": "^28.1.2"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc --build tsconfig.build.json",
|
|
45
|
+
"test": "jest"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/blob.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BlobRef, LexMap, isBlobRef } from '@atproto/lex-data'
|
|
2
|
+
import { parseLexLink } from './link.js'
|
|
3
|
+
|
|
4
|
+
export function parseBlobRef(
|
|
5
|
+
input: LexMap,
|
|
6
|
+
options?: { strict?: boolean },
|
|
7
|
+
): BlobRef | undefined {
|
|
8
|
+
if (input.$type !== 'blob') return undefined
|
|
9
|
+
|
|
10
|
+
const ref = input?.ref
|
|
11
|
+
if (!ref || typeof ref !== 'object') return undefined
|
|
12
|
+
|
|
13
|
+
// @NOTE Because json to lex conversion can be performed both in a depth-first
|
|
14
|
+
// manner (e.g. via lexParse) or in a breadth-first manner (e.g. via
|
|
15
|
+
// jsonToLex), the `ref` property may either be a LexMap with a $link
|
|
16
|
+
// property, or it may already be a CID instance.
|
|
17
|
+
|
|
18
|
+
if ('$link' in ref) {
|
|
19
|
+
const cid = parseLexLink(ref)
|
|
20
|
+
if (!cid) return undefined
|
|
21
|
+
|
|
22
|
+
const blob = { ...input, ref: cid }
|
|
23
|
+
if (isBlobRef(blob, options)) return blob
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (isBlobRef(input)) {
|
|
27
|
+
return input
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
package/src/bytes.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { fromBase64, toBase64 } from '@atproto/lex-data'
|
|
2
|
+
import { JsonValue } from './json.js'
|
|
3
|
+
|
|
4
|
+
export function parseLexBytes(input?: {
|
|
5
|
+
$bytes?: unknown
|
|
6
|
+
}): Uint8Array | undefined {
|
|
7
|
+
if (!input || !('$bytes' in input)) {
|
|
8
|
+
return undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
for (const key in input) {
|
|
12
|
+
if (key !== '$bytes') {
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof input.$bytes !== 'string') {
|
|
18
|
+
throw new TypeError('$bytes must be a base64-encoded string')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return fromBase64(input.$bytes)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function encodeLexBytes(bytes: Uint8Array): JsonValue {
|
|
25
|
+
return { $bytes: toBase64(bytes) }
|
|
26
|
+
}
|
package/src/index.ts
ADDED
package/src/json.ts
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { CID, LexValue, lexEquals } from '@atproto/lex-data'
|
|
2
|
+
import { JsonValue } from './json.js'
|
|
3
|
+
import { jsonToLex, lexParse, lexStringify, lexToJson } from './lex-json.js'
|
|
4
|
+
|
|
5
|
+
export const validVectors: Array<{
|
|
6
|
+
name: string
|
|
7
|
+
json: JsonValue
|
|
8
|
+
lex: LexValue
|
|
9
|
+
}> = [
|
|
10
|
+
{
|
|
11
|
+
name: 'pure json',
|
|
12
|
+
json: {
|
|
13
|
+
string: 'abc',
|
|
14
|
+
unicode: 'a~öñ©⽘☎𓋓😀👨👩👧👧',
|
|
15
|
+
integer: 123,
|
|
16
|
+
bool: true,
|
|
17
|
+
null: null,
|
|
18
|
+
array: ['abc', 'def', 'ghi'],
|
|
19
|
+
object: {
|
|
20
|
+
string: 'abc',
|
|
21
|
+
number: 123,
|
|
22
|
+
bool: true,
|
|
23
|
+
arr: ['abc', 'def', 'ghi'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
lex: {
|
|
27
|
+
string: 'abc',
|
|
28
|
+
unicode: 'a~öñ©⽘☎𓋓😀👨👩👧👧',
|
|
29
|
+
integer: 123,
|
|
30
|
+
bool: true,
|
|
31
|
+
null: null,
|
|
32
|
+
array: ['abc', 'def', 'ghi'],
|
|
33
|
+
object: {
|
|
34
|
+
string: 'abc',
|
|
35
|
+
number: 123,
|
|
36
|
+
bool: true,
|
|
37
|
+
arr: ['abc', 'def', 'ghi'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'lex data',
|
|
43
|
+
json: {
|
|
44
|
+
a: {
|
|
45
|
+
$link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
46
|
+
},
|
|
47
|
+
b: {
|
|
48
|
+
$bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
|
|
49
|
+
},
|
|
50
|
+
c: {
|
|
51
|
+
$type: 'blob',
|
|
52
|
+
ref: {
|
|
53
|
+
$link: 'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',
|
|
54
|
+
},
|
|
55
|
+
mimeType: 'image/jpeg',
|
|
56
|
+
size: 10000,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
lex: {
|
|
60
|
+
a: CID.parse(
|
|
61
|
+
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
62
|
+
),
|
|
63
|
+
b: new Uint8Array([
|
|
64
|
+
156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253,
|
|
65
|
+
65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194,
|
|
66
|
+
204, 141,
|
|
67
|
+
]),
|
|
68
|
+
c: {
|
|
69
|
+
$type: 'blob',
|
|
70
|
+
ref: CID.parse(
|
|
71
|
+
'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',
|
|
72
|
+
),
|
|
73
|
+
mimeType: 'image/jpeg',
|
|
74
|
+
size: 10000,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'lexArray',
|
|
80
|
+
json: [
|
|
81
|
+
{
|
|
82
|
+
$link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
$link: 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
$link: 'bafyreiaizynclnqiolq7byfpjjtgqzn4sfrsgn7z2hhf6bo4utdwkin7ke',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
$link: 'bafyreifd4w4tcr5tluxz7osjtnofffvtsmgdqcfrfi6evjde4pl27lrjpy',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
lex: [
|
|
95
|
+
CID.parse('bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a'),
|
|
96
|
+
CID.parse('bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q'),
|
|
97
|
+
CID.parse('bafyreiaizynclnqiolq7byfpjjtgqzn4sfrsgn7z2hhf6bo4utdwkin7ke'),
|
|
98
|
+
CID.parse('bafyreifd4w4tcr5tluxz7osjtnofffvtsmgdqcfrfi6evjde4pl27lrjpy'),
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'root cid',
|
|
103
|
+
json: {
|
|
104
|
+
$link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
105
|
+
},
|
|
106
|
+
lex: CID.parse(
|
|
107
|
+
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'root bytes',
|
|
112
|
+
json: {
|
|
113
|
+
$bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
|
|
114
|
+
},
|
|
115
|
+
lex: new Uint8Array([
|
|
116
|
+
156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253, 65,
|
|
117
|
+
60, 242, 11, 98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194, 204,
|
|
118
|
+
141,
|
|
119
|
+
]),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'lexNested',
|
|
123
|
+
json: {
|
|
124
|
+
a: {
|
|
125
|
+
b: [
|
|
126
|
+
{
|
|
127
|
+
d: [
|
|
128
|
+
{
|
|
129
|
+
$link:
|
|
130
|
+
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
$link:
|
|
134
|
+
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
e: [
|
|
138
|
+
{
|
|
139
|
+
$bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
$bytes: 'iE+sPoHobU9tSIqGI+309LLCcWQIRmEXwxcoDt19tas',
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
lex: {
|
|
150
|
+
a: {
|
|
151
|
+
b: [
|
|
152
|
+
{
|
|
153
|
+
d: [
|
|
154
|
+
CID.parse(
|
|
155
|
+
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
156
|
+
),
|
|
157
|
+
CID.parse(
|
|
158
|
+
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
159
|
+
),
|
|
160
|
+
],
|
|
161
|
+
e: [
|
|
162
|
+
new Uint8Array([
|
|
163
|
+
156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174,
|
|
164
|
+
161, 253, 65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238,
|
|
165
|
+
190, 176, 26, 194, 204, 141,
|
|
166
|
+
]),
|
|
167
|
+
new Uint8Array([
|
|
168
|
+
136, 79, 172, 62, 129, 232, 109, 79, 109, 72, 138, 134, 35, 237,
|
|
169
|
+
244, 244, 178, 194, 113, 100, 8, 70, 97, 23, 195, 23, 40, 14,
|
|
170
|
+
221, 125, 181, 171,
|
|
171
|
+
]),
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'empty structures',
|
|
180
|
+
json: {
|
|
181
|
+
emptyObject: {},
|
|
182
|
+
emptyArray: [],
|
|
183
|
+
emtyBytes: { $bytes: '' },
|
|
184
|
+
},
|
|
185
|
+
lex: {
|
|
186
|
+
emptyObject: {},
|
|
187
|
+
emptyArray: [],
|
|
188
|
+
emtyBytes: new Uint8Array([]),
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'mixed types in array',
|
|
193
|
+
json: {
|
|
194
|
+
arr: [
|
|
195
|
+
'string',
|
|
196
|
+
123,
|
|
197
|
+
true,
|
|
198
|
+
null,
|
|
199
|
+
{
|
|
200
|
+
$link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
201
|
+
},
|
|
202
|
+
{ $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0' },
|
|
203
|
+
{ nested: 'object' },
|
|
204
|
+
['nested', 'array'],
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
lex: {
|
|
208
|
+
arr: [
|
|
209
|
+
'string',
|
|
210
|
+
123,
|
|
211
|
+
true,
|
|
212
|
+
null,
|
|
213
|
+
CID.parse(
|
|
214
|
+
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
215
|
+
),
|
|
216
|
+
new Uint8Array([
|
|
217
|
+
156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253,
|
|
218
|
+
65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194,
|
|
219
|
+
204, 141,
|
|
220
|
+
]),
|
|
221
|
+
{ nested: 'object' },
|
|
222
|
+
['nested', 'array'],
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "mismatched order in object doesn't affect equality",
|
|
228
|
+
json: {
|
|
229
|
+
a: 'valueA',
|
|
230
|
+
b: 'valueB',
|
|
231
|
+
c: {
|
|
232
|
+
$link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
233
|
+
},
|
|
234
|
+
d: {
|
|
235
|
+
a: 'valueA',
|
|
236
|
+
b: 'valueB',
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
lex: {
|
|
240
|
+
c: CID.parse(
|
|
241
|
+
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
|
|
242
|
+
),
|
|
243
|
+
d: {
|
|
244
|
+
b: 'valueB',
|
|
245
|
+
a: 'valueA',
|
|
246
|
+
},
|
|
247
|
+
a: 'valueA',
|
|
248
|
+
b: 'valueB',
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
export const acceptableVectors: Array<{
|
|
254
|
+
note: string
|
|
255
|
+
json: JsonValue
|
|
256
|
+
}> = [
|
|
257
|
+
{
|
|
258
|
+
note: 'non string $type',
|
|
259
|
+
json: {
|
|
260
|
+
$type: 3124,
|
|
261
|
+
foo: 'bar',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
note: 'object with float values',
|
|
266
|
+
json: {
|
|
267
|
+
a: 1.5,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
note: 'blob with wrong field type',
|
|
272
|
+
json: {
|
|
273
|
+
$type: 'blob',
|
|
274
|
+
ref: 'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',
|
|
275
|
+
mimeType: 'image/jpeg',
|
|
276
|
+
size: 10000,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
note: 'blob with missing key',
|
|
281
|
+
json: {
|
|
282
|
+
$type: 'blob',
|
|
283
|
+
mimeType: 'image/jpeg',
|
|
284
|
+
size: 10000,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
note: 'blob with extra fields',
|
|
289
|
+
json: {
|
|
290
|
+
$type: 'blob',
|
|
291
|
+
ref: {
|
|
292
|
+
$link: 'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',
|
|
293
|
+
},
|
|
294
|
+
mimeType: 'image/jpeg',
|
|
295
|
+
size: 10000,
|
|
296
|
+
other: 'blah',
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
note: 'bytes with extra fields',
|
|
301
|
+
json: {
|
|
302
|
+
$bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
|
|
303
|
+
other: 'blah',
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
note: 'link with extra fields',
|
|
308
|
+
json: {
|
|
309
|
+
$link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',
|
|
310
|
+
other: 'blah',
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
note: '$bytes and $link',
|
|
315
|
+
json: {
|
|
316
|
+
$bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
|
|
317
|
+
$link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
note: '$bytes and $type',
|
|
322
|
+
json: {
|
|
323
|
+
$type: 'bytes',
|
|
324
|
+
$bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
note: '$link and $type',
|
|
329
|
+
json: {
|
|
330
|
+
$type: 'blob',
|
|
331
|
+
$link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
export const invalidVectors: Array<{
|
|
337
|
+
note: string
|
|
338
|
+
json: JsonValue
|
|
339
|
+
}> = [
|
|
340
|
+
{
|
|
341
|
+
note: 'bytes with wrong field type',
|
|
342
|
+
json: {
|
|
343
|
+
$bytes: [1, 2, 3],
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
note: 'invalid base64 in $bytes',
|
|
348
|
+
json: {
|
|
349
|
+
$bytes: '🐻',
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
note: 'link with wrong field type',
|
|
354
|
+
json: {
|
|
355
|
+
$link: 1234,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
note: 'link with bogus CID',
|
|
360
|
+
json: {
|
|
361
|
+
$link: '.',
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
describe('lexParse', () => {
|
|
367
|
+
describe('valid vectors', () => {
|
|
368
|
+
for (const { name, json, lex } of validVectors) {
|
|
369
|
+
describe(name, () => {
|
|
370
|
+
it('parses lex from string', () => {
|
|
371
|
+
expect(
|
|
372
|
+
lexEquals(lex, lexParse(JSON.stringify(json), { strict: false })),
|
|
373
|
+
).toBe(true)
|
|
374
|
+
expect(
|
|
375
|
+
lexEquals(lex, lexParse(JSON.stringify(json), { strict: true })),
|
|
376
|
+
).toBe(true)
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
describe('acceptable vectors', () => {
|
|
383
|
+
for (const { note, json } of acceptableVectors) {
|
|
384
|
+
describe(note, () => {
|
|
385
|
+
it('parses lex from string in non-strict mode', () => {
|
|
386
|
+
expect(() =>
|
|
387
|
+
lexParse(JSON.stringify(json), { strict: false }),
|
|
388
|
+
).not.toThrow()
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('parses lex from string in strict mode', () => {
|
|
392
|
+
expect(() =>
|
|
393
|
+
lexParse(JSON.stringify(json), { strict: true }),
|
|
394
|
+
).toThrow()
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
describe('invalid vectors', () => {
|
|
401
|
+
for (const { note, json } of invalidVectors) {
|
|
402
|
+
describe(note, () => {
|
|
403
|
+
it('throws when parsing malformed JSON', () => {
|
|
404
|
+
expect(() =>
|
|
405
|
+
lexParse(JSON.stringify(json), { strict: false }),
|
|
406
|
+
).toThrow()
|
|
407
|
+
expect(() =>
|
|
408
|
+
lexParse(JSON.stringify(json), { strict: true }),
|
|
409
|
+
).toThrow()
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
describe('lexStringify', () => {
|
|
417
|
+
describe('valid vectors', () => {
|
|
418
|
+
for (const { name, json, lex } of validVectors) {
|
|
419
|
+
describe(name, () => {
|
|
420
|
+
it('stringifies lex to string', () => {
|
|
421
|
+
expect(JSON.parse(lexStringify(lex))).toEqual(json)
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
describe('jsonToLex', () => {
|
|
429
|
+
describe('valid vectors', () => {
|
|
430
|
+
for (const { name, json, lex } of validVectors) {
|
|
431
|
+
describe(name, () => {
|
|
432
|
+
it('converts json to lex (in strict mode)', () => {
|
|
433
|
+
expect(lexEquals(jsonToLex(json, { strict: true }), lex)).toBe(true)
|
|
434
|
+
expect(lexEquals(lex, jsonToLex(json, { strict: true }))).toBe(true)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('converts json to lex (in non-strict mode)', () => {
|
|
438
|
+
expect(lexEquals(jsonToLex(json, { strict: false }), lex)).toBe(true)
|
|
439
|
+
expect(lexEquals(lex, jsonToLex(json, { strict: false }))).toBe(true)
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
describe('acceptable vectors', () => {
|
|
446
|
+
for (const { note, json } of acceptableVectors) {
|
|
447
|
+
describe(note, () => {
|
|
448
|
+
it('parses lex from json in strict mode', () => {
|
|
449
|
+
expect(() => jsonToLex(json, { strict: true })).toThrow()
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('parses lex from json in non-strict mode', () => {
|
|
453
|
+
expect(() => jsonToLex(json, { strict: false })).not.toThrow()
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
describe('invalid vectors', () => {
|
|
460
|
+
for (const { note, json } of invalidVectors) {
|
|
461
|
+
describe(note, () => {
|
|
462
|
+
it(`throws for malformed object`, () => {
|
|
463
|
+
expect(() => jsonToLex(json, { strict: true })).toThrow()
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('throws for nested malformed object', () => {
|
|
467
|
+
expect(() => jsonToLex({ nested: json }, { strict: true })).toThrow()
|
|
468
|
+
expect(() => jsonToLex([json], { strict: true })).toThrow()
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
describe('lexToJson', () => {
|
|
476
|
+
describe('valid vectors', () => {
|
|
477
|
+
for (const { name, json, lex } of validVectors) {
|
|
478
|
+
describe(name, () => {
|
|
479
|
+
it('converts lex to json', () => {
|
|
480
|
+
expect(lexToJson(lex)).toEqual(json)
|
|
481
|
+
expect(lexToJson(lex)).toEqual(json)
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
describe('json > lex > json', () => {
|
|
489
|
+
describe('valid vectors', () => {
|
|
490
|
+
for (const { name, json } of validVectors) {
|
|
491
|
+
describe(name, () => {
|
|
492
|
+
it('converts json to lex', () => {
|
|
493
|
+
expect(lexToJson(jsonToLex(json))).toEqual(json)
|
|
494
|
+
})
|
|
495
|
+
})
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
describe('lex > json > lex', () => {
|
|
501
|
+
describe('valid vectors', () => {
|
|
502
|
+
for (const { name, lex } of validVectors) {
|
|
503
|
+
describe(name, () => {
|
|
504
|
+
it('converts lex to json', () => {
|
|
505
|
+
expect(lexEquals(jsonToLex(lexToJson(lex)), lex)).toBe(true)
|
|
506
|
+
expect(lexEquals(lex, jsonToLex(lexToJson(lex)))).toBe(true)
|
|
507
|
+
})
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
})
|
package/src/lex-json.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlobRef,
|
|
3
|
+
CID,
|
|
4
|
+
LexArray,
|
|
5
|
+
LexMap,
|
|
6
|
+
LexValue,
|
|
7
|
+
isCid,
|
|
8
|
+
} from '@atproto/lex-data'
|
|
9
|
+
import { parseBlobRef } from './blob.js'
|
|
10
|
+
import { encodeLexBytes, parseLexBytes } from './bytes.js'
|
|
11
|
+
import { JsonObject, JsonValue } from './json.js'
|
|
12
|
+
import { encodeLexLink, parseLexLink } from './link.js'
|
|
13
|
+
|
|
14
|
+
export function lexStringify(input: LexValue): string {
|
|
15
|
+
// @NOTE Because of the way the "replacer" works in JSON.stringify, it's
|
|
16
|
+
// simpler to convert Lex to JSON first rather than trying to do it
|
|
17
|
+
// on-the-fly.
|
|
18
|
+
return JSON.stringify(lexToJson(input))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type LexParseOptions = {
|
|
22
|
+
/**
|
|
23
|
+
* Forbids the presence of invalid Lex values (e.g. non-integer numbers,
|
|
24
|
+
* malformed $link, $bytes, blob objects, etc.)
|
|
25
|
+
*/
|
|
26
|
+
strict?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function lexParse(
|
|
30
|
+
input: string,
|
|
31
|
+
options: LexParseOptions = { strict: false },
|
|
32
|
+
): LexValue {
|
|
33
|
+
return JSON.parse(input, function (key: string, value: JsonValue): LexValue {
|
|
34
|
+
switch (typeof value) {
|
|
35
|
+
case 'object':
|
|
36
|
+
if (value === null) return null
|
|
37
|
+
if (Array.isArray(value)) return value
|
|
38
|
+
return parseSpecialJsonObject(value, options) ?? value
|
|
39
|
+
case 'number':
|
|
40
|
+
if (Number.isInteger(value)) return value
|
|
41
|
+
if (options.strict) {
|
|
42
|
+
throw new TypeError(`Invalid non-integer number: ${value}`)
|
|
43
|
+
}
|
|
44
|
+
// fallthrough
|
|
45
|
+
default:
|
|
46
|
+
return value
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function jsonToLex(
|
|
52
|
+
value: JsonValue,
|
|
53
|
+
options: LexParseOptions = { strict: false },
|
|
54
|
+
): LexValue {
|
|
55
|
+
switch (typeof value) {
|
|
56
|
+
case 'object': {
|
|
57
|
+
if (value === null) return null
|
|
58
|
+
if (Array.isArray(value)) return jsonArrayToLex(value, options)
|
|
59
|
+
return (
|
|
60
|
+
parseSpecialJsonObject(value, options) ??
|
|
61
|
+
jsonObjectToLexMap(value, options)
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
case 'number':
|
|
65
|
+
if (Number.isInteger(value)) return value
|
|
66
|
+
if (options.strict) {
|
|
67
|
+
throw new TypeError(`Invalid non-integer number: ${value}`)
|
|
68
|
+
}
|
|
69
|
+
// fallthrough
|
|
70
|
+
case 'boolean':
|
|
71
|
+
case 'string':
|
|
72
|
+
return value
|
|
73
|
+
default:
|
|
74
|
+
throw new TypeError(`Invalid JSON value: ${typeof value}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function jsonArrayToLex(
|
|
79
|
+
input: JsonValue[],
|
|
80
|
+
options: LexParseOptions,
|
|
81
|
+
): LexValue[] {
|
|
82
|
+
// Lazily copy value
|
|
83
|
+
let copy: LexValue[] | undefined
|
|
84
|
+
for (let i = 0; i < input.length; i++) {
|
|
85
|
+
const inputItem = input[i]
|
|
86
|
+
const item = jsonToLex(inputItem, options)
|
|
87
|
+
if (item !== inputItem) {
|
|
88
|
+
copy ??= Array.from(input)
|
|
89
|
+
copy[i] = item
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return copy ?? input
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function jsonObjectToLexMap(
|
|
96
|
+
input: JsonObject,
|
|
97
|
+
options: LexParseOptions,
|
|
98
|
+
): LexMap {
|
|
99
|
+
// Lazily copy value
|
|
100
|
+
let copy: LexMap | undefined = undefined
|
|
101
|
+
for (const [key, jsonValue] of Object.entries(input)) {
|
|
102
|
+
// Prevent prototype pollution
|
|
103
|
+
if (key === '__proto__') {
|
|
104
|
+
throw new TypeError('Invalid key: __proto__')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Ignore (strip) undefined values
|
|
108
|
+
if (jsonValue === undefined) {
|
|
109
|
+
copy ??= { ...input }
|
|
110
|
+
delete copy[key]
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const value = jsonToLex(jsonValue!, options)
|
|
115
|
+
if (value !== jsonValue) {
|
|
116
|
+
copy ??= { ...input }
|
|
117
|
+
copy[key] = value
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return copy ?? input
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function lexToJson(value: LexValue): JsonValue {
|
|
124
|
+
switch (typeof value) {
|
|
125
|
+
case 'object':
|
|
126
|
+
if (value === null) {
|
|
127
|
+
return value
|
|
128
|
+
} else if (Array.isArray(value)) {
|
|
129
|
+
return lexArrayToJson(value)
|
|
130
|
+
} else if (isCid(value)) {
|
|
131
|
+
return encodeLexLink(value)
|
|
132
|
+
} else if (value instanceof Uint8Array) {
|
|
133
|
+
return encodeLexBytes(value)
|
|
134
|
+
} else {
|
|
135
|
+
return encodeLexMap(value)
|
|
136
|
+
}
|
|
137
|
+
case 'boolean':
|
|
138
|
+
case 'string':
|
|
139
|
+
case 'number':
|
|
140
|
+
return value
|
|
141
|
+
default:
|
|
142
|
+
throw new TypeError(`Invalid Lex value: ${typeof value}`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function lexArrayToJson(input: LexArray): JsonValue[] {
|
|
147
|
+
// Lazily copy value
|
|
148
|
+
let copy: JsonValue[] | undefined
|
|
149
|
+
for (let i = 0; i < input.length; i++) {
|
|
150
|
+
const inputItem = input[i]
|
|
151
|
+
const item = lexToJson(inputItem)
|
|
152
|
+
if (item !== inputItem) {
|
|
153
|
+
copy ??= Array.from(input) as JsonValue[]
|
|
154
|
+
copy[i] = item
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return copy ?? (input as JsonValue[])
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function encodeLexMap(input: LexMap): JsonObject {
|
|
161
|
+
// Lazily copy value
|
|
162
|
+
let copy: JsonObject | undefined = undefined
|
|
163
|
+
for (const [key, lexValue] of Object.entries(input)) {
|
|
164
|
+
// Prevent prototype pollution
|
|
165
|
+
if (key === '__proto__') {
|
|
166
|
+
throw new TypeError('Invalid key: __proto__')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Ignore (strip) undefined values
|
|
170
|
+
if (lexValue === undefined) {
|
|
171
|
+
copy ??= { ...input } as JsonObject
|
|
172
|
+
delete copy[key]
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const jsonValue = lexToJson(lexValue!)
|
|
177
|
+
if (jsonValue !== lexValue) {
|
|
178
|
+
copy ??= { ...input } as JsonObject
|
|
179
|
+
copy[key] = jsonValue
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return copy ?? (input as JsonObject)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseSpecialJsonObject(
|
|
186
|
+
input: LexMap,
|
|
187
|
+
options: LexParseOptions,
|
|
188
|
+
): CID | Uint8Array | BlobRef | undefined {
|
|
189
|
+
// Hot path: use hints to avoid parsing when possible
|
|
190
|
+
|
|
191
|
+
if (input.$link !== undefined) {
|
|
192
|
+
const cid = parseLexLink(input)
|
|
193
|
+
if (cid) return cid
|
|
194
|
+
if (options.strict) throw new TypeError(`Invalid $link object`)
|
|
195
|
+
} else if (input.$bytes !== undefined) {
|
|
196
|
+
const bytes = parseLexBytes(input)
|
|
197
|
+
if (bytes) return bytes
|
|
198
|
+
if (options.strict) throw new TypeError(`Invalid $bytes object`)
|
|
199
|
+
} else if (input.$type !== undefined) {
|
|
200
|
+
// @NOTE Since blobs are "just" regular lex objects with a special shape,
|
|
201
|
+
// and because an object that does not conform to the blob shape would still
|
|
202
|
+
// result in undefined being returned, we only attempt to parse blobs when
|
|
203
|
+
// the strict option is enabled.
|
|
204
|
+
if (options.strict) {
|
|
205
|
+
if (input.$type === 'blob') {
|
|
206
|
+
const blob = parseBlobRef(input, options)
|
|
207
|
+
if (blob) return blob
|
|
208
|
+
throw new TypeError(`Invalid blob object`)
|
|
209
|
+
} else if (typeof input.$type !== 'string') {
|
|
210
|
+
throw new TypeError(`Invalid $type property (${typeof input.$type})`)
|
|
211
|
+
} else if (input.$type.length === 0) {
|
|
212
|
+
throw new TypeError(`Empty $type property`)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// @NOTE We ignore legacy blob representation here. They can be handled at the
|
|
218
|
+
// application level if needed.
|
|
219
|
+
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
package/src/link.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CID } from '@atproto/lex-data'
|
|
2
|
+
import { JsonValue } from './json.js'
|
|
3
|
+
|
|
4
|
+
export function parseLexLink(input?: { $link?: unknown }): CID | undefined {
|
|
5
|
+
if (!input || !('$link' in input)) {
|
|
6
|
+
return undefined
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
for (const key in input) {
|
|
10
|
+
if (key !== '$link') {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof input.$link !== 'string') {
|
|
16
|
+
throw new TypeError('$link must be a base32-encoded CID string')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (input.$link.length === 0) {
|
|
20
|
+
throw new TypeError('CID string in $link cannot be empty')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Arbitrary limit to prevent DoS via extremely long CIDs
|
|
24
|
+
if (input.$link.length > 2048) {
|
|
25
|
+
throw new TypeError('CID string in $link is too long')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
return CID.parse(input.$link)
|
|
30
|
+
} catch (cause) {
|
|
31
|
+
throw new TypeError('Invalid CID string in $link', { cause })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function encodeLexLink(cid: CID): JsonValue {
|
|
36
|
+
return { $link: cid.toString() }
|
|
37
|
+
}
|