Part 4:  Validation, Errors & Results

Shape + format validation, the `Result<T, E>` functional error pattern (`Ok`/`Err`/`ResultUtils`), typed errors (`PurlError`, `PurlInjectionError`), and per-ecosystem rule plumbing.

Topics: Anatomy Building Parsing Validation URL Ecosystems Comparison Security Architecture Builders Contributing Converters Hardening Release Tour VERS
src/validate.ts
17 sections
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Section 1 of 17
5import { PurlError, PurlInjectionError } from './error.js'
6import { isNullishOrEmptyString } from './lang.js'
7import {
8 ArrayIsArray,
9 ObjectKeys,
10 ReflectApply,
11 StringPrototypeCharCodeAt,
12 StringPrototypeIncludes,
13} from '@socketsecurity/lib/primordials'
14import {
15 findCommandInjectionCharCode,
16 findInjectionCharCode,
17 formatInjectionChar,
18 isNonEmptyString,
19} from './strings.js'
20
21import type { QualifiersObject } from './purl-component.js'
Section 2 of 17
26function validateEmptyByType(
27 type: string,
28 name: string,
29 value: unknown,
30 options?: { throws?: boolean | undefined } | boolean | undefined,
31): boolean {
32 // Support both legacy boolean parameter and new options object for backward compatibility
33 const { throws = false } =
34 typeof options === 'boolean' ? { throws: options } : (options ?? {})
35 if (!isNullishOrEmptyString(value)) {
36 if (throws) {
37 throw new PurlError(`${type} "${name}" component must be empty`)
38 }
39 return false
40 }
41 return true
42}
Section 3 of 17
52function validateNoInjectionByType(
53 type: string,
54 component: string,
55 value: string | undefined,
56 throws: boolean,
57): boolean {
58 if (typeof value === 'string') {
59 const code = findInjectionCharCode(value)
60 if (code !== -1) {
61 if (throws) {
62 throw new PurlInjectionError(
63 type,
64 component,
65 code,
66 formatInjectionChar(code),
67 )
68 }
69 return false
70 }
71 }
72 return true
73}
Section 4 of 17
79function validateName(
80 name: unknown,
81 options?: { throws?: boolean | undefined } | boolean | undefined,
82): boolean {
83 // Support both legacy boolean parameter and new options object for backward compatibility
84 const opts = typeof options === 'boolean' ? { throws: options } : options
85 const { throws = false } = opts ?? {}
86
87 // First validate it's a required string
88 if (
89 !validateRequired('name', name, opts) ||
90 !validateStrings('name', name, opts)
91 ) {
92 return false
93 }
94
95 // Validate length (npm package name limit is `214` characters)
96 const MAX_NAME_LENGTH = 214
97 if (typeof name === 'string' && name.length > MAX_NAME_LENGTH) {
98 if (throws) {
99 throw new PurlError(
100 `"name" exceeds maximum length of ${MAX_NAME_LENGTH} characters`,
101 )
102 }
103 return false
104 }
105
106 return true
107}
Section 5 of 17
113function validateNamespace(
114 namespace: unknown,
115 options?: { throws?: boolean | undefined } | boolean | undefined,
116): boolean {
117 // Support both legacy boolean parameter and new options object for backward compatibility
118 const opts = typeof options === 'boolean' ? { throws: options } : options
119 const { throws = false } = opts ?? {}
120
121 if (!validateStrings('namespace', namespace, opts)) {
122 return false
123 }
124
125 // Validate length (reasonable limit for namespace)
126 const MAX_NAMESPACE_LENGTH = 512
127 if (
128 typeof namespace === 'string' &&
129 namespace.length > MAX_NAMESPACE_LENGTH
130 ) {
131 if (throws) {
132 throw new PurlError(
133 `"namespace" exceeds maximum length of ${MAX_NAMESPACE_LENGTH} characters`,
134 )
135 }
136 return false
137 }
138
139 return true
140}
Section 6 of 17
146function validateQualifierKey(
147 key: string,
148 options?: { throws?: boolean | undefined } | boolean | undefined,
149): boolean {
150 // Support both legacy boolean parameter and new options object for backward compatibility
151 const opts = typeof options === 'boolean' ? { throws: options } : options
152 const { throws = false } = opts ?? {}
153 // Qualifier keys must not be empty
154 if (key.length === 0) {
155 if (throws) {
156 throw new PurlError('qualifier key must not be empty')
157 }
158 return false
159 }
160 // Qualifier keys must not exceed reasonable length
161 const MAX_QUALIFIER_KEY_LENGTH = 256
162 if (key.length > MAX_QUALIFIER_KEY_LENGTH) {
163 if (throws) {
164 throw new PurlError(
165 `qualifier key exceeds maximum length of ${MAX_QUALIFIER_KEY_LENGTH} characters`,
166 )
167 }
168 return false
169 }
170 // A key cannot start with a number
171 if (!validateStartsWithoutNumber('qualifier', key, opts)) {
172 return false
173 }
174 // The key must be composed only of ASCII letters and numbers,
175 // `'.'`, `'-'` and `'_'` (period, dash and underscore)
176 for (let i = 0, { length } = key as string; i < length; i += 1) {
177 const code = StringPrototypeCharCodeAt(key as string, i)
178 // biome-ignore format: newlines
179 if (
180 !(
181 // 0-9
182 (
183 (code >= 48 && code <= 57) ||
184 // A-Z
185 (code >= 65 && code <= 90) ||
186 // a-z
187 (code >= 97 && code <= 122) ||
188 // .
189 code === 46 ||
190 // -
191 code === 45 ||
192 code === 95
193 )
194 // _
195 )
196 ) {
197 if (throws) {
198 throw new PurlError(`qualifier key "${key}" must match [a-z0-9.\\-_]`)
199 }
200 return false
201 }
202 }
203 return true
204}
Section 7 of 17
210function validateQualifiers(
211 qualifiers: unknown,
212 options?: { throws?: boolean | undefined } | boolean | undefined,
213): boolean {
214 // Support both legacy boolean parameter and new options object for backward compatibility
215 const opts = typeof options === 'boolean' ? { throws: options } : options
216 const { throws = false } = opts ?? {}
217 if (qualifiers === null || qualifiers === undefined) {
218 return true
219 }
220 if (typeof qualifiers !== 'object' || ArrayIsArray(qualifiers)) {
221 if (throws) {
222 throw new PurlError('"qualifiers" must be a plain object')
223 }
224 return false
225 }
226 const qualifiersObj = qualifiers as QualifiersObject | URLSearchParams
227 const keysProperty = (qualifiersObj as QualifiersObject)['keys']
228 // type-coverage:ignore-next-line -- TypeScript correctly infers this type through the ternary and cast
229 const keysIterable: Iterable<string> =
230 // `URLSearchParams` instances have a `"keys"` method that returns an iterator
231 (
232 typeof keysProperty === 'function'
233 ? ReflectApply(keysProperty, qualifiersObj, [])
234 : ObjectKeys(qualifiers as QualifiersObject)
235 ) as Iterable<string>
236 // Use `for-of` to work with `URLSearchParams#keys` iterators
237 // type-coverage:ignore-next-line -- TypeScript correctly infers the iteration type
238 for (const key of keysIterable) {
239 if (typeof key !== 'string') {
240 if (throws) {
241 throw new PurlError('qualifier key must be a string')
242 }
243 return false
244 }
245 if (!validateQualifierKey(key, opts)) {
246 return false
247 }
248 // Validate qualifier values for command injection characters.
249 // Uses the narrower command injection scanner to allow URL-safe characters
250 // (`?`, `&`, `=`, `:`, `/`, `#`) that are legitimate in qualifier values like
251 // `download_url`, `repository_url`, and `vcs_url`.
252 const value =
253 typeof (qualifiersObj as QualifiersObject)[key] === 'string'
254 ? ((qualifiersObj as QualifiersObject)[key] as string)
255 : undefined
256 if (value !== undefined) {
257 // Qualifier values must not exceed reasonable length
258 const MAX_QUALIFIER_VALUE_LENGTH = 65536
259 if (value.length > MAX_QUALIFIER_VALUE_LENGTH) {
260 if (throws) {
261 throw new PurlError(
262 `qualifier "${key}" value exceeds maximum length of ${MAX_QUALIFIER_VALUE_LENGTH} characters`,
263 )
264 }
265 return false
266 }
267 const code = findCommandInjectionCharCode(value)
268 if (code !== -1) {
269 if (throws) {
270 throw new PurlInjectionError(
271 'purl',
272 `qualifier "${key}"`,
273 code,
274 formatInjectionChar(code),
275 )
276 }
277 return false
278 }
279 }
280 }
281 return true
282}
Section 8 of 17
288function validateRequired(
289 name: string,
290 value: unknown,
291 options?: { throws?: boolean | undefined } | boolean | undefined,
292): boolean {
293 // Support both legacy boolean parameter and new options object for backward compatibility
294 const { throws = false } =
295 typeof options === 'boolean' ? { throws: options } : (options ?? {})
296 if (isNullishOrEmptyString(value)) {
297 if (throws) {
298 throw new PurlError(`"${name}" is a required component`)
299 }
300 return false
301 }
302 return true
303}
Section 9 of 17
309function validateRequiredByType(
310 type: string,
311 name: string,
312 value: unknown,
313 options?: { throws?: boolean | undefined } | boolean | undefined,
314): boolean {
315 // Support both legacy boolean parameter and new options object for backward compatibility
316 const { throws = false } =
317 typeof options === 'boolean' ? { throws: options } : (options ?? {})
318 if (isNullishOrEmptyString(value)) {
319 if (throws) {
320 throw new PurlError(`${type} requires a "${name}" component`)
321 }
322 return false
323 }
324 return true
325}
Section 10 of 17
331function validateStartsWithoutNumber(
332 name: string,
333 value: string,
334 options?: { throws?: boolean | undefined } | boolean | undefined,
335): boolean {
336 // Support both legacy boolean parameter and new options object for backward compatibility
337 const { throws = false } =
338 typeof options === 'boolean' ? { throws: options } : (options ?? {})
339 if (isNonEmptyString(value)) {
340 const code = StringPrototypeCharCodeAt(value, 0)
Section 11 of 17
342 if (throws) {
Section 11 of 17
342 if (throws) {
343 throw new PurlError(`${name} "${value}" cannot start with a number`)
344 }
345 return false
346 }
347 }
348 return true
349}
Section 13 of 17
355function validateStrings(
356 name: string,
357 value: unknown,
358 options?: { throws?: boolean | undefined } | boolean | undefined,
359): boolean {
360 // Support both legacy boolean parameter and new options object for backward compatibility
361 const { throws = false } =
362 typeof options === 'boolean' ? { throws: options } : (options ?? {})
363 if (value === null || value === undefined) {
364 return true
365 }
366 if (typeof value !== 'string') {
367 if (throws) {
368 throw new PurlError(`"${name}" must be a string`)
369 }
370 return false
371 }
372 // Reject `null` bytes which cause truncation in C-based consumers
373 if (StringPrototypeIncludes(value, '\x00')) {
374 if (throws) {
375 throw new PurlError(`"${name}" must not contain null bytes`)
376 }
377 return false
378 }
379 return true
380}
Section 14 of 17
390function validateSubpath(
391 subpath: unknown,
392 options?: { throws?: boolean | undefined } | boolean | undefined,
393): boolean {
394 // Support both legacy boolean parameter and new options object for backward compatibility
395 const opts = typeof options === 'boolean' ? { throws: options } : options
396 const { throws = false } = opts ?? {}
397 if (!validateStrings('subpath', subpath, opts)) {
398 return false
399 }
400 if (typeof subpath === 'string') {
401 const code = findCommandInjectionCharCode(subpath)
402 if (code !== -1) {
403 if (throws) {
404 throw new PurlInjectionError(
405 'purl',
406 'subpath',
407 code,
408 formatInjectionChar(code),
409 )
410 }
411 return false
412 }
413 }
414 return true
415}
Section 15 of 17
421function validateType(
422 type: unknown,
423 options?: { throws?: boolean | undefined } | boolean | undefined,
424): boolean {
425 // Support both legacy boolean parameter and new options object for backward compatibility
426 const opts = typeof options === 'boolean' ? { throws: options } : options
427 const { throws = false } = opts ?? {}
428 // The type cannot be nullish, an empty string, or start with a number
429 if (
430 !validateRequired('type', type, opts) ||
431 !validateStrings('type', type, opts) ||
432 !validateStartsWithoutNumber('type', type as string, opts)
433 ) {
434 return false
435 }
436 // The package type is composed only of ASCII letters and numbers,
437 // `'.'` (period), and `'-'` (dash)
438 for (let i = 0, { length } = type as string; i < length; i += 1) {
439 const code = StringPrototypeCharCodeAt(type as string, i)
440 // biome-ignore format: newlines
441 if (
442 !(
443 // 0-9
444 (
445 (code >= 48 && code <= 57) ||
446 // A-Z
447 (code >= 65 && code <= 90) ||
448 // a-z
449 (code >= 97 && code <= 122) ||
450 // .
451 code === 46 ||
452 code === 45
453 )
454 // -
455 )
456 ) {
457 if (throws) {
458 throw new PurlError(`type "${type}" must match [A-Za-z0-9.\\-]`)
Section 16 of 17
460 }
461 return false
462 }
463 }
464 return true
465}
Section 17 of 17
474function validateVersion(
475 version: unknown,
476 options?: { throws?: boolean | undefined } | boolean | undefined,
477): boolean {
478 // Support both legacy boolean parameter and new options object for backward compatibility
479 const opts = typeof options === 'boolean' ? { throws: options } : options
480 const { throws = false } = opts ?? {}
481
482 if (!validateStrings('version', version, opts)) {
483 return false
484 }
485
486 // Validate length (reasonable limit for version strings)
487 const MAX_VERSION_LENGTH = 256
488 if (typeof version === 'string' && version.length > MAX_VERSION_LENGTH) {
489 if (throws) {
490 throw new PurlError(
491 `"version" exceeds maximum length of ${MAX_VERSION_LENGTH} characters`,
492 )
493 }
494 return false
495 }
496
497 // Reject command injection characters
498 if (typeof version === 'string') {
499 const code = findCommandInjectionCharCode(version)
500 if (code !== -1) {
501 if (throws) {
502 throw new PurlInjectionError(
503 'purl',
504 'version',
505 code,
506 formatInjectionChar(code),
507 )
508 }
509 return false
510 }
511 }
512
513 return true
514}
515
516export {
517 validateEmptyByType,
518 validateName,
519 validateNamespace,
520 validateNoInjectionByType,
521 validateQualifiers,
522 validateQualifierKey,
523 validateRequired,
524 validateRequiredByType,
525 validateStartsWithoutNumber,
526 validateStrings,
527 validateSubpath,
528 validateType,
529 validateVersion,
530}
src/result.ts
30 sections
Section 1 of 30
22import { ArrayPrototypePush, ErrorCtor } from '@socketsecurity/lib/primordials'
Section 2 of 30
28export type Result<T, E = Error> = Ok<T> | Err<E>
Section 3 of 30
37export class Ok<T> {
38 readonly kind = 'ok' as const
39 readonly value: T
40
41 constructor(value: T) {
42 this.value = value
43 }
Section 4 of 30
48 andThen<U, F>(fn: (_value: T) => Result<U, F>): Result<U, F> {
49 return fn(this.value)
50 }
Section 5 of 30
55 isErr(): boolean {
56 return false
57 }
Section 6 of 30
62 isOk(): this is Ok<T> {
63 return true
64 }
Section 7 of 30
69 map<U>(fn: (_value: T) => U): Result<U, never> {
70 return new Ok(fn(this.value))
71 }
Section 8 of 30
76 mapErr<F>(_fn: (_error: never) => F): Result<T, F> {
77 return this as unknown as Result<T, F>
78 }
Section 9 of 30
83 orElse<U>(_fn: (_error: never) => Result<U, never>): Result<T | U, never> {
84 return this
85 }
Section 10 of 30
90 unwrap(): T {
91 return this.value
92 }
Section 11 of 30
97 unwrapOr(_defaultValue: T): T {
98 return this.value
99 }
Section 12 of 30
104 unwrapOrElse(_fn: (_error: never) => T): T {
105 return this.value
106 }
107}
Section 13 of 30
112export class Err<E = Error> {
113 readonly kind = 'err' as const
114 readonly error: E
115
116 constructor(error: E) {
117 this.error = error
118 }
Section 14 of 30
123 andThen<U, F>(_fn: (_value: never) => Result<U, F>): Result<U, E | F> {
124 return this as unknown as Result<U, E | F>
125 }
Section 15 of 30
130 isErr(): this is Err<E> {
131 return true
132 }
Section 16 of 30
137 isOk(): boolean {
138 return false
139 }
Section 17 of 30
144 map<U>(_fn: (_value: never) => U): Result<U, E> {
145 return this as unknown as Result<U, E>
146 }
Section 18 of 30
151 mapErr<F>(fn: (_error: E) => F): Result<never, F> {
152 return new Err(fn(this.error))
153 }
Section 19 of 30
158 orElse<T, F>(fn: (_error: E) => Result<T, F>): Result<T, F> {
159 return fn(this.error)
160 }
Section 20 of 30
165 unwrap(): never {
166 if (this.error instanceof Error) {
167 throw this.error
168 }
169 throw new ErrorCtor(String(this.error))
170 }
Section 21 of 30
175 unwrapOr<T>(defaultValue: T): T {
176 return defaultValue
177 }
Section 22 of 30
182 unwrapOrElse<T>(fn: (_error: E) => T): T {
183 return fn(this.error)
184 }
185}
Section 23 of 30
190export function ok<T>(value: T): Ok<T> {
191 return new Ok(value)
192}
Section 24 of 30
197export function err<E = Error>(error: E): Err<E> {
198 return new Err(error)
199}
Section 25 of 30
204export const ResultUtils = {
Section 26 of 30
208 all<T extends ReadonlyArray<Result<unknown, unknown>>>(
209 results: T,
210 ): Result<
211 { [K in keyof T]: T[K] extends Result<infer U, any> ? U : never },
212 T[number] extends Result<any, infer E> ? E : never
213 > {
214 type ExtractedValues = {
215 [K in keyof T]: T[K] extends Result<infer U, any> ? U : never
216 }
217 type ExtractedValue = T[number] extends Result<infer U, any> ? U : never
218 type ExtractedError = T[number] extends Result<any, infer E> ? E : never
219 const values: ExtractedValue[] = []
220 for (let i = 0; i < results.length; i++) {
221 const result = results[i]!
222 if (result.isErr()) {
223 return result as unknown as Result<ExtractedValues, ExtractedError>
224 }
225 ArrayPrototypePush(values, (result as Ok<ExtractedValue>).value)
226 }
227 return ok(values as ExtractedValues)
228 },
Section 27 of 30
234 any<T extends ReadonlyArray<Result<unknown, unknown>>>(
235 results: T,
236 ): T[number] {
237 let lastError: Result<unknown, unknown> = err(
238 new ErrorCtor('No results provided'),
239 )
240 for (const result of results) {
241 if (result.isOk()) {
242 return result
243 }
244 lastError = result
245 }
246 return lastError as T[number]
247 },
Section 28 of 30
252 err: err,
Section 29 of 30
257 from<T>(fn: () => T): Result<T, Error> {
258 try {
259 return ok(fn())
260 } catch (e) {
261 return err(e instanceof Error ? e : new ErrorCtor(String(e)))
262 }
263 },
Section 30 of 30
268 ok: ok,
269}
src/error.ts
7 sections
1 2 3 4 5 6 7
Section 1 of 7
12function errorMessage(e: unknown): string {
13 if (e instanceof Error) {
14 return e.message || 'Unknown error'
15 }
16 if (e === null || e === undefined) {
17 return 'Unknown error'
18 }
19 return String(e) || 'Unknown error'
20}
Section 2 of 7
35function formatPurlErrorMessage(message = ''): string {
36 const { length } = message
37 let formatted = ''
38 if (length) {
39 // Lower case start of message
40 const code0 = StringPrototypeCharCodeAt(message, 0)
41 formatted =
Section 3 of 7
43 ? `${StringPrototypeToLowerCase(message[0]!)}${StringPrototypeSlice(message, 1)}`
Section 3 of 7
43 ? `${StringPrototypeToLowerCase(message[0]!)}${StringPrototypeSlice(message, 1)}`
44 : message
45 // Remove period from end of message
46 if (
47 length > 1 &&
Section 5 of 7
49 StringPrototypeCharCodeAt(message, length - 2) !== 46
50 ) {
51 formatted = StringPrototypeSlice(formatted, 0, -1)
52 }
53 }
54 return `Invalid purl: ${formatted}`
55}
Section 6 of 7
60class PurlError extends Error {
61 constructor(
62 message?: string | undefined,
63 options?: ErrorOptions | undefined,
64 ) {
65 super(formatPurlErrorMessage(message), options)
66 }
67}
Section 7 of 7
80class PurlInjectionError extends PurlError {
81 readonly charCode: number
82 readonly component: string
83 readonly purlType: string
84
85 constructor(
86 purlType: string,
87 component: string,
88 charCode: number,
89 charLabel: string,
90 ) {
91 super(
92 `${purlType} "${component}" component contains injection character ${charLabel}`,
93 )
94 this.charCode = charCode
95 this.component = component
96 this.purlType = purlType
97 ObjectFreeze(this)
98 }
99}
100ObjectFreeze(PurlInjectionError.prototype)
101
102export { errorMessage, formatPurlErrorMessage, PurlError, PurlInjectionError }
src/purl-type.ts
3 sections
1 2 3
Section 1 of 3
9import { PurlInjectionError } from './error.js'
10import { createHelpersNamespaceObject } from './helpers.js'
11import { findInjectionCharCode, formatInjectionChar } from './strings.js'
12import { normalize as alpmNormalize } from './purl-types/alpm.js'
13import { normalize as apkNormalize } from './purl-types/apk.js'
14import {
15 normalize as bazelNormalize,
16 validate as bazelValidate,
17} from './purl-types/bazel.js'
18import {
19 normalize as bitbucketNormalize,
20 validate as bitbucketValidate,
21} from './purl-types/bitbucket.js'
22import { normalize as bitnamiNormalize } from './purl-types/bitnami.js'
23import { validate as cargoValidate } from './purl-types/cargo.js'
24import { validate as cocoaodsValidate } from './purl-types/cocoapods.js'
25import { normalize as composerNormalize } from './purl-types/composer.js'
26import { validate as conanValidate } from './purl-types/conan.js'
27import {
28 normalize as condaNormalize,
29 validate as condaValidate,
30} from './purl-types/conda.js'
31import { validate as cpanValidate } from './purl-types/cpan.js'
32import { validate as cranValidate } from './purl-types/cran.js'
33import { normalize as debNormalize } from './purl-types/deb.js'
34import {
35 normalize as dockerNormalize,
36 validate as dockerValidate,
37} from './purl-types/docker.js'
38import { validate as gemValidate } from './purl-types/gem.js'
39import { normalize as genericNormalize } from './purl-types/generic.js'
40import {
41 normalize as githubNormalize,
42 validate as githubValidate,
43} from './purl-types/github.js'
44import {
45 normalize as gitlabNormalize,
46 validate as gitlabValidate,
47} from './purl-types/gitlab.js'
48import { validate as golangValidate } from './purl-types/golang.js'
49import {
50 normalize as hexNormalize,
51 validate as hexValidate,
52} from './purl-types/hex.js'
53import { normalize as huggingfaceNormalize } from './purl-types/huggingface.js'
54import {
55 normalize as juliaNormalize,
56 validate as juliaValidate,
57} from './purl-types/julia.js'
58import { normalize as luarocksNormalize } from './purl-types/luarocks.js'
59import { validate as mavenValidate } from './purl-types/maven.js'
60import {
61 normalize as mlflowNormalize,
62 validate as mlflowValidate,
63} from './purl-types/mlflow.js'
64import {
65 normalize as npmNormalize,
66 validate as npmValidate,
67} from './purl-types/npm.js'
68import { validate as nugetValidate } from './purl-types/nuget.js'
69import {
70 normalize as ociNormalize,
71 validate as ociValidate,
72} from './purl-types/oci.js'
73import { validate as opamValidate } from './purl-types/opam.js'
74import {
75 normalize as otpNormalize,
76 validate as otpValidate,
77} from './purl-types/otp.js'
78import {
79 normalize as pubNormalize,
80 validate as pubValidate,
81} from './purl-types/pub.js'
82import {
83 normalize as pypiNormalize,
84 validate as pypiValidate,
85} from './purl-types/pypi.js'
86import { normalize as qpkgNormalize } from './purl-types/qpkg.js'
87import { normalize as rpmNormalize } from './purl-types/rpm.js'
88import { normalize as socketNormalize } from './purl-types/socket.js'
89import { validate as swidValidate } from './purl-types/swid.js'
90import { validate as swiftValidate } from './purl-types/swift.js'
91import { normalize as unknownNormalize } from './purl-types/unknown.js'
92import {
93 normalize as vscodeExtensionNormalize,
94 validate as vscodeExtensionValidate,
95} from './purl-types/vscode-extension.js'
96import {
97 normalize as yoctoNormalize,
98 validate as yoctoValidate,
99} from './purl-types/yocto.js'
100
101interface PurlObject {
102 name: string
103 namespace?: string | undefined
104 qualifiers?: Record<string, string> | undefined
105 subpath?: string | undefined
106 type?: string | undefined
107 version?: string | undefined
108}
Section 2 of 3
113const PurlTypNormalizer = (purl: PurlObject): PurlObject => purl
Section 3 of 3
121function PurlTypeValidator(purl: PurlObject, throws: boolean): boolean {
122 const type = purl.type ?? 'unknown'
123 if (typeof purl.namespace === 'string') {
124 const nsCode = findInjectionCharCode(purl.namespace)
125 if (nsCode !== -1) {
126 if (throws) {
127 throw new PurlInjectionError(
128 type,
129 'namespace',
130 nsCode,
131 formatInjectionChar(nsCode),
132 )
133 }
134 return false
135 }
136 }
137 const nameCode = findInjectionCharCode(purl.name)
138 if (nameCode !== -1) {
139 if (throws) {
140 throw new PurlInjectionError(
141 type,
142 'name',
143 nameCode,
144 formatInjectionChar(nameCode),
145 )
146 }
147 return false
148 }
149 return true
150}
151
152// PURL types:
153// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst
154const PurlType = createHelpersNamespaceObject(
155 {
156 normalize: {
157 alpm: alpmNormalize,
158 apk: apkNormalize,
159 bazel: bazelNormalize,
160 bitbucket: bitbucketNormalize,
161 bitnami: bitnamiNormalize,
162 composer: composerNormalize,
163 conda: condaNormalize,
164 deb: debNormalize,
165 docker: dockerNormalize,
166 generic: genericNormalize,
167 github: githubNormalize,
168 gitlab: gitlabNormalize,
169 hex: hexNormalize,
170 huggingface: huggingfaceNormalize,
171 julia: juliaNormalize,
172 luarocks: luarocksNormalize,
173 mlflow: mlflowNormalize,
174 npm: npmNormalize,
175 oci: ociNormalize,
176 otp: otpNormalize,
177 pub: pubNormalize,
178 pypi: pypiNormalize,
179 qpkg: qpkgNormalize,
180 rpm: rpmNormalize,
181 socket: socketNormalize,
182 unknown: unknownNormalize,
183 'vscode-extension': vscodeExtensionNormalize,
184 yocto: yoctoNormalize,
185 },
186 validate: {
187 bazel: bazelValidate,
188 bitbucket: bitbucketValidate,
189 cargo: cargoValidate,
190 cocoapods: cocoaodsValidate,
191 conda: condaValidate,
192 conan: conanValidate,
193 cpan: cpanValidate,
194 cran: cranValidate,
195 docker: dockerValidate,
196 gem: gemValidate,
197 github: githubValidate,
198 gitlab: gitlabValidate,
199 golang: golangValidate,
200 hex: hexValidate,
201 julia: juliaValidate,
202 maven: mavenValidate,
203 mlflow: mlflowValidate,
204 npm: npmValidate,
205 nuget: nugetValidate,
206 oci: ociValidate,
207 opam: opamValidate,
208 otp: otpValidate,
209 pub: pubValidate,
210 pypi: pypiValidate,
211 swift: swiftValidate,
212 swid: swidValidate,
213 'vscode-extension': vscodeExtensionValidate,
214 yocto: yoctoValidate,
215 },
216 },
217 {
218 normalize: PurlTypNormalizer,
219 validate: PurlTypeValidator,
220 },
221)
222
223export { PurlType }