Skip to content

Commit 1da42fe

Browse files
committed
refactor: improve source code quality and security
- Fix ReDoS vulnerabilities and add input validation - Add length limits and circular dependency resolution - Standardize error messages and error handling - Replace process.exit with process.exitCode - Use @socketsecurity/registry utilities - Improve type safety and remove any types - Add explicit undefined for optional properties - Optimize performance and reduce allocations
1 parent 5e2a161 commit 1da42fe

File tree

12 files changed

+284
-139
lines changed

12 files changed

+284
-139
lines changed

src/decode.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
*/
55
import { PurlError } from './error.js'
66

7-
// IMPORTANT: Do not use destructuring here - use direct assignment instead.
7+
// IMPORTANT: Do not use destructuring here - use direct assignment instead
88
// tsgo has a bug that incorrectly transpiles destructured exports, resulting in
9-
// `exports.decodeComponent = void 0;` which causes runtime errors.
9+
// `exports.decodeComponent = void 0;` which causes runtime errors
1010
// See: https://github.com/SocketDev/socket-packageurl-js/issues/3
1111
const decodeComponent = globalThis.decodeURIComponent
1212

src/encode.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
import { isObject } from './objects.js'
1010
import { isNonEmptyString } from './strings.js'
1111

12-
// IMPORTANT: Do not use destructuring here (e.g., const { encodeURIComponent } = globalThis).
12+
// IMPORTANT: Do not use destructuring here (e.g., const { encodeURIComponent } = globalThis)
1313
// tsgo has a bug that incorrectly transpiles destructured exports, resulting in
14-
// `exports.encodeComponent = void 0;` which causes runtime errors.
14+
// `exports.encodeComponent = void 0;` which causes runtime errors
1515
// See: https://github.com/SocketDev/socket-packageurl-js/issues/3
1616
const encodeComponent = globalThis.encodeURIComponent
1717

