Part 1:  Anatomy of a PURL

Understand the `pkg:type/ns/name@version?q#sub` shape and how it maps to the `PackageURL` class.

Topics: Anatomy Building Parsing Validation URL Ecosystems Comparison Security Architecture Builders Contributing Converters Hardening Release Tour VERS
src/package-url.ts
33 sections
Section 1 of 33
22import {
23 _registerPackageURL,
24 compare as comparePurls,
25 equals as equalsPurls,
26} from './compare.js'
27import { decodePurlComponent } from './decode.js'
28import { PurlError } from './error.js'
29import {
30 normalizeName,
31 normalizeNamespace,
32 normalizeQualifiers,
33 normalizeSubpath,
34 normalizeType,
35 normalizeVersion,
36} from './normalize.js'
37import { isObject, recursiveFreeze } from './objects.js'
38import {
39 ArrayIsArray,
40 BufferByteLength,
41 ErrorCtor,
42 JSONParse,
43 JSONStringify,
44 MapCtor,
45 ObjectCreate,
46 ObjectFreeze,
47 ObjectKeys,
48 ReflectDefineProperty,
49 ReflectGetOwnPropertyDescriptor,
50 ReflectSetPrototypeOf,
51 RegExpPrototypeTest,
52 StringPrototypeCharCodeAt,
53 StringPrototypeIncludes,
54 StringPrototypeIndexOf,
55 StringPrototypeLastIndexOf,
56 StringPrototypeSlice,
57 StringPrototypeSplit,
58 StringPrototypeStartsWith,
59 SyntaxErrorCtor,
60 URLCtor,
61 URLSearchParamsCtor,
62} from '@socketsecurity/lib/primordials'
63import { PurlComponent } from './purl-component.js'
64import { PurlQualifierNames } from './purl-qualifier-names.js'
65import { PurlType } from './purl-type.js'
66import { parseNpmSpecifier } from './purl-types/npm.js'
67import { Err, Ok, ResultUtils, err, ok } from './result.js'
68import { stringify, stringifySpec } from './stringify.js'
69import { isBlank, isNonEmptyString, trimLeadingSlashes } from './strings.js'
70import {
71 UrlConverter,
72 _registerPackageURLForUrlConverter,
73} from './url-converter.js'
74import {
75 validateName,
76 validateNamespace,
77 validateQualifiers,
78 validateSubpath,
79 validateType,
80 validateVersion,
81} from './validate.js'
82
83import type { QualifiersObject } from './purl-component.js'
84import type { Result } from './result.js'
85import type { DownloadUrl, RepositoryUrl } from './url-converter.js'
Section 2 of 33
101export type PackageURLComponentValue = string | QualifiersObject | undefined
Section 3 of 33
107export type PackageURLObject = {
108 type?: string | undefined
109 namespace?: string | undefined
110 name?: string | undefined
111 version?: string | undefined
112 qualifiers?: QualifiersObject | undefined
113 subpath?: string | undefined
114}
Section 4 of 33
121export type ParsedPurlComponents = [
122 type: string | undefined,
123 namespace: string | undefined,
124 name: string | undefined,
125 version: string | undefined,
126 qualifiers: URLSearchParams | undefined,
127 subpath: string | undefined,
128]
129
130// LRU flyweight cache for `fromString` — avoids re-parsing identical PURL strings.
131// Bounded to prevent memory leaks. Uses a `Map` for O(1) lookup with LRU eviction.
132const FLYWEIGHT_CACHE_MAX = 1024
133const flyweightCache = new MapCtor<string, PackageURL>()
134
135// Pattern to match URLs with schemes other than "pkg"
136// Limited to 256 chars for scheme to prevent ReDoS
137const OTHER_SCHEME_PATTERN = ObjectFreeze(/^[a-zA-Z][a-zA-Z0-9+.-]{0,255}:\/\//)
138
139// Pattern to match purl-like strings with type/name format
140// Limited to 256 chars for type to prevent ReDoS
141const PURL_LIKE_PATTERN = ObjectFreeze(/^[a-zA-Z0-9+.-]{1,256}\//)
Section 5 of 33
147class PackageURL {
148 static Component = recursiveFreeze(PurlComponent)
149 static KnownQualifierNames = recursiveFreeze(PurlQualifierNames)
150 static Type = recursiveFreeze(PurlType)
Section 6 of 33
153 _cachedString?: string | undefined
154
155 name?: string | undefined
156 namespace?: string | undefined
157 qualifiers?: QualifiersObject | undefined
158 subpath?: string | undefined
159 type?: string | undefined
160 version?: string | undefined
161
162 constructor(
163 rawType: unknown,
164 rawNamespace: unknown,
165 rawName: unknown,
166 rawVersion: unknown,
167 rawQualifiers: unknown,
168 rawSubpath: unknown,
169 ) {
170 const type = isNonEmptyString(rawType) ? normalizeType(rawType) : rawType
171 validateType(type, true)
172
173 const namespace = isNonEmptyString(rawNamespace)
174 ? normalizeNamespace(rawNamespace)
175 : rawNamespace
176 validateNamespace(namespace, true)
177
178 const name = isNonEmptyString(rawName) ? normalizeName(rawName) : rawName
179 validateName(name, true)
180
181 const version = isNonEmptyString(rawVersion)
182 ? normalizeVersion(rawVersion)
183 : rawVersion
184 validateVersion(version, true)
185
186 const qualifiers =
187 typeof rawQualifiers === 'string' || isObject(rawQualifiers)
188 ? normalizeQualifiers(rawQualifiers as string | QualifiersObject)
189 : rawQualifiers
190 validateQualifiers(qualifiers, true)
191
192 const subpath = isNonEmptyString(rawSubpath)
193 ? normalizeSubpath(rawSubpath)
194 : rawSubpath
195 validateSubpath(subpath, true)
196
197 this.type = type as string
198 this.name = name as string
199 if (namespace !== undefined) {
200 this.namespace = namespace as string
201 }
202 if (version !== undefined) {
203 this.version = version as string
204 }
205 this.qualifiers = (qualifiers as QualifiersObject) ?? undefined
206 if (subpath !== undefined) {
207 this.subpath = subpath as string
208 }
209
210 const typeHelpers = PurlType[type as string]
211 if (typeHelpers) {
212 ;(typeHelpers?.['normalize'] as (_purl: PackageURL) => void)?.(this)
213 ;(
214 typeHelpers?.['validate'] as (
215 _purl: PackageURL,
216 _throws: boolean,
217 ) => boolean
218 )?.(this, true)
219 }
220 }
Section 7 of 33
225 toJSON(): PackageURLObject {
226 return this.toObject()
227 }
Section 8 of 33
232 toJSONString(): string {
233 return JSONStringify(this.toObject())
234 }
Section 9 of 33
239 toObject(): PackageURLObject {
240 const result: PackageURLObject = { __proto__: null } as PackageURLObject
241 if (this.type !== undefined) {
242 result.type = this.type
243 }
244 if (this.namespace !== undefined) {
245 result.namespace = this.namespace
246 }
247 if (this.name !== undefined) {
248 result.name = this.name
249 }
250 if (this.version !== undefined) {
251 result.version = this.version
252 }
253 if (this.qualifiers !== undefined) {
254 const qualifiersCopy = ObjectCreate(null) as QualifiersObject
255 for (const key of ObjectKeys(this.qualifiers)) {
256 qualifiersCopy[key] = this.qualifiers[key]!
257 }
258 result.qualifiers = qualifiersCopy
259 }
260 if (this.subpath !== undefined) {
261 result.subpath = this.subpath
262 }
263 return result
264 }
Section 10 of 33
274 toSpec() {
275 return stringifySpec(this)
276 }
277
278 toString() {
279 let cached = this._cachedString
280 if (cached === undefined) {
281 cached = stringify(this)
282 this._cachedString = cached
283 }
284 return cached
285 }
Section 11 of 33
294 withVersion(version: string | undefined): PackageURL {
295 return new PackageURL(
296 this.type,
297 this.namespace,
298 this.name,
299 version,
300 this.qualifiers,
301 this.subpath,
302 )
303 }
Section 12 of 33
312 withNamespace(namespace: string | undefined): PackageURL {
313 return new PackageURL(
314 this.type,
315 namespace,
316 this.name,
317 this.version,
318 this.qualifiers,
319 this.subpath,
320 )
321 }
Section 13 of 33
334 withQualifier(key: string, value: string): PackageURL {
335 return new PackageURL(
336 this.type,
337 this.namespace,
338 this.name,
339 this.version,
340 {
341 __proto__: null,
342 ...this.qualifiers,
343 [key]: value,
344 } as unknown as Record<string, string>,
345 this.subpath,
346 )
347 }
Section 14 of 33
356 withQualifiers(qualifiers: Record<string, string> | undefined): PackageURL {
357 return new PackageURL(
358 this.type,
359 this.namespace,
360 this.name,
361 this.version,
362 qualifiers,
363 this.subpath,
364 )
365 }
Section 15 of 33
374 withSubpath(subpath: string | undefined): PackageURL {
375 return new PackageURL(
376 this.type,
377 this.namespace,
378 this.name,
379 this.version,
380 this.qualifiers,
381 subpath,
382 )
383 }
Section 16 of 33
394 equals(other: PackageURL): boolean {
395 return equalsPurls(this, other)
396 }
Section 17 of 33
407 static equals(a: PackageURL, b: PackageURL): boolean {
408 return equalsPurls(a, b)
409 }
Section 18 of 33
422 compare(other: PackageURL): -1 | 0 | 1 {
423 return comparePurls(this, other)
424 }
Section 19 of 33
439 static compare(a: PackageURL, b: PackageURL): -1 | 0 | 1 {
440 return comparePurls(a, b)
441 }
Section 20 of 33
446 static fromJSON(json: unknown): PackageURL {
447 if (typeof json !== 'string') {
448 throw new ErrorCtor('JSON string argument is required.')
449 }
450
451 // Size limit: 1MB to prevent memory exhaustion
452 // Check actual byte size, not character length
453 const MAX_JSON_SIZE = 1024 * 1024
454 const byteSize = BufferByteLength!(json, 'utf8')
455 if (byteSize > MAX_JSON_SIZE) {
456 throw new ErrorCtor(
457 `JSON string exceeds maximum size limit of ${MAX_JSON_SIZE} bytes`,
458 )
459 }
460
461 let parsed: unknown
462 try {
463 parsed = JSONParse(json)
464 } catch (e) {
465 // For JSON parsing errors, throw a SyntaxError with the expected message
466 throw new SyntaxErrorCtor('Failed to parse PackageURL from JSON', {
467 cause: e,
468 })
469 }
470
471 // Validate parsed result is an object
472 if (!parsed || typeof parsed !== 'object' || ArrayIsArray(parsed)) {
473 throw new ErrorCtor('JSON must parse to an object.')
474 }
475
476 // Cast to record type for safe property access
477 const parsedRecord = parsed as Record<string, unknown>
478
479 // Create a safe object without prototype chain to prevent prototype pollution
480 const safeObject: PackageURLObject = {
481 __proto__: null,
482 type: parsedRecord['type'] as string | undefined,
483 namespace: parsedRecord['namespace'] as string | undefined,
484 name: parsedRecord['name'] as string | undefined,
485 version: parsedRecord['version'] as string | undefined,
486 qualifiers: parsedRecord['qualifiers'] as
487 | Record<string, string>
488 | undefined,
489 subpath: parsedRecord['subpath'] as string | undefined,
490 } as PackageURLObject
491
492 return PackageURL.fromObject(safeObject)
493 }
Section 21 of 33
498 static fromObject(obj: unknown): PackageURL {
499 if (!isObject(obj)) {
500 throw new ErrorCtor('Object argument is required.')
501 }
502 const typedObj = obj as Record<string, unknown>
503 return new PackageURL(
504 typedObj['type'],
505 typedObj['namespace'],
506 typedObj['name'],
507 typedObj['version'],
508 typedObj['qualifiers'],
509 typedObj['subpath'],
510 )
511 }
512
513 static fromString(purlStr: unknown): PackageURL {
514 // Flyweight: return cached instance for identical strings
515 if (typeof purlStr === 'string') {
516 const cached = flyweightCache.get(purlStr)
517 if (cached !== undefined) {
518 // Promote to most-recently-used by re-inserting.
519 flyweightCache.delete(purlStr)
520 flyweightCache.set(purlStr, cached)
521 return cached
522 }
523 }
524 const purl = new PackageURL(...PackageURL.parseString(purlStr))
525 // Eagerly populate the `toString` cache before freezing
526 purl.toString()
527 // Deep freeze the instance and its nested qualifiers object to prevent
528 // cache poisoning via mutation of shared cached instances.
529 recursiveFreeze(purl)
530 // Cache the frozen result for future lookups — freezing prevents
531 // cache poisoning via property mutation on shared instances.
532 if (typeof purlStr === 'string') {
533 if (flyweightCache.size >= FLYWEIGHT_CACHE_MAX) {
534 // Evict oldest entry (first key in `Map` iteration order)
535 const oldest = flyweightCache.keys().next().value
536 if (oldest !== undefined) {
537 flyweightCache.delete(oldest)
538 }
539 }
540 flyweightCache.set(purlStr, purl)
541 }
542 return purl
543 }
Section 22 of 33
590 static fromNpm(specifier: unknown): PackageURL {
591 const { name, namespace, version } = parseNpmSpecifier(specifier)
592 return new PackageURL('npm', namespace, name, version, undefined, undefined)
593 }
Section 23 of 33
619 static fromSpec(type: string, specifier: unknown): PackageURL {
620 switch (type) {
621 case 'npm': {
622 const { name, namespace, version } = parseNpmSpecifier(specifier)
623 return new PackageURL(
624 'npm',
625 namespace,
626 name,
627 version,
628 undefined,
629 undefined,
630 )
631 }
632 default:
633 throw new ErrorCtor(
634 `Unsupported package type: ${type}. Currently supported: npm`,
635 )
636 }
637 }
638
639 static parseString(purlStr: unknown): ParsedPurlComponents {
640 // https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#how-to-parse-a-purl-string-in-its-components
641 if (typeof purlStr !== 'string') {
642 throw new ErrorCtor('A purl string argument is required.')
643 }
644 if (isBlank(purlStr)) {
645 return [undefined, undefined, undefined, undefined, undefined, undefined]
646 }
647
648 // Input length validation to prevent DoS
649 // Reasonable limit for a package URL
650 const MAX_PURL_LENGTH = 4096
651 if (purlStr.length > MAX_PURL_LENGTH) {
652 throw new ErrorCtor(
653 `Package URL exceeds maximum length of ${MAX_PURL_LENGTH} characters.`,
654 )
655 }
656
657 // If the string doesn't start with "pkg:" but looks like a purl format, prepend "pkg:" and try parsing
658 if (!StringPrototypeStartsWith(purlStr, 'pkg:')) {
659 // Only auto-prepend "pkg:" if the string looks like a purl (contains a type/name pattern)
660 // and doesn't look like a URL with a different scheme
661 const hasOtherScheme = RegExpPrototypeTest(OTHER_SCHEME_PATTERN, purlStr)
662 const looksLikePurl = RegExpPrototypeTest(PURL_LIKE_PATTERN, purlStr)
663
664 if (!hasOtherScheme && looksLikePurl) {
665 return PackageURL.parseString(`pkg:${purlStr}`)
666 }
667 }
668
669 // Split the remainder once from left on ':'
670 const colonIndex = StringPrototypeIndexOf(purlStr, ':')
671 // Use WHATWG URL to split up the purl string
Section 24 of 33
673 // - Split the purl string once from right on '#'
674 // - Split the remainder once from right on '?'
675 // - Split the remainder once from left on ':'
676 let url: URL | undefined
677 let hasAuth = false
678 if (colonIndex !== -1) {
679 try {
680 // Since a purl never contains a URL Authority, its scheme
681 // must not be suffixed with double slash as in 'pkg://'
682 // and should use instead 'pkg:'. Purl parsers must accept
683 // URLs such as 'pkg://' and must ignore the '//'
684 const beforeColon = StringPrototypeSlice(purlStr, 0, colonIndex)
685 const afterColon = StringPrototypeSlice(purlStr, colonIndex + 1)
686 const trimmedAfterColon = trimLeadingSlashes(afterColon)
687 url = new URLCtor(`${beforeColon}:${trimmedAfterColon}`)
688 // Check for auth (user:pass@host) without creating a second URL.
689 // When leading slashes were trimmed, the original string had an authority
690 // section (e.g., pkg://user:pass@host/...). Detect `@` in the authority
691 // by checking between the `//` and the next `/`.
Section 25 of 33
693 if (afterColon.length !== trimmedAfterColon.length) {
694 // afterColon starts with slashes — find the authority section
695 const authorityStart = StringPrototypeIndexOf(afterColon, '//') + 2
696 const authorityEnd = StringPrototypeIndexOf(
697 afterColon,
698 '/',
699 authorityStart,
700 )
701 const authority =
702 authorityEnd === -1
703 ? StringPrototypeSlice(afterColon, authorityStart)
704 : StringPrototypeSlice(afterColon, authorityStart, authorityEnd)
705 hasAuth = StringPrototypeIncludes(authority, '@')
706 }
707 } catch (e) {
708 throw new PurlError('failed to parse as URL', {
709 cause: e,
710 })
711 }
712 }
713 // The scheme is a constant with the value "pkg"
Section 26 of 33
715 url?.protocol !== 'pkg:'
716 ) {
717 throw new PurlError('missing required "pkg" scheme component')
Section 27 of 33
719 }
720 // A purl must NOT contain a URL Authority i.e. there is no support for
721 // username, password, host and port components
722 if (hasAuth) {
723 throw new PurlError('cannot contain a "user:pass@host:port"')
724 }
725
726 const { pathname } = url
727 const firstSlashIndex = StringPrototypeIndexOf(pathname, '/')
728 const rawType = decodePurlComponent(
729 'type',
730 firstSlashIndex === -1
731 ? pathname
732 : StringPrototypeSlice(pathname, 0, firstSlashIndex),
733 )
734 if (firstSlashIndex < 1) {
735 return [rawType, undefined, undefined, undefined, undefined, undefined]
736 }
737
738 let rawVersion: string | undefined
739 // Both branches of this ternary are tested, but V8 reports phantom branch combinations
Section 28 of 33
741 // Deviate from the specification to handle a special npm purl type case for
742 // pnpm ids such as 'pkg:npm/next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)'
743 let atSignIndex =
744 rawType === 'npm'
745 ? StringPrototypeIndexOf(pathname, '@', firstSlashIndex + 2)
746 : StringPrototypeLastIndexOf(pathname, '@')
Section 29 of 33
748 // When a forward slash ('/') is directly preceding an '@' symbol,
749 // then the '@' symbol is NOT considered a version separator
750 if (
751 atSignIndex > 0 &&
Section 30 of 33
753 ) {
754 atSignIndex = -1
755 }
756 const beforeVersion = StringPrototypeSlice(
757 pathname,
758 rawType.length + 1,
759 atSignIndex === -1 ? pathname.length : atSignIndex,
760 )
761 if (atSignIndex !== -1) {
762 // Split the remainder once from right on '@'
763 rawVersion = decodePurlComponent(
764 'version',
765 StringPrototypeSlice(pathname, atSignIndex + 1),
766 )
767 }
768
769 let rawNamespace: string | undefined
770 let rawName: string
771 const lastSlashIndex = StringPrototypeLastIndexOf(beforeVersion, '/')
772 if (lastSlashIndex === -1) {
773 // Split the remainder once from right on '/'
774 rawName = decodePurlComponent('name', beforeVersion)
775 } else {
776 // Split the remainder once from right on '/'
777 rawName = decodePurlComponent(
778 'name',
779 StringPrototypeSlice(beforeVersion, lastSlashIndex + 1),
780 )
781 // Split the remainder on '/'
782 rawNamespace = decodePurlComponent(
783 'namespace',
784 StringPrototypeSlice(beforeVersion, 0, lastSlashIndex),
785 )
786 }
787
788 let rawQualifiers: URLSearchParams | undefined
789 if (url.searchParams.size !== 0) {
790 const search = StringPrototypeSlice(url.search, 1)
791 const searchParams = new URLSearchParamsCtor()
792 const entries = StringPrototypeSplit(search, '&' as any)
793 for (let i = 0, { length } = entries; i < length; i += 1) {
794 const entry = entries[i]!
795 // Slice on the FIRST '=' so values containing '=' (e.g.
796 // download_url=https://example.com/x?a=1, base64 padding `==`)
797 // round-trip intact. Splitting on '=' and indexing [1] silently
798 // truncates everything after the second '='.
799 const eqIndex = StringPrototypeIndexOf(entry, '=')
800 const key =
801 eqIndex === -1 ? entry : StringPrototypeSlice(entry, 0, eqIndex)
802 // Validate qualifier key is not empty (reject malformed PURLs like ?&key=val or ?key=val&)
803 if (key.length === 0) {
804 throw new PurlError('qualifier key must not be empty')
805 }
806 const value = decodePurlComponent(
807 'qualifiers',
808 eqIndex === -1 ? '' : StringPrototypeSlice(entry, eqIndex + 1),
809 )
810 // Use `URLSearchParams#append` to preserve plus signs
811 // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs
Section 31 of 33
813 key,
814 value,
815 )
816 }
817 // Split the remainder once from right on '?'
818 rawQualifiers = searchParams
819 }
820
821 let rawSubpath: string | undefined
822 const { hash } = url
823 if (hash.length !== 0) {
824 // Split the purl string once from right on '#'
825 rawSubpath = decodePurlComponent('subpath', StringPrototypeSlice(hash, 1))
826 }
827
828 return [
829 rawType,
830 rawNamespace,
831 rawName,
832 rawVersion,
833 rawQualifiers,
834 rawSubpath,
835 ]
836 }
Section 32 of 33
850 static isValid(purlStr: unknown): boolean {
851 return PackageURL.tryFromString(purlStr).isOk()
852 }
Section 33 of 33
872 static fromUrl(urlStr: string): PackageURL | undefined {
873 return UrlConverter.fromUrl(urlStr)
874 }
875
876 static tryFromJSON(json: unknown): Result<PackageURL, Error> {
877 return ResultUtils.from(() => PackageURL.fromJSON(json))
878 }
879
880 static tryFromObject(obj: unknown): Result<PackageURL, Error> {
881 return ResultUtils.from(() => PackageURL.fromObject(obj))
882 }
883
884 static tryFromString(purlStr: unknown): Result<PackageURL, Error> {
885 return ResultUtils.from(() => PackageURL.fromString(purlStr))
886 }
887
888 static tryParseString(purlStr: unknown): Result<unknown[], Error> {
889 return ResultUtils.from(() => PackageURL.parseString(purlStr))
890 }
891}
892
893for (const staticProp of ['Component', 'KnownQualifierNames', 'Type']) {
894 ReflectDefineProperty(PackageURL, staticProp, {
895 ...ReflectGetOwnPropertyDescriptor(PackageURL, staticProp),
896 writable: false,
897 })
898}
899
900ReflectSetPrototypeOf(PackageURL.prototype, null)
901
902// Register PackageURL with compare module for string-based comparison support.
903_registerPackageURL(PackageURL)
904
905// Register PackageURL with url-converter module for fromUrl construction.
906_registerPackageURLForUrlConverter(PackageURL)
907
908export {
909 Err,
910 Ok,
911 PackageURL,
912 PurlComponent,
913 PurlQualifierNames,
914 PurlType,
915 ResultUtils,
916 UrlConverter,
917 err,
918 ok,
919}
920export type { DownloadUrl, RepositoryUrl, Result }
src/purl-component.ts
7 sections
1 2 3 4 5 6 7
Section 1 of 7
5import {
6 encodeComponent,
7 encodeName,
8 encodeNamespace,
9 encodeQualifierParam,
10 encodeQualifiers,
11 encodeSubpath,
12 encodeVersion,
13} from './encode.js'
14import { createHelpersNamespaceObject } from './helpers.js'
15import {
16 normalizeName,
17 normalizeNamespace,
18 normalizeQualifiers,
19 normalizeSubpath,
20 normalizeType,
21 normalizeVersion,
22} from './normalize.js'
23import { isNonEmptyString } from './strings.js'
24import {
25 validateName,
26 validateNamespace,
27 validateQualifierKey,
28 validateQualifiers,
29 validateSubpath,
30 validateType,
31 validateVersion,
32} from './validate.js'
Section 2 of 7
37export type ComponentEncoder = (_value: unknown) => string
38export type ComponentNormalizer = (_value: string) => string | undefined
39export type ComponentValidator = (_value: unknown, _throws: boolean) => boolean
40export type QualifiersValue = string | number | boolean | null | undefined
41export type QualifiersObject = Record<string, QualifiersValue>
42
43const componentSortOrderLookup = {
44 __proto__: null,
45 type: 0,
46 namespace: 1,
47 name: 2,
48 version: 3,
49 qualifiers: 4,
50 qualifierKey: 5,
51 qualifierValue: 6,
52 subpath: 7,
53}
Section 3 of 7
58function componentComparator(compA: string, compB: string): number {
59 return componentSortOrder(compA) - componentSortOrder(compB)
60}
Section 4 of 7
65function componentSortOrder(comp: string): number {
66 return (
67 (componentSortOrderLookup as unknown as Record<string, number>)[comp] ??
68 // Unknown components sort after all known ones
69 8
70 )
71}
Section 5 of 7
76function PurlComponentEncoder(comp: unknown): string {
77 return isNonEmptyString(comp) ? encodeComponent(comp) : ''
78}
Section 6 of 7
83function PurlComponentStringNormalizer(comp: unknown): string | undefined {
84 return typeof comp === 'string' ? comp : undefined
85}
Section 7 of 7
90function PurlComponentValidator(_comp: unknown, _throws: boolean): boolean {
91 return true
92}
93
94// Rules for each purl component:
95// https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#rules-for-each-purl-component
96const PurlComponent = createHelpersNamespaceObject(
97 {
98 encode: {
99 name: encodeName,
100 namespace: encodeNamespace,
101 version: encodeVersion,
102 qualifiers: encodeQualifiers,
103 qualifierKey: encodeQualifierParam,
104 qualifierValue: encodeQualifierParam,
105 subpath: encodeSubpath,
106 },
107 normalize: {
108 type: normalizeType,
109 namespace: normalizeNamespace,
110 name: normalizeName,
111 version: normalizeVersion,
112 qualifiers: normalizeQualifiers,
113 subpath: normalizeSubpath,
114 },
115 validate: {
116 type: validateType,
117 namespace: validateNamespace,
118 name: validateName,
119 version: validateVersion,
120 qualifierKey: validateQualifierKey,
121 qualifiers: validateQualifiers,
122 subpath: validateSubpath,
123 },
124 },
125 {
126 comparator: componentComparator,
127 encode: PurlComponentEncoder,
128 normalize: PurlComponentStringNormalizer,
129 validate: PurlComponentValidator,
130 },
131)
132
133export {
134 PurlComponent,
135 PurlComponentEncoder,
136 PurlComponentStringNormalizer,
137 PurlComponentValidator,
138 componentComparator,
139 componentSortOrder,
140}
src/constants.ts
1 section
1
Section 1 of 1
4const LOOP_SENTINEL = 1_000_000
5
6export { LOOP_SENTINEL }