@c9up/rune 0.1.7 → 0.1.9
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/MessagesProvider.d.ts +5 -0
- package/dist/MessagesProvider.d.ts.map +1 -1
- package/dist/MessagesProvider.js +1 -1
- package/dist/MessagesProvider.js.map +1 -1
- package/dist/Schema.d.ts +831 -35
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +2342 -140
- package/dist/Schema.js.map +1 -1
- package/dist/date.d.ts +36 -0
- package/dist/date.d.ts.map +1 -0
- package/dist/date.js +275 -0
- package/dist/date.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -1
- package/dist/formats.d.ts +148 -0
- package/dist/formats.d.ts.map +1 -0
- package/dist/formats.js +671 -0
- package/dist/formats.js.map +1 -0
- package/dist/index.d.ts +150 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +180 -1
- package/dist/index.js.map +1 -1
- package/dist/magic.d.ts +30 -0
- package/dist/magic.d.ts.map +1 -0
- package/dist/magic.js +154 -0
- package/dist/magic.js.map +1 -0
- package/dist/native.d.ts +18 -6
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +34 -19
- package/dist/native.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +9 -1
- package/src/MessagesProvider.ts +1 -1
- package/src/Schema.ts +3389 -177
- package/src/date.ts +320 -0
- package/src/errors.ts +11 -0
- package/src/formats.ts +776 -0
- package/src/index.ts +269 -0
- package/src/magic.ts +181 -0
- package/src/native.ts +36 -21
- package/src/types.ts +55 -0
package/src/index.ts
CHANGED
|
@@ -4,15 +4,42 @@
|
|
|
4
4
|
* @implements FR38, FR39, FR40, FR41, FR42
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
export type { DateFormat } from "./date.js";
|
|
8
|
+
/**
|
|
9
|
+
* Namespace import, mirroring `import { errors } from '@vinejs/vine'` — Adonis
|
|
10
|
+
* code catches on `errors.E_VALIDATION_ERROR`, so the namespace has to exist at
|
|
11
|
+
* the root and not only on the subpath.
|
|
12
|
+
*/
|
|
13
|
+
export * as errors from "./errors.js";
|
|
7
14
|
export { RuneError, RuneValidationError } from "./errors.js";
|
|
15
|
+
export type {
|
|
16
|
+
AlphaOptions,
|
|
17
|
+
EmailOptions,
|
|
18
|
+
NormalizeEmailOptions,
|
|
19
|
+
NormalizeUrlOptions,
|
|
20
|
+
UrlOptions,
|
|
21
|
+
} from "./formats.js";
|
|
8
22
|
export type { MessagesProviderContract } from "./MessagesProvider.js";
|
|
9
23
|
export { SimpleMessagesProvider } from "./MessagesProvider.js";
|
|
24
|
+
export {
|
|
25
|
+
assertNativeAvailable,
|
|
26
|
+
isNativeAvailable,
|
|
27
|
+
RuneNativeRequiredError,
|
|
28
|
+
} from "./native.js";
|
|
10
29
|
export type {
|
|
30
|
+
AsyncCompiledRule,
|
|
31
|
+
AsyncRuleValidator,
|
|
11
32
|
CompiledRule,
|
|
33
|
+
CreateRuleOptions,
|
|
34
|
+
DatabaseLookup,
|
|
35
|
+
DatabaseResolver,
|
|
36
|
+
DatabaseRuleOptions,
|
|
12
37
|
FieldContext,
|
|
13
38
|
Infer,
|
|
39
|
+
RecordKeysCallback,
|
|
14
40
|
RuleChain,
|
|
15
41
|
RuleValidator,
|
|
42
|
+
UnionNoMatchCallback,
|
|
16
43
|
ValidateOptions,
|
|
17
44
|
ValidationError,
|
|
18
45
|
ValidationMessageParams,
|
|
@@ -21,9 +48,251 @@ export type {
|
|
|
21
48
|
ValidationTranslator,
|
|
22
49
|
} from "./Schema.js";
|
|
23
50
|
export {
|
|
51
|
+
bindDatabase,
|
|
52
|
+
bindHostResolver,
|
|
53
|
+
bindRosetta,
|
|
54
|
+
compile,
|
|
55
|
+
create,
|
|
56
|
+
createAsyncRule,
|
|
57
|
+
createRule,
|
|
58
|
+
rules,
|
|
59
|
+
schema,
|
|
60
|
+
setConvertEmptyStringsToNull,
|
|
61
|
+
setDateTransform,
|
|
62
|
+
setGlobalErrorReporter,
|
|
63
|
+
setValidationTranslator,
|
|
64
|
+
} from "./Schema.js";
|
|
65
|
+
|
|
66
|
+
import type { MessagesProviderContract } from "./MessagesProvider.js";
|
|
67
|
+
import type { RuleChain, ValidateOptions } from "./Schema.js";
|
|
68
|
+
import {
|
|
69
|
+
bindDatabase,
|
|
70
|
+
bindHostResolver,
|
|
24
71
|
bindRosetta,
|
|
72
|
+
compile,
|
|
73
|
+
create,
|
|
74
|
+
createAsyncRule,
|
|
25
75
|
createRule,
|
|
76
|
+
getConvertEmptyStringsToNull,
|
|
77
|
+
getGlobalErrorReporter,
|
|
78
|
+
getGlobalMessagesProvider,
|
|
79
|
+
group,
|
|
80
|
+
groupElse,
|
|
81
|
+
groupIf,
|
|
26
82
|
rules,
|
|
27
83
|
schema,
|
|
84
|
+
setConvertEmptyStringsToNull,
|
|
85
|
+
setDateTransform,
|
|
86
|
+
setGlobalErrorReporter,
|
|
87
|
+
setGlobalMessagesProvider,
|
|
28
88
|
setValidationTranslator,
|
|
29
89
|
} from "./Schema.js";
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A validator whose entry points REQUIRE `{ meta }`.
|
|
93
|
+
*
|
|
94
|
+
* VineJS refuses `validate(data)` once `withMetaData<T>()` declared metadata as
|
|
95
|
+
* required; keeping `meta` optional meant the guard existed at runtime but the
|
|
96
|
+
* compiler still waved the missing-metadata call through.
|
|
97
|
+
*/
|
|
98
|
+
type WithRequiredMeta<V, M> = {
|
|
99
|
+
[K in keyof V]: V[K] extends (data: infer D, options?: infer O) => infer R
|
|
100
|
+
? (data: D, options: Omit<O & object, "meta"> & { meta: M }) => R
|
|
101
|
+
: V[K];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Default export, mirroring `import vine from '@vinejs/vine'`.
|
|
106
|
+
*
|
|
107
|
+
* Named `rune`, not `vine` — the deviation is the package name, nothing else:
|
|
108
|
+
* the type factories (`rune.string()`, `rune.date()`, …) and the validator
|
|
109
|
+
* factories (`rune.create`, `rune.compile`) sit on one object exactly like
|
|
110
|
+
* VineJS, so an Adonis validator transcribes line for line.
|
|
111
|
+
*/
|
|
112
|
+
const rune = {
|
|
113
|
+
...rules,
|
|
114
|
+
schema,
|
|
115
|
+
create,
|
|
116
|
+
compile,
|
|
117
|
+
createRule,
|
|
118
|
+
createAsyncRule,
|
|
119
|
+
// `group` carries its branch factories, mirroring `vine.group.if/else`.
|
|
120
|
+
group: Object.assign(group, {
|
|
121
|
+
if: groupIf,
|
|
122
|
+
else: groupElse,
|
|
123
|
+
otherwise: groupElse,
|
|
124
|
+
}),
|
|
125
|
+
bindDatabase,
|
|
126
|
+
bindHostResolver,
|
|
127
|
+
bindRosetta,
|
|
128
|
+
setDateTransform,
|
|
129
|
+
setValidationTranslator,
|
|
130
|
+
/**
|
|
131
|
+
* Convert `""` to `null` before validating (VineJS
|
|
132
|
+
* `convertEmptyStringsToNull`). An HTML form posts empty inputs as `""`, and
|
|
133
|
+
* an optional field should read that as "absent", not as a present empty
|
|
134
|
+
* string that fails `minLength`.
|
|
135
|
+
*/
|
|
136
|
+
set convertEmptyStringsToNull(enabled: boolean) {
|
|
137
|
+
setConvertEmptyStringsToNull(enabled);
|
|
138
|
+
},
|
|
139
|
+
get convertEmptyStringsToNull(): boolean {
|
|
140
|
+
return getConvertEmptyStringsToNull();
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* Predicate helpers VineJS exposes as `vine.helpers`, for writing custom
|
|
144
|
+
* rules without reimplementing the same three checks each time.
|
|
145
|
+
*/
|
|
146
|
+
helpers: {
|
|
147
|
+
/** `true` for `true`, `1`, `"1"`, `"true"`, `"on"`, `"yes"`. */
|
|
148
|
+
isTrue: (value: unknown): boolean =>
|
|
149
|
+
value === true ||
|
|
150
|
+
value === 1 ||
|
|
151
|
+
(typeof value === "string" &&
|
|
152
|
+
["1", "true", "on", "yes"].includes(value.toLowerCase())),
|
|
153
|
+
/** `true` for `false`, `0`, `"0"`, `"false"`, `"off"`, `"no"`. */
|
|
154
|
+
isFalse: (value: unknown): boolean =>
|
|
155
|
+
value === false ||
|
|
156
|
+
value === 0 ||
|
|
157
|
+
(typeof value === "string" &&
|
|
158
|
+
["0", "false", "off", "no"].includes(value.toLowerCase())),
|
|
159
|
+
/** Neither `undefined` nor `null` (an empty string IS defined). */
|
|
160
|
+
exists: (value: unknown): boolean => value !== undefined && value !== null,
|
|
161
|
+
/** `undefined` or `null`. */
|
|
162
|
+
isMissing: (value: unknown): boolean =>
|
|
163
|
+
value === undefined || value === null,
|
|
164
|
+
/** A plain object, not an array and not `null`. */
|
|
165
|
+
isObject: (value: unknown): value is Record<string, unknown> =>
|
|
166
|
+
typeof value === "object" && value !== null && !Array.isArray(value),
|
|
167
|
+
/** An array. */
|
|
168
|
+
isArray: Array.isArray,
|
|
169
|
+
/** Every listed key is present on the object (VineJS `helpers.hasKeys`). */
|
|
170
|
+
hasKeys: (value: unknown, keys: readonly string[]): boolean =>
|
|
171
|
+
typeof value === "object" &&
|
|
172
|
+
value !== null &&
|
|
173
|
+
keys.every((key) => key in value),
|
|
174
|
+
/**
|
|
175
|
+
* No duplicate in the data set, optionally compared on one or more fields
|
|
176
|
+
* (VineJS `helpers.isDistinct`). `null` / `undefined` items are ignored,
|
|
177
|
+
* and items missing a compared key are SKIPPED, so two absent values are
|
|
178
|
+
* not a duplicate of each other.
|
|
179
|
+
*/
|
|
180
|
+
isDistinct: (
|
|
181
|
+
dataSet: readonly unknown[],
|
|
182
|
+
fields?: string | string[],
|
|
183
|
+
): boolean => {
|
|
184
|
+
const list = fields === undefined ? null : [fields].flat();
|
|
185
|
+
const keys: string[] = [];
|
|
186
|
+
for (const item of dataSet) {
|
|
187
|
+
if (item === null || item === undefined) continue;
|
|
188
|
+
if (list === null) {
|
|
189
|
+
keys.push(JSON.stringify(item));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (typeof item !== "object" || item === null) continue;
|
|
193
|
+
const record: Record<string, unknown> = { ...item };
|
|
194
|
+
if (list.some((k) => record[k] === undefined || record[k] === null)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
keys.push(JSON.stringify(list.map((k) => record[k])));
|
|
198
|
+
}
|
|
199
|
+
return new Set(keys).size === keys.length;
|
|
200
|
+
},
|
|
201
|
+
/** Read a dotted path off the validated data (VineJS `helpers.getNestedValue`). */
|
|
202
|
+
getNestedValue: (
|
|
203
|
+
key: string,
|
|
204
|
+
field: { data: Record<string, unknown> },
|
|
205
|
+
): unknown => {
|
|
206
|
+
let cursor: unknown = field.data;
|
|
207
|
+
for (const segment of key.split(".")) {
|
|
208
|
+
if (typeof cursor !== "object" || cursor === null) return undefined;
|
|
209
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
210
|
+
}
|
|
211
|
+
return cursor;
|
|
212
|
+
},
|
|
213
|
+
/** Make every property of a shape optional (VineJS `helpers.optional`). */
|
|
214
|
+
optional: (props: Record<string, RuleChain>): Record<string, RuleChain> =>
|
|
215
|
+
Object.fromEntries(
|
|
216
|
+
Object.entries(props).map(([key, chain]) => [
|
|
217
|
+
key,
|
|
218
|
+
chain.clone().optional(),
|
|
219
|
+
]),
|
|
220
|
+
),
|
|
221
|
+
},
|
|
222
|
+
/** One-shot validation, VineJS `vine.validate({ schema, data })`. */
|
|
223
|
+
validate<T extends Record<string, RuleChain>>(
|
|
224
|
+
options: { schema: T | RuleChain; data: unknown } & ValidateOptions,
|
|
225
|
+
) {
|
|
226
|
+
// Spread the whole ValidateOptions rather than re-listing its keys: the
|
|
227
|
+
// hand-listed version silently dropped `errorReporter` when it was added.
|
|
228
|
+
const { schema: fields, data, ...validateOptions } = options;
|
|
229
|
+
return create(fields as T).validate(data, validateOptions);
|
|
230
|
+
},
|
|
231
|
+
/** One-shot non-throwing validation, VineJS `vine.tryValidate`. */
|
|
232
|
+
tryValidate<T extends Record<string, RuleChain>>(
|
|
233
|
+
options: { schema: T | RuleChain; data: unknown } & ValidateOptions,
|
|
234
|
+
) {
|
|
235
|
+
// Spread the whole ValidateOptions rather than re-listing its keys: the
|
|
236
|
+
// hand-listed version silently dropped `errorReporter` when it was added.
|
|
237
|
+
const { schema: fields, data, ...validateOptions } = options;
|
|
238
|
+
return create(fields as T).tryValidate(data, validateOptions);
|
|
239
|
+
},
|
|
240
|
+
/**
|
|
241
|
+
* Type the `meta` passed to `validate(data, { meta })` (VineJS
|
|
242
|
+
* `withMetaData`). Purely a typing seam — rune carries meta on the call.
|
|
243
|
+
*/
|
|
244
|
+
withMetaData<M extends Record<string, unknown>>(
|
|
245
|
+
validateMeta?: (meta: M) => void,
|
|
246
|
+
): {
|
|
247
|
+
create<T extends Record<string, RuleChain>>(
|
|
248
|
+
fields: T,
|
|
249
|
+
): WithRequiredMeta<ReturnType<typeof create<T>>, M>;
|
|
250
|
+
} {
|
|
251
|
+
return {
|
|
252
|
+
create<T extends Record<string, RuleChain>>(fields: T) {
|
|
253
|
+
const validator = create(fields);
|
|
254
|
+
if (!validateMeta) {
|
|
255
|
+
return validator as WithRequiredMeta<ReturnType<typeof create<T>>, M>;
|
|
256
|
+
}
|
|
257
|
+
// The callback runs BEFORE the payload: meta that is wrong makes
|
|
258
|
+
// every rule reading it meaningless, so failing early is the only
|
|
259
|
+
// honest outcome.
|
|
260
|
+
const guard = <A extends unknown[], R>(
|
|
261
|
+
run: (...args: A) => R,
|
|
262
|
+
): ((...args: A) => R) => {
|
|
263
|
+
return (...args: A): R => {
|
|
264
|
+
const options = args[1] as { meta?: M } | undefined;
|
|
265
|
+
validateMeta((options?.meta ?? {}) as M);
|
|
266
|
+
return run(...args);
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
...validator,
|
|
271
|
+
validate: guard(validator.validate),
|
|
272
|
+
validateResult: guard(validator.validateResult),
|
|
273
|
+
validateResultAsync: guard(validator.validateResultAsync),
|
|
274
|
+
validateOrThrow: guard(validator.validateOrThrow),
|
|
275
|
+
validateOrThrowAsync: guard(validator.validateOrThrowAsync),
|
|
276
|
+
tryValidate: guard(validator.tryValidate),
|
|
277
|
+
tryValidateSync: guard(validator.tryValidateSync),
|
|
278
|
+
} as WithRequiredMeta<ReturnType<typeof create<T>>, M>;
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
/** Process-wide error reporter (VineJS `vine.errorReporter`). */
|
|
283
|
+
set errorReporter(reporter: Parameters<typeof setGlobalErrorReporter>[0],) {
|
|
284
|
+
setGlobalErrorReporter(reporter);
|
|
285
|
+
},
|
|
286
|
+
get errorReporter(): ReturnType<typeof getGlobalErrorReporter> {
|
|
287
|
+
return getGlobalErrorReporter();
|
|
288
|
+
},
|
|
289
|
+
/** Bind the global messages provider (VineJS `vine.messagesProvider`). */
|
|
290
|
+
set messagesProvider(provider: MessagesProviderContract | null) {
|
|
291
|
+
setGlobalMessagesProvider(provider);
|
|
292
|
+
},
|
|
293
|
+
get messagesProvider(): MessagesProviderContract | null {
|
|
294
|
+
return getGlobalMessagesProvider();
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export default rune;
|
package/src/magic.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-based file type detection — magic numbers.
|
|
3
|
+
*
|
|
4
|
+
* Adonis rejects a `.exe` renamed to `.jpg` because its bodyparser sniffs the
|
|
5
|
+
* real bytes. rune validates a structural object, so on its own it can only
|
|
6
|
+
* trust what the upload DECLARES. This module closes that gap when a byte
|
|
7
|
+
* source is reachable: no external dependency, just the signature table and
|
|
8
|
+
* `node:fs` to read the first bytes.
|
|
9
|
+
*
|
|
10
|
+
* Signatures are matched at their documented offsets, longest-first, so a
|
|
11
|
+
* container that shares a prefix with another format is not mistaken for it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { open } from "node:fs/promises";
|
|
15
|
+
|
|
16
|
+
/** One magic-number signature. */
|
|
17
|
+
interface Signature {
|
|
18
|
+
/** File extension, lowercase, no dot. */
|
|
19
|
+
ext: string;
|
|
20
|
+
/** Canonical MIME type. */
|
|
21
|
+
mime: string;
|
|
22
|
+
/** Byte offset the pattern starts at. */
|
|
23
|
+
offset: number;
|
|
24
|
+
/** Bytes to match; `null` matches any byte at that position. */
|
|
25
|
+
bytes: ReadonlyArray<number | null>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ascii = (text: string): number[] =>
|
|
29
|
+
[...text].map((char) => char.charCodeAt(0));
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ordered longest-pattern-first within an offset group. `zip` sits LAST of the
|
|
33
|
+
* PK family because docx/xlsx/pptx are zip containers: matching `zip` first
|
|
34
|
+
* would label every Office document a zip.
|
|
35
|
+
*/
|
|
36
|
+
const SIGNATURES: readonly Signature[] = [
|
|
37
|
+
{
|
|
38
|
+
ext: "png",
|
|
39
|
+
mime: "image/png",
|
|
40
|
+
offset: 0,
|
|
41
|
+
bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
|
|
42
|
+
},
|
|
43
|
+
{ ext: "jpg", mime: "image/jpeg", offset: 0, bytes: [0xff, 0xd8, 0xff] },
|
|
44
|
+
{ ext: "gif", mime: "image/gif", offset: 0, bytes: ascii("GIF8") },
|
|
45
|
+
{ ext: "bmp", mime: "image/bmp", offset: 0, bytes: ascii("BM") },
|
|
46
|
+
{ ext: "webp", mime: "image/webp", offset: 8, bytes: ascii("WEBP") },
|
|
47
|
+
{ ext: "avif", mime: "image/avif", offset: 4, bytes: ascii("ftypavif") },
|
|
48
|
+
{ ext: "heic", mime: "image/heic", offset: 4, bytes: ascii("ftypheic") },
|
|
49
|
+
{
|
|
50
|
+
ext: "tif",
|
|
51
|
+
mime: "image/tiff",
|
|
52
|
+
offset: 0,
|
|
53
|
+
bytes: [0x49, 0x49, 0x2a, 0x00],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
ext: "tif",
|
|
57
|
+
mime: "image/tiff",
|
|
58
|
+
offset: 0,
|
|
59
|
+
bytes: [0x4d, 0x4d, 0x00, 0x2a],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
ext: "ico",
|
|
63
|
+
mime: "image/x-icon",
|
|
64
|
+
offset: 0,
|
|
65
|
+
bytes: [0x00, 0x00, 0x01, 0x00],
|
|
66
|
+
},
|
|
67
|
+
{ ext: "pdf", mime: "application/pdf", offset: 0, bytes: ascii("%PDF-") },
|
|
68
|
+
{ ext: "mp4", mime: "video/mp4", offset: 4, bytes: ascii("ftyp") },
|
|
69
|
+
{
|
|
70
|
+
ext: "webm",
|
|
71
|
+
mime: "video/webm",
|
|
72
|
+
offset: 0,
|
|
73
|
+
bytes: [0x1a, 0x45, 0xdf, 0xa3],
|
|
74
|
+
},
|
|
75
|
+
{ ext: "ogg", mime: "audio/ogg", offset: 0, bytes: ascii("OggS") },
|
|
76
|
+
{ ext: "wav", mime: "audio/wav", offset: 8, bytes: ascii("WAVE") },
|
|
77
|
+
{ ext: "mp3", mime: "audio/mpeg", offset: 0, bytes: ascii("ID3") },
|
|
78
|
+
{ ext: "mp3", mime: "audio/mpeg", offset: 0, bytes: [0xff, 0xfb] },
|
|
79
|
+
{ ext: "flac", mime: "audio/flac", offset: 0, bytes: ascii("fLaC") },
|
|
80
|
+
{ ext: "gz", mime: "application/gzip", offset: 0, bytes: [0x1f, 0x8b] },
|
|
81
|
+
{ ext: "bz2", mime: "application/x-bzip2", offset: 0, bytes: ascii("BZh") },
|
|
82
|
+
{
|
|
83
|
+
ext: "7z",
|
|
84
|
+
mime: "application/x-7z-compressed",
|
|
85
|
+
offset: 0,
|
|
86
|
+
bytes: [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c],
|
|
87
|
+
},
|
|
88
|
+
{ ext: "rar", mime: "application/vnd.rar", offset: 0, bytes: ascii("Rar!") },
|
|
89
|
+
{
|
|
90
|
+
ext: "zip",
|
|
91
|
+
mime: "application/zip",
|
|
92
|
+
offset: 0,
|
|
93
|
+
bytes: [0x50, 0x4b, 0x03, 0x04],
|
|
94
|
+
},
|
|
95
|
+
// Executables — the whole point of sniffing: these must never pass as media.
|
|
96
|
+
{
|
|
97
|
+
ext: "exe",
|
|
98
|
+
mime: "application/vnd.microsoft.portable-executable",
|
|
99
|
+
offset: 0,
|
|
100
|
+
bytes: ascii("MZ"),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
ext: "elf",
|
|
104
|
+
mime: "application/x-elf",
|
|
105
|
+
offset: 0,
|
|
106
|
+
bytes: [0x7f, 0x45, 0x4c, 0x46],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
ext: "class",
|
|
110
|
+
mime: "application/java-vm",
|
|
111
|
+
offset: 0,
|
|
112
|
+
bytes: [0xca, 0xfe, 0xba, 0xbe],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
ext: "wasm",
|
|
116
|
+
mime: "application/wasm",
|
|
117
|
+
offset: 0,
|
|
118
|
+
bytes: [0x00, 0x61, 0x73, 0x6d],
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/** How many leading bytes are enough for every signature above. */
|
|
123
|
+
export const MAGIC_HEAD_BYTES = 32;
|
|
124
|
+
|
|
125
|
+
/** The detected type of a byte head, or `null` when nothing matches. */
|
|
126
|
+
export interface DetectedType {
|
|
127
|
+
ext: string;
|
|
128
|
+
mime: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Does `head` carry `signature` at its documented offset? */
|
|
132
|
+
function matches(head: Uint8Array, signature: Signature): boolean {
|
|
133
|
+
if (head.length < signature.offset + signature.bytes.length) return false;
|
|
134
|
+
return signature.bytes.every((byte, index) => {
|
|
135
|
+
if (byte === null) return true;
|
|
136
|
+
return head[signature.offset + index] === byte;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Detect the type of a byte head. Returns `null` when no signature matches —
|
|
142
|
+
* never a guess: an unrecognised head must be reported as unrecognised, not
|
|
143
|
+
* silently accepted as whatever was declared.
|
|
144
|
+
*/
|
|
145
|
+
export function detectFileType(head: Uint8Array): DetectedType | null {
|
|
146
|
+
for (const signature of SIGNATURES) {
|
|
147
|
+
if (matches(head, signature))
|
|
148
|
+
return { ext: signature.ext, mime: signature.mime };
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Read the leading bytes of a file. */
|
|
154
|
+
export async function readHead(
|
|
155
|
+
path: string,
|
|
156
|
+
length = MAGIC_HEAD_BYTES,
|
|
157
|
+
): Promise<Uint8Array> {
|
|
158
|
+
const handle = await open(path, "r");
|
|
159
|
+
try {
|
|
160
|
+
const buffer = new Uint8Array(length);
|
|
161
|
+
const { bytesRead } = await handle.read(buffer, 0, length, 0);
|
|
162
|
+
return buffer.subarray(0, bytesRead);
|
|
163
|
+
} finally {
|
|
164
|
+
await handle.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Extensions that a given detected type legitimately covers. */
|
|
169
|
+
const ALIASES: Record<string, readonly string[]> = {
|
|
170
|
+
jpg: ["jpg", "jpeg"],
|
|
171
|
+
tif: ["tif", "tiff"],
|
|
172
|
+
// A zip container is what an Office document actually is on the wire.
|
|
173
|
+
zip: ["zip", "docx", "xlsx", "pptx", "odt", "ods", "odp", "epub", "jar"],
|
|
174
|
+
mp4: ["mp4", "m4v", "m4a", "mov"],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** Is `declared` an acceptable spelling of `detected`? */
|
|
178
|
+
export function extensionMatches(detected: string, declared: string): boolean {
|
|
179
|
+
const normalized = declared.replace(/^\./, "").toLowerCase();
|
|
180
|
+
return (ALIASES[detected] ?? [detected]).includes(normalized);
|
|
181
|
+
}
|
package/src/native.ts
CHANGED
|
@@ -58,28 +58,43 @@ export function isNativeAvailable(): boolean {
|
|
|
58
58
|
return native !== undefined;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
/** Why the engine could not be loaded, phrased for whoever has to fix it. */
|
|
62
|
+
function unavailableReason(): string {
|
|
63
|
+
const target = `${platform}-${arch}`;
|
|
64
|
+
if (loadError !== undefined) {
|
|
65
|
+
return `failed to load (${loadError instanceof Error ? loadError.message : String(loadError)})`;
|
|
66
|
+
}
|
|
67
|
+
return platformMap[target] !== undefined
|
|
68
|
+
? "binary not found"
|
|
69
|
+
: `no prebuilt binary for ${target}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Raised when a schema needs the Rust engine and it is not there. */
|
|
73
|
+
export class RuneNativeRequiredError extends Error {
|
|
74
|
+
readonly code = "RUNE_NAPI_REQUIRED" as const;
|
|
75
|
+
constructor() {
|
|
76
|
+
super(
|
|
77
|
+
`[RUNE_NAPI_REQUIRED] The Rust validation engine is required but not loaded — ${unavailableReason()}.\n` +
|
|
78
|
+
"Install the prebuilt binary for this platform, or build it with `pnpm build:napi`.",
|
|
79
|
+
);
|
|
80
|
+
this.name = "RuneNativeRequiredError";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
62
83
|
|
|
63
84
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
85
|
+
* Refuse to validate without the native engine.
|
|
86
|
+
*
|
|
87
|
+
* There is a TypeScript validator, and it used to take over silently with a
|
|
88
|
+
* one-time warning. That made a schema's verdict depend on whether a prebuilt
|
|
89
|
+
* binary happened to load: two deployments of the same code could disagree on
|
|
90
|
+
* whether a payload is valid, and the one that fell back lost the reason to
|
|
91
|
+
* have Rust at all. A schema the engine can run is now run BY the engine or
|
|
92
|
+
* not at all.
|
|
93
|
+
*
|
|
94
|
+
* The TypeScript path stays for what the engine genuinely cannot do — custom
|
|
95
|
+
* rules, a translator, a messages provider — where it is the only
|
|
96
|
+
* implementation, not a second one.
|
|
69
97
|
*/
|
|
70
|
-
export function
|
|
71
|
-
if (
|
|
72
|
-
warnedFallback = true;
|
|
73
|
-
const target = `${platform}-${arch}`;
|
|
74
|
-
const reason =
|
|
75
|
-
loadError !== undefined
|
|
76
|
-
? `failed to load (${loadError instanceof Error ? loadError.message : String(loadError)})`
|
|
77
|
-
: platformMap[target] !== undefined
|
|
78
|
-
? "binary not found"
|
|
79
|
-
: `no prebuilt binary for ${target}`;
|
|
80
|
-
console.warn(
|
|
81
|
-
`[rune] native validation engine unavailable — ${reason}. Falling back to the ` +
|
|
82
|
-
"TypeScript validator, whose results can differ subtly from the Rust engine — " +
|
|
83
|
-
"validation is platform-dependent for this process.",
|
|
84
|
-
);
|
|
98
|
+
export function assertNativeAvailable(): void {
|
|
99
|
+
if (native === undefined) throw new RuneNativeRequiredError();
|
|
85
100
|
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @c9up/rune/types
|
|
3
|
+
* @description
|
|
4
|
+
* Type-only surface, mirroring `@vinejs/vine/types`.
|
|
5
|
+
*
|
|
6
|
+
* A separate subpath so a consumer can `import type { FieldContext } from
|
|
7
|
+
* "@c9up/rune/types"` exactly as it would from Vine — importing types through
|
|
8
|
+
* the value entrypoint pulls the whole module graph into a file that only
|
|
9
|
+
* needed a signature.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type { DateFormat } from "./date.js";
|
|
13
|
+
export type {
|
|
14
|
+
AlphaOptions,
|
|
15
|
+
EmailOptions,
|
|
16
|
+
NormalizeEmailOptions,
|
|
17
|
+
NormalizeUrlOptions,
|
|
18
|
+
UrlOptions,
|
|
19
|
+
} from "./formats.js";
|
|
20
|
+
export type {
|
|
21
|
+
MessageArgs,
|
|
22
|
+
MessagesProviderContract,
|
|
23
|
+
ValidationFields,
|
|
24
|
+
ValidationMessages,
|
|
25
|
+
} from "./MessagesProvider.js";
|
|
26
|
+
export type {
|
|
27
|
+
AsyncCompiledRule,
|
|
28
|
+
AsyncRuleValidator,
|
|
29
|
+
CompiledRule,
|
|
30
|
+
ConditionalBranch,
|
|
31
|
+
ConditionalGroup,
|
|
32
|
+
CreateRuleOptions,
|
|
33
|
+
DatabaseLookup,
|
|
34
|
+
DatabaseResolver,
|
|
35
|
+
DatabaseRuleOptions,
|
|
36
|
+
ErrorReporterContract,
|
|
37
|
+
ErrorReporterFactory,
|
|
38
|
+
FieldContext,
|
|
39
|
+
FileLike,
|
|
40
|
+
HostResolver,
|
|
41
|
+
Infer,
|
|
42
|
+
JsonSchemaModifier,
|
|
43
|
+
ParseContext,
|
|
44
|
+
RuleChain,
|
|
45
|
+
RuleDef,
|
|
46
|
+
RuleValidator,
|
|
47
|
+
SchemaIntrospection,
|
|
48
|
+
UnionBranch,
|
|
49
|
+
ValidateOptions,
|
|
50
|
+
ValidationError,
|
|
51
|
+
ValidationMessageParams,
|
|
52
|
+
ValidationResult,
|
|
53
|
+
ValidationSchema,
|
|
54
|
+
ValidationTranslator,
|
|
55
|
+
} from "./Schema.js";
|