@c9up/rune 0.1.6 → 0.1.8
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 +45 -0
- package/dist/MessagesProvider.d.ts.map +1 -0
- package/dist/MessagesProvider.js +77 -0
- package/dist/MessagesProvider.js.map +1 -0
- package/dist/Schema.d.ts +912 -38
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +2841 -219
- 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 +42 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +37 -0
- package/dist/errors.js.map +1 -1
- package/dist/formats.d.ts +149 -0
- package/dist/formats.d.ts.map +1 -0
- package/dist/formats.js +612 -0
- package/dist/formats.js.map +1 -0
- package/dist/index.d.ts +152 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +181 -2
- 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/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 +11 -1
- package/src/MessagesProvider.ts +115 -0
- package/src/Schema.ts +3970 -215
- package/src/date.ts +320 -0
- package/src/errors.ts +59 -0
- package/src/formats.ts +721 -0
- package/src/index.ts +266 -1
- package/src/magic.ts +181 -0
- package/src/types.ts +55 -0
package/src/index.ts
CHANGED
|
@@ -4,10 +4,33 @@
|
|
|
4
4
|
* @implements FR38, FR39, FR40, FR41, FR42
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export {
|
|
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";
|
|
14
|
+
export { RuneError, RuneValidationError } from "./errors.js";
|
|
15
|
+
export type {
|
|
16
|
+
AlphaOptions,
|
|
17
|
+
EmailOptions,
|
|
18
|
+
NormalizeEmailOptions,
|
|
19
|
+
NormalizeUrlOptions,
|
|
20
|
+
UrlOptions,
|
|
21
|
+
} from "./formats.js";
|
|
22
|
+
export type { MessagesProviderContract } from "./MessagesProvider.js";
|
|
23
|
+
export { SimpleMessagesProvider } from "./MessagesProvider.js";
|
|
8
24
|
export type {
|
|
25
|
+
AsyncCompiledRule,
|
|
26
|
+
AsyncRuleValidator,
|
|
9
27
|
CompiledRule,
|
|
28
|
+
CreateRuleOptions,
|
|
29
|
+
DatabaseLookup,
|
|
30
|
+
DatabaseResolver,
|
|
31
|
+
DatabaseRuleOptions,
|
|
10
32
|
FieldContext,
|
|
33
|
+
Infer,
|
|
11
34
|
RuleChain,
|
|
12
35
|
RuleValidator,
|
|
13
36
|
ValidateOptions,
|
|
@@ -18,9 +41,251 @@ export type {
|
|
|
18
41
|
ValidationTranslator,
|
|
19
42
|
} from "./Schema.js";
|
|
20
43
|
export {
|
|
44
|
+
bindDatabase,
|
|
45
|
+
bindHostResolver,
|
|
21
46
|
bindRosetta,
|
|
47
|
+
compile,
|
|
48
|
+
create,
|
|
49
|
+
createAsyncRule,
|
|
22
50
|
createRule,
|
|
23
51
|
rules,
|
|
24
52
|
schema,
|
|
53
|
+
setConvertEmptyStringsToNull,
|
|
54
|
+
setDateTransform,
|
|
55
|
+
setGlobalErrorReporter,
|
|
25
56
|
setValidationTranslator,
|
|
26
57
|
} from "./Schema.js";
|
|
58
|
+
|
|
59
|
+
import type { MessagesProviderContract } from "./MessagesProvider.js";
|
|
60
|
+
import type { RuleChain, ValidateOptions } from "./Schema.js";
|
|
61
|
+
import {
|
|
62
|
+
bindDatabase,
|
|
63
|
+
bindHostResolver,
|
|
64
|
+
bindRosetta,
|
|
65
|
+
compile,
|
|
66
|
+
create,
|
|
67
|
+
createAsyncRule,
|
|
68
|
+
createRule,
|
|
69
|
+
getConvertEmptyStringsToNull,
|
|
70
|
+
getGlobalErrorReporter,
|
|
71
|
+
getGlobalMessagesProvider,
|
|
72
|
+
group,
|
|
73
|
+
groupElse,
|
|
74
|
+
groupIf,
|
|
75
|
+
rules,
|
|
76
|
+
schema,
|
|
77
|
+
setConvertEmptyStringsToNull,
|
|
78
|
+
setDateTransform,
|
|
79
|
+
setGlobalErrorReporter,
|
|
80
|
+
setGlobalMessagesProvider,
|
|
81
|
+
setValidationTranslator,
|
|
82
|
+
} from "./Schema.js";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A validator whose entry points REQUIRE `{ meta }`.
|
|
86
|
+
*
|
|
87
|
+
* VineJS refuses `validate(data)` once `withMetaData<T>()` declared metadata as
|
|
88
|
+
* required; keeping `meta` optional meant the guard existed at runtime but the
|
|
89
|
+
* compiler still waved the missing-metadata call through.
|
|
90
|
+
*/
|
|
91
|
+
type WithRequiredMeta<V, M> = {
|
|
92
|
+
[K in keyof V]: V[K] extends (data: infer D, options?: infer O) => infer R
|
|
93
|
+
? (data: D, options: Omit<O & object, "meta"> & { meta: M }) => R
|
|
94
|
+
: V[K];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Default export, mirroring `import vine from '@vinejs/vine'`.
|
|
99
|
+
*
|
|
100
|
+
* Named `rune`, not `vine` — the deviation is the package name, nothing else:
|
|
101
|
+
* the type factories (`rune.string()`, `rune.date()`, …) and the validator
|
|
102
|
+
* factories (`rune.create`, `rune.compile`) sit on one object exactly like
|
|
103
|
+
* VineJS, so an Adonis validator transcribes line for line.
|
|
104
|
+
*/
|
|
105
|
+
const rune = {
|
|
106
|
+
...rules,
|
|
107
|
+
schema,
|
|
108
|
+
create,
|
|
109
|
+
compile,
|
|
110
|
+
createRule,
|
|
111
|
+
createAsyncRule,
|
|
112
|
+
// `group` carries its branch factories, mirroring `vine.group.if/else`.
|
|
113
|
+
group: Object.assign(group, {
|
|
114
|
+
if: groupIf,
|
|
115
|
+
else: groupElse,
|
|
116
|
+
otherwise: groupElse,
|
|
117
|
+
}),
|
|
118
|
+
bindDatabase,
|
|
119
|
+
bindHostResolver,
|
|
120
|
+
bindRosetta,
|
|
121
|
+
setDateTransform,
|
|
122
|
+
setValidationTranslator,
|
|
123
|
+
/**
|
|
124
|
+
* Convert `""` to `null` before validating (VineJS
|
|
125
|
+
* `convertEmptyStringsToNull`). An HTML form posts empty inputs as `""`, and
|
|
126
|
+
* an optional field should read that as "absent", not as a present empty
|
|
127
|
+
* string that fails `minLength`.
|
|
128
|
+
*/
|
|
129
|
+
set convertEmptyStringsToNull(enabled: boolean) {
|
|
130
|
+
setConvertEmptyStringsToNull(enabled);
|
|
131
|
+
},
|
|
132
|
+
get convertEmptyStringsToNull(): boolean {
|
|
133
|
+
return getConvertEmptyStringsToNull();
|
|
134
|
+
},
|
|
135
|
+
/**
|
|
136
|
+
* Predicate helpers VineJS exposes as `vine.helpers`, for writing custom
|
|
137
|
+
* rules without reimplementing the same three checks each time.
|
|
138
|
+
*/
|
|
139
|
+
helpers: {
|
|
140
|
+
/** `true` for `true`, `1`, `"1"`, `"true"`, `"on"`, `"yes"`. */
|
|
141
|
+
isTrue: (value: unknown): boolean =>
|
|
142
|
+
value === true ||
|
|
143
|
+
value === 1 ||
|
|
144
|
+
(typeof value === "string" &&
|
|
145
|
+
["1", "true", "on", "yes"].includes(value.toLowerCase())),
|
|
146
|
+
/** `true` for `false`, `0`, `"0"`, `"false"`, `"off"`, `"no"`. */
|
|
147
|
+
isFalse: (value: unknown): boolean =>
|
|
148
|
+
value === false ||
|
|
149
|
+
value === 0 ||
|
|
150
|
+
(typeof value === "string" &&
|
|
151
|
+
["0", "false", "off", "no"].includes(value.toLowerCase())),
|
|
152
|
+
/** Neither `undefined` nor `null` (an empty string IS defined). */
|
|
153
|
+
exists: (value: unknown): boolean => value !== undefined && value !== null,
|
|
154
|
+
/** `undefined` or `null`. */
|
|
155
|
+
isMissing: (value: unknown): boolean =>
|
|
156
|
+
value === undefined || value === null,
|
|
157
|
+
/** A plain object, not an array and not `null`. */
|
|
158
|
+
isObject: (value: unknown): value is Record<string, unknown> =>
|
|
159
|
+
typeof value === "object" && value !== null && !Array.isArray(value),
|
|
160
|
+
/** An array. */
|
|
161
|
+
isArray: Array.isArray,
|
|
162
|
+
/** Every listed key is present on the object (VineJS `helpers.hasKeys`). */
|
|
163
|
+
hasKeys: (value: unknown, keys: readonly string[]): boolean =>
|
|
164
|
+
typeof value === "object" &&
|
|
165
|
+
value !== null &&
|
|
166
|
+
keys.every((key) => key in value),
|
|
167
|
+
/**
|
|
168
|
+
* No duplicate in the data set, optionally compared on one or more fields
|
|
169
|
+
* (VineJS `helpers.isDistinct`). `null` / `undefined` items are ignored,
|
|
170
|
+
* and items missing a compared key are SKIPPED, so two absent values are
|
|
171
|
+
* not a duplicate of each other.
|
|
172
|
+
*/
|
|
173
|
+
isDistinct: (
|
|
174
|
+
dataSet: readonly unknown[],
|
|
175
|
+
fields?: string | string[],
|
|
176
|
+
): boolean => {
|
|
177
|
+
const list = fields === undefined ? null : [fields].flat();
|
|
178
|
+
const keys: string[] = [];
|
|
179
|
+
for (const item of dataSet) {
|
|
180
|
+
if (item === null || item === undefined) continue;
|
|
181
|
+
if (list === null) {
|
|
182
|
+
keys.push(JSON.stringify(item));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (typeof item !== "object" || item === null) continue;
|
|
186
|
+
const record: Record<string, unknown> = { ...item };
|
|
187
|
+
if (list.some((k) => record[k] === undefined || record[k] === null)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
keys.push(JSON.stringify(list.map((k) => record[k])));
|
|
191
|
+
}
|
|
192
|
+
return new Set(keys).size === keys.length;
|
|
193
|
+
},
|
|
194
|
+
/** Read a dotted path off the validated data (VineJS `helpers.getNestedValue`). */
|
|
195
|
+
getNestedValue: (
|
|
196
|
+
key: string,
|
|
197
|
+
field: { data: Record<string, unknown> },
|
|
198
|
+
): unknown => {
|
|
199
|
+
let cursor: unknown = field.data;
|
|
200
|
+
for (const segment of key.split(".")) {
|
|
201
|
+
if (typeof cursor !== "object" || cursor === null) return undefined;
|
|
202
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
203
|
+
}
|
|
204
|
+
return cursor;
|
|
205
|
+
},
|
|
206
|
+
/** Make every property of a shape optional (VineJS `helpers.optional`). */
|
|
207
|
+
optional: (props: Record<string, RuleChain>): Record<string, RuleChain> =>
|
|
208
|
+
Object.fromEntries(
|
|
209
|
+
Object.entries(props).map(([key, chain]) => [
|
|
210
|
+
key,
|
|
211
|
+
chain.clone().optional(),
|
|
212
|
+
]),
|
|
213
|
+
),
|
|
214
|
+
},
|
|
215
|
+
/** One-shot validation, VineJS `vine.validate({ schema, data })`. */
|
|
216
|
+
validate<T extends Record<string, RuleChain>>(
|
|
217
|
+
options: { schema: T | RuleChain; data: unknown } & ValidateOptions,
|
|
218
|
+
) {
|
|
219
|
+
// Spread the whole ValidateOptions rather than re-listing its keys: the
|
|
220
|
+
// hand-listed version silently dropped `errorReporter` when it was added.
|
|
221
|
+
const { schema: fields, data, ...validateOptions } = options;
|
|
222
|
+
return create(fields as T).validate(data, validateOptions);
|
|
223
|
+
},
|
|
224
|
+
/** One-shot non-throwing validation, VineJS `vine.tryValidate`. */
|
|
225
|
+
tryValidate<T extends Record<string, RuleChain>>(
|
|
226
|
+
options: { schema: T | RuleChain; data: unknown } & ValidateOptions,
|
|
227
|
+
) {
|
|
228
|
+
// Spread the whole ValidateOptions rather than re-listing its keys: the
|
|
229
|
+
// hand-listed version silently dropped `errorReporter` when it was added.
|
|
230
|
+
const { schema: fields, data, ...validateOptions } = options;
|
|
231
|
+
return create(fields as T).tryValidate(data, validateOptions);
|
|
232
|
+
},
|
|
233
|
+
/**
|
|
234
|
+
* Type the `meta` passed to `validate(data, { meta })` (VineJS
|
|
235
|
+
* `withMetaData`). Purely a typing seam — rune carries meta on the call.
|
|
236
|
+
*/
|
|
237
|
+
withMetaData<M extends Record<string, unknown>>(
|
|
238
|
+
validateMeta?: (meta: M) => void,
|
|
239
|
+
): {
|
|
240
|
+
create<T extends Record<string, RuleChain>>(
|
|
241
|
+
fields: T,
|
|
242
|
+
): WithRequiredMeta<ReturnType<typeof create<T>>, M>;
|
|
243
|
+
} {
|
|
244
|
+
return {
|
|
245
|
+
create<T extends Record<string, RuleChain>>(fields: T) {
|
|
246
|
+
const validator = create(fields);
|
|
247
|
+
if (!validateMeta) {
|
|
248
|
+
return validator as WithRequiredMeta<ReturnType<typeof create<T>>, M>;
|
|
249
|
+
}
|
|
250
|
+
// The callback runs BEFORE the payload: meta that is wrong makes
|
|
251
|
+
// every rule reading it meaningless, so failing early is the only
|
|
252
|
+
// honest outcome.
|
|
253
|
+
const guard = <A extends unknown[], R>(
|
|
254
|
+
run: (...args: A) => R,
|
|
255
|
+
): ((...args: A) => R) => {
|
|
256
|
+
return (...args: A): R => {
|
|
257
|
+
const options = args[1] as { meta?: M } | undefined;
|
|
258
|
+
validateMeta((options?.meta ?? {}) as M);
|
|
259
|
+
return run(...args);
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
return {
|
|
263
|
+
...validator,
|
|
264
|
+
validate: guard(validator.validate),
|
|
265
|
+
validateResult: guard(validator.validateResult),
|
|
266
|
+
validateResultAsync: guard(validator.validateResultAsync),
|
|
267
|
+
validateOrThrow: guard(validator.validateOrThrow),
|
|
268
|
+
validateOrThrowAsync: guard(validator.validateOrThrowAsync),
|
|
269
|
+
tryValidate: guard(validator.tryValidate),
|
|
270
|
+
tryValidateSync: guard(validator.tryValidateSync),
|
|
271
|
+
} as WithRequiredMeta<ReturnType<typeof create<T>>, M>;
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
/** Process-wide error reporter (VineJS `vine.errorReporter`). */
|
|
276
|
+
set errorReporter(reporter: Parameters<typeof setGlobalErrorReporter>[0],) {
|
|
277
|
+
setGlobalErrorReporter(reporter);
|
|
278
|
+
},
|
|
279
|
+
get errorReporter(): ReturnType<typeof getGlobalErrorReporter> {
|
|
280
|
+
return getGlobalErrorReporter();
|
|
281
|
+
},
|
|
282
|
+
/** Bind the global messages provider (VineJS `vine.messagesProvider`). */
|
|
283
|
+
set messagesProvider(provider: MessagesProviderContract | null) {
|
|
284
|
+
setGlobalMessagesProvider(provider);
|
|
285
|
+
},
|
|
286
|
+
get messagesProvider(): MessagesProviderContract | null {
|
|
287
|
+
return getGlobalMessagesProvider();
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
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/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";
|