@@ -39,13 +39,13 @@ function encodeNamespace(namespace: unknown): string {
3939
function encodeQualifierParam(param: unknown): string {
4040
if (isNonEmptyString(param)) {
4141
const value = prepareValueForSearchParams(param)
42-
// Use URLSearchParams#set to preserve plus signs.
42+
// Use URLSearchParams#set to preserve plus signs
4343
// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs
44-
// Use a local instance to avoid mutation issues in concurrent scenarios.
44+
// Use a local instance to avoid mutation issues in concurrent scenarios
4545
const searchParams = new URLSearchParams()
4646
searchParams.set(REUSED_SEARCH_PARAMS_KEY, value)
4747
// Param key and value are encoded with `percentEncodeSet` of
48-
// 'application/x-www-form-urlencoded' and `spaceAsPlus` of `true`.
48+
// 'application/x-www-form-urlencoded' and `spaceAsPlus` of `true`
4949
// https://url.spec.whatwg.org/#urlencoded-serializing
5050
const search = searchParams.toString()
5151
return normalizeSearchParamsEncoding(
@@ -60,15 +60,15 @@ function encodeQualifierParam(param: unknown): string {
6060
*/
6161
function encodeQualifiers(qualifiers: unknown): string {
6262
if (isObject(qualifiers)) {
63-
// Sort this list of qualifier strings lexicographically.
63+
// Sort this list of qualifier strings lexicographically
6464
const qualifiersKeys = Object.keys(qualifiers).sort()
6565
const searchParams = new URLSearchParams()
6666
for (let i = 0, { length } = qualifiersKeys; i < length; i += 1) {
6767
const key = qualifiersKeys[i]!
6868
const value = prepareValueForSearchParams(
6969
(qualifiers as Record<string, unknown>)[key],
7070
)
71-
// Use URLSearchParams#set to preserve plus signs.
71+
// Use URLSearchParams#set to preserve plus signs
7272
// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs
7373
searchParams.set(key!, value)
7474
}
@@ -106,7 +106,7 @@ function normalizeSearchParamsEncoding(encoded: string): string {
106106
* Prepare string value for URLSearchParams encoding.
107107
*/
108108
function prepareValueForSearchParams(value: unknown): string {
109-
// Replace spaces with %20's so they don't get converted to plus signs.
109+
// Replace spaces with %20's so they don't get converted to plus signs
110110
return String(value).replaceAll(' ', '%20')
111111
}
112112

src/error.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ function formatPurlErrorMessage(message = ''): string {
1010
const { length } = message
1111
let formatted = ''
1212
if (length) {
13-
// Lower case start of message.
13+
// Lower case start of message
1414
const code0 = message.charCodeAt(0)
1515
formatted =
1616
code0 >= 65 /*'A'*/ && code0 <= 90 /*'Z'*/
1717
? `${message[0]!.toLowerCase()}${message.slice(1)}`
1818
: message
19-
// Remove period from end of message.
19+
// Remove period from end of message
2020
if (
2121
length > 1 &&
2222
message.charCodeAt(length - 1) === 46 /*'.'*/ &&

src/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function createHelpersNamespaceObject(
1717
comparator?: ((_a: string, _b: string) => number) | undefined
1818
}
1919
const helperNames = Object.keys(helpers).sort()
20-
// Collect all unique property names from all helper objects.
20+
// Collect all unique property names from all helper objects
2121
const propNames = [
2222
...new Set(
2323
Object.values(helpers).flatMap((helper: Record<string, unknown>) =>
@@ -26,7 +26,7 @@ function createHelpersNamespaceObject(
2626
),
2727
].sort(comparator)
2828
const nsObject: Record<string, Record<string, unknown>> = Object.create(null)
29-
// Build inverted structure: property -> {helper1: value1, helper2: value2}.
29+
// Build inverted structure: property -> {helper1: value1, helper2: value2}
3030
for (let i = 0, { length } = propNames; i < length; i += 1) {
3131
const propName = propNames[i]!
3232
const helpersForProp: Record<string, unknown> = Object.create(null)

src/index.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,66 @@ SOFTWARE.
2222

2323
/**
2424
* @fileoverview Main entry point for the socket-packageurl-js library.
25-
* Provides exports for PackageURL, PurlComponent, PurlQualifierNames, and PurlType.
25+
*
26+
* This library provides a complete implementation of the Package URL (purl) specification.
27+
* Package URLs are used to identify and locate software packages in a standardized way
28+
* across different package management systems and ecosystems.
29+
*
30+
* Core exports:
31+
* - PackageURL: Main class for parsing and constructing package URLs
32+
* - PackageURLBuilder: Builder pattern for constructing package URLs
33+
* - PurlType: Type-specific normalization and validation rules
34+
* - PurlComponent: Component encoding/decoding utilities
35+
* - PurlQualifierNames: Known qualifier names from the specification
36+
*
37+
* Utility exports:
38+
* - UrlConverter: Convert between purls and repository/download URLs
39+
* - Result utilities: Functional error handling with Ok/Err pattern
2640
*/
2741

28-
/* c8 ignore start - Re-export only file, no logic to test. */
42+
/* c8 ignore start - Re-export only file, no logic to test */
43+
44+
// ============================================================================
45+
// Core Classes and Functions
46+
// ============================================================================
2947
export {
30-
Err,
31-
Ok,
3248
PackageURL,
33-
PackageURLBuilder,
3449
PurlComponent,
3550
PurlQualifierNames,
3651
PurlType,
37-
ResultUtils,
52+
} from './package-url.js'
53+
54+
export { PackageURLBuilder } from './package-url-builder.js'
55+
56+
// ============================================================================
57+
// Utility Classes and Functions
58+
// ============================================================================
59+
export {
3860
UrlConverter,
61+
} from './package-url.js'
62+
63+
export {
64+
Err,
65+
Ok,
66+
ResultUtils,
3967
err,
4068
ok,
4169
} from './package-url.js'
70+
71+
// ============================================================================
72+
// TypeScript Type Definitions
73+
// ============================================================================
4274
export type {
4375
DownloadUrl,
4476
RepositoryUrl,
4577
Result,
4678
} from './package-url.js'
4779

80+
// ============================================================================
81+
// Registry Integration
82+
// ============================================================================
4883
// Re-export PURL types from socket-registry for consistency
4984
export { PURL_Type } from '@socketsecurity/registry'
5085
export type { EcosystemString } from '@socketsecurity/registry'
86+
5187
/* c8 ignore stop */

src/normalize.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,31 @@ function normalizePurlPath(
3434
let collapsed = ''
3535
let start = 0
3636
// Leading and trailing slashes, i.e. '/', are not significant and should be
37-
// stripped in the canonical form.
37+
// stripped in the canonical form
3838
while (pathname.charCodeAt(start) === 47 /*'/'*/) {
3939
start += 1
4040
}
4141
let nextIndex = pathname.indexOf('/', start)
4242
if (nextIndex === -1) {
43-
// No slashes found - return trimmed pathname.
43+
// No slashes found - return trimmed pathname
4444
return pathname.slice(start)
4545
}
4646
// Discard any empty string segments by collapsing repeated segment
47-
// separator slashes, i.e. '/'.
47+
// separator slashes, i.e. '/'
4848
while (nextIndex !== -1) {
4949
const segment = pathname.slice(start, nextIndex)
5050
if (callback === undefined || callback(segment)) {
51-
// Add segment with separator if not first segment.
51+
// Add segment with separator if not first segment
5252
collapsed = collapsed + (collapsed.length === 0 ? '' : '/') + segment
5353
}
54-
// Skip to next segment, consuming multiple consecutive slashes.
54+
// Skip to next segment, consuming multiple consecutive slashes
5555
start = nextIndex + 1
5656
while (pathname.charCodeAt(start) === 47) {
5757
start += 1
5858
}
5959
nextIndex = pathname.indexOf('/', start)
6060
}
61-
// Handle last segment after final slash.
61+
// Handle last segment after final slash
6262
const lastSegment = pathname.slice(start)
6363
if (
6464
lastSegment.length !== 0 &&
@@ -76,19 +76,19 @@ function normalizeQualifiers(
7676
rawQualifiers: unknown,
7777
): Record<string, string> | undefined {
7878
let qualifiers: Record<string, string> | undefined
79-
// Use for-of to work with entries iterators.
79+
// Use for-of to work with entries iterators
8080
for (const { 0: key, 1: value } of qualifiersToEntries(rawQualifiers)) {
8181
const strValue = typeof value === 'string' ? value : String(value)
8282
const trimmed = strValue.trim()
8383
// A key=value pair with an empty value is the same as no key/value
84-
// at all for this key.
84+
// at all for this key
8585
if (trimmed.length === 0) {
8686
continue
8787
}
8888
if (qualifiers === undefined) {
8989
qualifiers = Object.create(null) as Record<string, string>
9090
}
91-
// A key is case insensitive. The canonical form is lowercase.
91+
// A key is case insensitive. The canonical form is lowercase
9292
qualifiers[key.toLowerCase()] = trimmed
9393
}
9494
return qualifiers
@@ -107,8 +107,8 @@ function normalizeSubpath(rawSubpath: unknown): string | undefined {
107107
* Normalize package type to lowercase.
108108
*/
109109
function normalizeType(rawType: unknown): string | undefined {
110-
// The type must NOT be percent-encoded.
111-
// The type is case insensitive. The canonical form is lowercase.
110+
// The type must NOT be percent-encoded
111+
// The type is case insensitive. The canonical form is lowercase
112112
return typeof rawType === 'string' ? rawType.trim().toLowerCase() : undefined
113113
}
114114

@@ -119,9 +119,9 @@ function normalizeVersion(rawVersion: unknown): string | undefined {
119119
return typeof rawVersion === 'string' ? rawVersion.trim() : undefined
120120
}
121121

122-
// IMPORTANT: Do not use destructuring here - use direct assignment instead.
122+
// IMPORTANT: Do not use destructuring here - use direct assignment instead
123123
// tsgo has a bug that incorrectly transpiles destructured exports, resulting in
124-
// `exports.ReflectApply = void 0;` which causes runtime errors.
124+
// `exports.ReflectApply = void 0;` which causes runtime errors
125125
// See: https://github.com/SocketDev/socket-packageurl-js/issues/3
126126
const ReflectApply = Reflect.apply
127127

@@ -132,7 +132,7 @@ function qualifiersToEntries(
132132
rawQualifiers: unknown,
133133
): Iterable<[string, string]> {
134134
if (isObject(rawQualifiers)) {
135-
// URLSearchParams instances have an "entries" method that returns an iterator.
135+
// URLSearchParams instances have an "entries" method that returns an iterator
136136
const rawQualifiersObj = rawQualifiers as QualifiersObject | URLSearchParams
137137
const entriesProperty = (rawQualifiersObj as QualifiersObject)['entries']
138138
return typeof entriesProperty === 'function'

src/objects.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@
22
* @fileoverview Object utility functions for type checking and immutable object creation.
33
* Provides object validation and recursive freezing utilities.
44
*/
5-
import { LOOP_SENTINEL } from './constants.js'
5+
// eslint-disable-next-line import-x/extensions -- External package, no .js extension needed
6+
import { isObject } from '@socketsecurity/registry/lib/objects'
67

7-
/**
8-
* Check if value is an object type.
9-
*/
10-
function isObject(value: unknown): value is object {
11-
return value !== null && typeof value === 'object'
12-
}
8+
import { LOOP_SENTINEL } from './constants.js'
139

1410
/**
1511
* Recursively freeze an object and all nested objects.
@@ -24,21 +20,21 @@ function recursiveFreeze<T>(value_: T): T {
2420
) {
2521
return value_
2622
}
27-
// Use breadth-first traversal to avoid stack overflow on deep objects.
23+
// Use breadth-first traversal to avoid stack overflow on deep objects
2824
const queue = [value_ as T & object]
2925
const visited = new WeakSet<object>()
3026
visited.add(value_ as T & object)
3127
let { length: queueLength } = queue
3228
let pos = 0
3329
while (pos < queueLength) {
34-
// Safety check to prevent processing excessively large object graphs.
30+
// Safety check to prevent processing excessively large object graphs
3531
if (pos === LOOP_SENTINEL) {
36-
throw new Error('Object graph too large (exceeds 1,000,000 items)')
32+
throw new Error('Object graph too large (exceeds 1,000,000 items).')
3733
}
3834
const obj = queue[pos++]!
3935
Object.freeze(obj)
4036
if (Array.isArray(obj)) {
41-
// Queue unfrozen array items for processing.
37+
// Queue unfrozen array items for processing
4238
for (let i = 0, { length } = obj; i < length; i += 1) {
4339
const item: unknown = obj[i]
4440
if (
@@ -52,10 +48,12 @@ function recursiveFreeze<T>(value_: T): T {
5248
}
5349
}
5450
} else {
55-
// Queue unfrozen object properties for processing.
51+
// Queue unfrozen object properties for processing
5652
const keys = Reflect.ownKeys(obj)
5753
for (let i = 0, { length } = keys; i < length; i += 1) {
58-
const propValue: unknown = (obj as any)[keys[i]!]
54+
const propValue: unknown = (obj as Record<PropertyKey, unknown>)[
55+
keys[i]!
56+
]
5957
if (
6058
propValue !== null &&
6159
(typeof propValue === 'object' || typeof propValue === 'function') &&

0 commit comments

Comments
 (0)