koffing 1.0.0 → 2.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/README.md +94 -7
- package/dist/data-CP51LAc9.cjs +404 -0
- package/dist/data-CRHBhbCJ.js +345 -0
- package/dist/index.cjs +441 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +27 -104
- package/dist/index.js +432 -430
- package/dist/types-BVzP_Ko4.d.cts +62 -0
- package/dist/types-BVzP_Ko4.d.ts +62 -0
- package/dist/validator.cjs +3563 -0
- package/dist/validator.d.cts +14 -0
- package/dist/validator.d.ts +14 -0
- package/dist/validator.js +3562 -0
- package/package.json +43 -22
- package/dist/index.mjs +0 -400
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
//#region src/limits.ts
|
|
2
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
3
|
+
maxInputLength: 2e6,
|
|
4
|
+
maxLineLength: 16384,
|
|
5
|
+
maxTeams: 1e3,
|
|
6
|
+
maxPokemon: 1e4,
|
|
7
|
+
maxMoves: 256,
|
|
8
|
+
maxDiagnostics: 1e3
|
|
9
|
+
});
|
|
10
|
+
var KoffingError = class extends Error {
|
|
11
|
+
diagnostics;
|
|
12
|
+
constructor(message, diagnostics) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "KoffingError";
|
|
15
|
+
this.diagnostics = diagnostics;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function resolveLimits(options = {}) {
|
|
19
|
+
if (options.mode !== void 0 && options.mode !== "strict" && options.mode !== "permissive") throw new TypeError("mode must be strict or permissive");
|
|
20
|
+
if (options.limits === void 0) return DEFAULT_LIMITS;
|
|
21
|
+
const limits = {
|
|
22
|
+
...DEFAULT_LIMITS,
|
|
23
|
+
...options.limits
|
|
24
|
+
};
|
|
25
|
+
for (const [key, value] of Object.entries(limits)) if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${key} must be a positive safe integer`);
|
|
26
|
+
return limits;
|
|
27
|
+
}
|
|
28
|
+
function fail(code, message, line) {
|
|
29
|
+
throw new KoffingError(message, [{
|
|
30
|
+
code,
|
|
31
|
+
message,
|
|
32
|
+
severity: "error",
|
|
33
|
+
...line === void 0 ? {} : { line }
|
|
34
|
+
}]);
|
|
35
|
+
}
|
|
36
|
+
function checkInput(input, limits) {
|
|
37
|
+
if (typeof input !== "string") throw new TypeError("Input must be a string");
|
|
38
|
+
if (input.length > limits.maxInputLength) fail("input-limit", `Input exceeds ${limits.maxInputLength} code units`);
|
|
39
|
+
}
|
|
40
|
+
function finish(diagnostics, options) {
|
|
41
|
+
if (options.mode === "strict" && diagnostics.length) throw new KoffingError(diagnostics[0].message, diagnostics);
|
|
42
|
+
}
|
|
43
|
+
function report(diagnostics, diagnostic, limits) {
|
|
44
|
+
if (diagnostics.length >= limits.maxDiagnostics) fail("diagnostic-limit", "Too many parsing issues", diagnostic.line);
|
|
45
|
+
diagnostics.push(diagnostic);
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/data.ts
|
|
49
|
+
const strings = [
|
|
50
|
+
"species",
|
|
51
|
+
"name",
|
|
52
|
+
"item",
|
|
53
|
+
"ability",
|
|
54
|
+
"nature",
|
|
55
|
+
"pokeball",
|
|
56
|
+
"hpType",
|
|
57
|
+
"teraType"
|
|
58
|
+
];
|
|
59
|
+
const numbers = [
|
|
60
|
+
"level",
|
|
61
|
+
"happiness",
|
|
62
|
+
"dynamaxLevel"
|
|
63
|
+
];
|
|
64
|
+
const flags = ["shiny", "gigantamax"];
|
|
65
|
+
const stats = [
|
|
66
|
+
"hp",
|
|
67
|
+
"atk",
|
|
68
|
+
"def",
|
|
69
|
+
"spa",
|
|
70
|
+
"spd",
|
|
71
|
+
"spe"
|
|
72
|
+
];
|
|
73
|
+
const statKeys = new Set(stats);
|
|
74
|
+
const teamKeys = /* @__PURE__ */ new Set([
|
|
75
|
+
"name",
|
|
76
|
+
"format",
|
|
77
|
+
"folder",
|
|
78
|
+
"pokemon"
|
|
79
|
+
]);
|
|
80
|
+
const collectionKeys = /* @__PURE__ */ new Set(["teams"]);
|
|
81
|
+
const setKeys = /* @__PURE__ */ new Set([
|
|
82
|
+
...strings,
|
|
83
|
+
...numbers,
|
|
84
|
+
...flags,
|
|
85
|
+
"gender",
|
|
86
|
+
"evs",
|
|
87
|
+
"ivs",
|
|
88
|
+
"moves"
|
|
89
|
+
]);
|
|
90
|
+
function objectKeys(value, path, limits) {
|
|
91
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) fail("invalid-json", `${path} must be a plain object`);
|
|
92
|
+
const keys = Reflect.ownKeys(value);
|
|
93
|
+
if (keys.length > limits.maxDiagnostics + 32) fail("property-limit", `${path} contains too many properties`);
|
|
94
|
+
return keys;
|
|
95
|
+
}
|
|
96
|
+
function property(value, key, path, limits) {
|
|
97
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
98
|
+
if (typeof key !== "string" || !("value" in descriptor)) fail("invalid-json", `${path} contains an accessor or symbol`);
|
|
99
|
+
if (key.length > Math.max(32, limits.maxLineLength)) fail("property-limit", `${path} contains an oversized property name`);
|
|
100
|
+
return descriptor.value;
|
|
101
|
+
}
|
|
102
|
+
function object(value, path, limits) {
|
|
103
|
+
const keys = objectKeys(value, path, limits);
|
|
104
|
+
const result = Object.create(null);
|
|
105
|
+
for (const key of keys) result[key] = property(value, key, path, limits);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
function array(value, path, maximum) {
|
|
109
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) fail("invalid-json", `${path} must be an array`);
|
|
110
|
+
if (value.length > maximum) fail("collection-limit", `${path} exceeds ${maximum} entries`);
|
|
111
|
+
if (Reflect.ownKeys(value).length !== value.length + 1) fail("invalid-json", `${path} contains non-index properties or holes`);
|
|
112
|
+
const result = [];
|
|
113
|
+
for (let i = 0; i < value.length; i++) {
|
|
114
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
|
|
115
|
+
if (!descriptor || !("value" in descriptor)) fail("invalid-json", `${path} must not contain holes or accessors`);
|
|
116
|
+
result.push(descriptor.value);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function text(value, path, limits, budget) {
|
|
121
|
+
if (typeof value !== "string" || /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(value) || value !== value.trim()) fail("invalid-text", `${path} must be trimmed text without control characters`);
|
|
122
|
+
if (value.length > limits.maxLineLength) fail("line-limit", `${path} exceeds ${limits.maxLineLength} code units`);
|
|
123
|
+
budget.used += value.length + 32;
|
|
124
|
+
if (budget.used > limits.maxInputLength) fail("input-limit", "Aggregate data exceeds configured input limit");
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
function finite(value, path) {
|
|
128
|
+
if (typeof value !== "number" || !Number.isFinite(value)) fail("invalid-number", `${path} must be finite`);
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function unknownKeys(source, known, path, diagnostics, limits) {
|
|
132
|
+
for (const key of Object.keys(source)) if (!known.has(key)) report(diagnostics, {
|
|
133
|
+
code: "unknown-field",
|
|
134
|
+
severity: "warning",
|
|
135
|
+
path: `${path}.${key}`,
|
|
136
|
+
message: `Unknown field ${path}.${key}`
|
|
137
|
+
}, limits);
|
|
138
|
+
}
|
|
139
|
+
function readSet(value, path, diagnostics, limits, budget) {
|
|
140
|
+
const keys = objectKeys(value, path, limits);
|
|
141
|
+
budget.used += 512;
|
|
142
|
+
if (budget.used > limits.maxInputLength) fail("input-limit", "Aggregate data exceeds configured input limit");
|
|
143
|
+
const result = {
|
|
144
|
+
species: "",
|
|
145
|
+
moves: []
|
|
146
|
+
};
|
|
147
|
+
let hasMoves = false;
|
|
148
|
+
for (const rawKey of keys) {
|
|
149
|
+
const entry = property(value, rawKey, path, limits);
|
|
150
|
+
const key = rawKey;
|
|
151
|
+
if (!setKeys.has(key)) {
|
|
152
|
+
report(diagnostics, {
|
|
153
|
+
code: "unknown-field",
|
|
154
|
+
severity: "warning",
|
|
155
|
+
path: `${path}.${key}`,
|
|
156
|
+
message: `Unknown field ${path}.${key}`
|
|
157
|
+
}, limits);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (entry === void 0 && key !== "species" && key !== "moves") continue;
|
|
161
|
+
switch (key) {
|
|
162
|
+
case "species":
|
|
163
|
+
case "name":
|
|
164
|
+
case "item":
|
|
165
|
+
case "ability":
|
|
166
|
+
case "nature":
|
|
167
|
+
case "pokeball":
|
|
168
|
+
case "hpType":
|
|
169
|
+
case "teraType":
|
|
170
|
+
result[key] = text(entry, `${path}.${key}`, limits, budget);
|
|
171
|
+
break;
|
|
172
|
+
case "level":
|
|
173
|
+
case "happiness":
|
|
174
|
+
case "dynamaxLevel":
|
|
175
|
+
result[key] = finite(entry, `${path}.${key}`);
|
|
176
|
+
break;
|
|
177
|
+
case "shiny":
|
|
178
|
+
case "gigantamax":
|
|
179
|
+
if (typeof entry !== "boolean") fail("invalid-boolean", `${path}.${key} must be boolean`);
|
|
180
|
+
result[key] = entry;
|
|
181
|
+
break;
|
|
182
|
+
case "gender":
|
|
183
|
+
if (entry !== "M" && entry !== "F" && entry !== "N" && entry !== "") fail("invalid-gender", `${path}.gender must be M, F, N or empty`);
|
|
184
|
+
result.gender = entry;
|
|
185
|
+
break;
|
|
186
|
+
case "evs":
|
|
187
|
+
case "ivs": {
|
|
188
|
+
const statPath = `${path}.${key}`;
|
|
189
|
+
const values = {};
|
|
190
|
+
for (const rawStat of objectKeys(entry, statPath, limits)) {
|
|
191
|
+
const number = property(entry, rawStat, statPath, limits);
|
|
192
|
+
const stat = rawStat;
|
|
193
|
+
if (!statKeys.has(stat)) report(diagnostics, {
|
|
194
|
+
code: "unknown-field",
|
|
195
|
+
severity: "warning",
|
|
196
|
+
path: `${statPath}.${stat}`,
|
|
197
|
+
message: `Unknown field ${statPath}.${stat}`
|
|
198
|
+
}, limits);
|
|
199
|
+
else if (number !== void 0) values[stat] = finite(number, `${statPath}.${stat}`);
|
|
200
|
+
}
|
|
201
|
+
result[key] = values;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "moves": {
|
|
205
|
+
hasMoves = true;
|
|
206
|
+
const moves = array(entry, `${path}.moves`, limits.maxMoves);
|
|
207
|
+
for (let index = 0; index < moves.length; index++) if (!text(moves[index], `${path}.moves[${index}]`, limits, budget)) fail("invalid-move", `${path}.moves[${index}] cannot be empty`);
|
|
208
|
+
result.moves = moves;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!result.species) fail("invalid-species", `${path}.species is required`);
|
|
214
|
+
if (!hasMoves) fail("invalid-json", `${path}.moves must be an array`);
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
/** Decode and copy known data only. Shape errors are fatal; unknown fields are diagnosed. */
|
|
218
|
+
function parseJSON(input, options = {}) {
|
|
219
|
+
const limits = resolveLimits(options);
|
|
220
|
+
let value = input;
|
|
221
|
+
if (typeof value === "string") {
|
|
222
|
+
checkInput(value, limits);
|
|
223
|
+
try {
|
|
224
|
+
value = JSON.parse(value);
|
|
225
|
+
} catch {
|
|
226
|
+
fail("invalid-json", "Invalid JSON syntax");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const diagnostics = [];
|
|
230
|
+
const budget = { used: 0 };
|
|
231
|
+
let rawTeams;
|
|
232
|
+
if (Array.isArray(value)) rawTeams = [{ pokemon: value }];
|
|
233
|
+
else {
|
|
234
|
+
const source = object(value, "input", limits);
|
|
235
|
+
if (Object.hasOwn(source, "teams")) {
|
|
236
|
+
unknownKeys(source, collectionKeys, "input", diagnostics, limits);
|
|
237
|
+
rawTeams = array(source.teams, "teams", limits.maxTeams);
|
|
238
|
+
} else if (Object.hasOwn(source, "pokemon")) rawTeams = [source];
|
|
239
|
+
else rawTeams = [{ pokemon: [source] }];
|
|
240
|
+
}
|
|
241
|
+
if (rawTeams.length > limits.maxTeams) fail("collection-limit", "Too many teams");
|
|
242
|
+
let total = 0;
|
|
243
|
+
const teams = rawTeams.map((raw, index) => {
|
|
244
|
+
const path = `teams[${index}]`;
|
|
245
|
+
const source = object(raw, path, limits);
|
|
246
|
+
unknownKeys(source, teamKeys, path, diagnostics, limits);
|
|
247
|
+
const members = array(source.pokemon, `${path}.pokemon`, limits.maxPokemon - total);
|
|
248
|
+
total += members.length;
|
|
249
|
+
const team = { pokemon: members.map((member, i) => readSet(member, `${path}.pokemon[${i}]`, diagnostics, limits, budget)) };
|
|
250
|
+
for (const key of [
|
|
251
|
+
"name",
|
|
252
|
+
"format",
|
|
253
|
+
"folder"
|
|
254
|
+
]) if (source[key] !== void 0) team[key] = text(source[key], `${path}.${key}`, limits, budget);
|
|
255
|
+
return team;
|
|
256
|
+
});
|
|
257
|
+
for (const [index, team] of teams.entries()) checkRanges(team.pokemon, `teams[${index}].pokemon`, diagnostics, limits);
|
|
258
|
+
finish(diagnostics, options);
|
|
259
|
+
return {
|
|
260
|
+
teams,
|
|
261
|
+
diagnostics
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** Generic traditional-format checks, not species or format legality validation. */
|
|
265
|
+
function checkRanges(pokemon, path, diagnostics, limits) {
|
|
266
|
+
const add = (code, path, message) => report(diagnostics, {
|
|
267
|
+
code,
|
|
268
|
+
severity: "warning",
|
|
269
|
+
path,
|
|
270
|
+
message
|
|
271
|
+
}, limits);
|
|
272
|
+
for (const [i, set] of pokemon.entries()) {
|
|
273
|
+
const prefix = `${path}[${i}]`;
|
|
274
|
+
const range = (value, min, max, field, code = "number-range") => {
|
|
275
|
+
if (value !== void 0 && (!Number.isInteger(value) || value < min || value > max)) add(code, `${prefix}.${field}`, `Expected an integer from ${min} to ${max}; value preserved`);
|
|
276
|
+
};
|
|
277
|
+
range(set.level, 1, 100, "level");
|
|
278
|
+
range(set.happiness, 0, 255, "happiness");
|
|
279
|
+
range(set.dynamaxLevel, 0, 10, "dynamaxLevel");
|
|
280
|
+
let evTotal = 0;
|
|
281
|
+
for (const stat of stats) {
|
|
282
|
+
const ev = set.evs?.[stat];
|
|
283
|
+
const iv = set.ivs?.[stat];
|
|
284
|
+
if (ev !== void 0) {
|
|
285
|
+
evTotal += ev;
|
|
286
|
+
range(ev, 0, 255, `evs.${stat}`, "stat-range");
|
|
287
|
+
}
|
|
288
|
+
if (iv !== void 0) range(iv, 0, 31, `ivs.${stat}`, "stat-range");
|
|
289
|
+
}
|
|
290
|
+
if (evTotal > 510) add("ev-total", `${prefix}.evs`, "Traditional EV total exceeds 510");
|
|
291
|
+
if (set.moves.length > 4) add("move-count", `${prefix}.moves`, "Traditional sets contain at most four moves");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function validateTeam(pokemon, options = {}) {
|
|
295
|
+
return parseJSON(pokemon, options).diagnostics;
|
|
296
|
+
}
|
|
297
|
+
/** Explicit, deterministic traditional-range sanitization; never mutates its input. */
|
|
298
|
+
function sanitizeTeam(pokemon, options = {}) {
|
|
299
|
+
const maxLevel = options.maxLevel ?? 100;
|
|
300
|
+
const maxMoves = options.maxMovesPerPokemon ?? 4;
|
|
301
|
+
if (!Number.isSafeInteger(maxLevel) || maxLevel < 1 || !Number.isSafeInteger(maxMoves) || maxMoves < 0) throw new TypeError("Sanitization limits must be valid nonnegative integers (maxLevel >= 1)");
|
|
302
|
+
const parsed = parseJSON(pokemon, {
|
|
303
|
+
...options,
|
|
304
|
+
mode: "permissive"
|
|
305
|
+
});
|
|
306
|
+
const result = parsed.teams[0].pokemon;
|
|
307
|
+
const diagnostics = parsed.diagnostics;
|
|
308
|
+
const limits = resolveLimits(options);
|
|
309
|
+
const changed = (path) => report(diagnostics, {
|
|
310
|
+
code: "sanitized",
|
|
311
|
+
severity: "warning",
|
|
312
|
+
path,
|
|
313
|
+
message: `Sanitized ${path}`
|
|
314
|
+
}, limits);
|
|
315
|
+
const clamp = (value, min, max, path) => {
|
|
316
|
+
const next = Math.max(min, Math.min(max, Math.trunc(value)));
|
|
317
|
+
if (next !== value) changed(path);
|
|
318
|
+
return next;
|
|
319
|
+
};
|
|
320
|
+
for (const [i, set] of result.entries()) {
|
|
321
|
+
const path = `pokemon[${i}]`;
|
|
322
|
+
if (set.level !== void 0) set.level = clamp(set.level, 1, maxLevel, `${path}.level`);
|
|
323
|
+
if (set.happiness !== void 0) set.happiness = clamp(set.happiness, 0, 255, `${path}.happiness`);
|
|
324
|
+
if (set.dynamaxLevel !== void 0) set.dynamaxLevel = clamp(set.dynamaxLevel, 0, 10, `${path}.dynamaxLevel`);
|
|
325
|
+
let evBudget = 510;
|
|
326
|
+
for (const stat of stats) {
|
|
327
|
+
if (set.evs?.[stat] !== void 0) {
|
|
328
|
+
set.evs[stat] = clamp(set.evs[stat], 0, Math.min(255, evBudget), `${path}.evs.${stat}`);
|
|
329
|
+
evBudget -= set.evs[stat];
|
|
330
|
+
}
|
|
331
|
+
if (set.ivs?.[stat] !== void 0) set.ivs[stat] = clamp(set.ivs[stat], 0, 31, `${path}.ivs.${stat}`);
|
|
332
|
+
}
|
|
333
|
+
if (set.moves.length > maxMoves) {
|
|
334
|
+
set.moves = set.moves.slice(0, maxMoves);
|
|
335
|
+
changed(`${path}.moves`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
finish(diagnostics, options);
|
|
339
|
+
return {
|
|
340
|
+
pokemon: result,
|
|
341
|
+
diagnostics
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
export { KoffingError as a, finish as c, DEFAULT_LIMITS as i, report as l, sanitizeTeam as n, checkInput as o, validateTeam as r, fail as s, parseJSON as t, resolveLimits as u };
|