forked from package-url/packageurl-js
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathnormalize.ts
More file actions
194 lines (182 loc) · 5.81 KB
/
normalize.ts
File metadata and controls
194 lines (182 loc) · 5.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
/**
* @fileoverview Normalization functions for PURL components.
* Handles path normalization, qualifier processing, and canonical form conversion.
*/
import { isObject } from './objects.js'
import {
ObjectCreate,
ObjectEntries,
ObjectFreeze,
ReflectApply,
StringPrototypeCharCodeAt,
StringPrototypeIndexOf,
StringPrototypeSlice,
StringPrototypeToLowerCase,
StringPrototypeTrim,
} from './primordials.js'
import { isBlank } from './strings.js'
const EMPTY_ENTRIES: Iterable<[string, string]> = ObjectFreeze(
[] as Array<[string, string]>,
)
import type { QualifiersObject } from './purl-component.js'
/**
* Normalize package name by trimming whitespace.
*/
function normalizeName(rawName: unknown): string | undefined {
return typeof rawName === 'string' ? StringPrototypeTrim(rawName) : undefined
}
/**
* Normalize package namespace by trimming and collapsing path separators.
*/
function normalizeNamespace(rawNamespace: unknown): string | undefined {
return typeof rawNamespace === 'string'
? normalizePurlPath(rawNamespace)
: undefined
}
/**
* Normalize purl path component by collapsing separators and filtering segments.
*/
function normalizePurlPath(
pathname: string,
options?: { filter?: ((_segment: string) => boolean) | undefined },
): string {
const { filter: callback } = options ?? {}
let collapsed = ''
let start = 0
// Leading and trailing slashes, i.e. '/', are not significant and should be
// stripped in the canonical form
while (StringPrototypeCharCodeAt(pathname, start) === 47 /*'/'*/) {
start += 1
}
let nextIndex = StringPrototypeIndexOf(pathname, '/', start)
if (nextIndex === -1) {
// No slashes found - return trimmed pathname
return StringPrototypeSlice(pathname, start)
}
// Discard any empty string segments by collapsing repeated segment
// separator slashes, i.e. '/'
while (nextIndex !== -1) {
const segment = StringPrototypeSlice(pathname, start, nextIndex)
if (callback === undefined || callback(segment)) {
// Add segment with separator if not first segment
collapsed = collapsed + (collapsed.length === 0 ? '' : '/') + segment
}
// Skip to next segment, consuming multiple consecutive slashes
start = nextIndex + 1
while (StringPrototypeCharCodeAt(pathname, start) === 47) {
start += 1
}
nextIndex = StringPrototypeIndexOf(pathname, '/', start)
}
// Handle last segment after final slash
const lastSegment = StringPrototypeSlice(pathname, start)
if (
lastSegment.length !== 0 &&
(callback === undefined || callback(lastSegment))
) {
// Add segment with separator if not first segment
collapsed = collapsed + (collapsed.length === 0 ? '' : '/') + lastSegment
}
return collapsed
}
/**
* Normalize qualifiers by trimming values and lowercasing keys.
*/
function normalizeQualifiers(
rawQualifiers: unknown,
): Record<string, string> | undefined {
let qualifiers: Record<string, string> | undefined
// Use for-of to work with entries iterators
for (const { 0: key, 1: value } of qualifiersToEntries(rawQualifiers)) {
const strValue = typeof value === 'string' ? value : String(value)
const trimmed = StringPrototypeTrim(strValue)
// A key=value pair with an empty value is the same as no key/value
// at all for this key
if (trimmed.length === 0) {
continue
}
if (qualifiers === undefined) {
qualifiers = ObjectCreate(null) as Record<string, string>
}
// A key is case insensitive. The canonical form is lowercase
qualifiers[StringPrototypeToLowerCase(key)] = trimmed
}
return qualifiers
}
/**
* Normalize subpath by filtering invalid segments.
*/
function normalizeSubpath(rawSubpath: unknown): string | undefined {
return typeof rawSubpath === 'string'
? normalizePurlPath(rawSubpath, { filter: subpathFilter })
: undefined
}
/**
* Normalize package type to lowercase.
*/
function normalizeType(rawType: unknown): string | undefined {
// The type must NOT be percent-encoded
// The type is case insensitive. The canonical form is lowercase
return typeof rawType === 'string'
? StringPrototypeToLowerCase(StringPrototypeTrim(rawType))
: undefined
}
/**
* Normalize package version by trimming whitespace.
*/
function normalizeVersion(rawVersion: unknown): string | undefined {
return typeof rawVersion === 'string'
? StringPrototypeTrim(rawVersion)
: undefined
}
/**
* Convert qualifiers to iterable entries.
*/
function qualifiersToEntries(
rawQualifiers: unknown,
): Iterable<[string, string]> {
if (isObject(rawQualifiers)) {
// URLSearchParams instances have an "entries" method that returns an iterator
const rawQualifiersObj = rawQualifiers as QualifiersObject | URLSearchParams
const entriesProperty = (rawQualifiersObj as QualifiersObject)['entries']
return typeof entriesProperty === 'function'
? (ReflectApply(entriesProperty, rawQualifiersObj, []) as Iterable<
[string, string]
>)
: (ObjectEntries(rawQualifiers as Record<string, string>) as Iterable<
[string, string]
>)
}
return typeof rawQualifiers === 'string'
? new URLSearchParams(rawQualifiers).entries()
: EMPTY_ENTRIES
}
/**
* Filter invalid subpath segments.
*/
function subpathFilter(segment: string): boolean {
// When percent-decoded, a segment
// - must not be any of '.' or '..'
// - must not be empty
const { length } = segment
if (length === 1 && StringPrototypeCharCodeAt(segment, 0) === 46 /*'.'*/) {
return false
}
if (
length === 2 &&
StringPrototypeCharCodeAt(segment, 0) === 46 &&
StringPrototypeCharCodeAt(segment, 1) === 46
) {
return false
}
return !isBlank(segment)
}
export {
normalizeName,
normalizeNamespace,
normalizePurlPath,
normalizeQualifiers,
normalizeSubpath,
normalizeType,
normalizeVersion,
}