Skip to content

Commit 47f6426

Browse files
authored
feat: add license file gatherer utility (#1249)
fixes #1162 - [x] implement - [x] test --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent a43c66f commit 47f6426

File tree

7 files changed

+437
-1
lines changed

7 files changed

+437
-1
lines changed

HISTORY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ All notable changes to this project will be documented in this file.
88

99
* Fixed
1010
* Type exports for the web (via [#1252])
11+
* Added
12+
* New class `Utils.LicenseUtility.LicenseEvidenceGatherer` ([#1162] via [#1249])
1113

14+
[#1162]: https://github.com/CycloneDX/cyclonedx-javascript-library/issues/1162
15+
[#1249]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1249
1216
[#1252]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1252
1317

1418
## 8.3.0 -- 2025-06-05

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"c8": "^10",
9898
"deepmerge": "^4.2.2",
9999
"fast-glob": "^3.3.1",
100+
"memfs": "4.17.2",
100101
"mocha": "11.5.0",
101102
"npm-run-all2": "^8",
102103
"rimraf": "^6",
@@ -190,7 +191,7 @@
190191
"test:lint": "tsc --noEmit",
191192
"test:standard": "npm --prefix tools/code-style exec -- eslint .",
192193
"cs-fix": "npm --prefix tools/code-style exec -- eslint --fix .",
193-
"api-doc": "run-p --aggregate-output -lc api-doc:*",
194+
"api-doc": "run-p --aggregate-output -lc api-doc:\\*",
194195
"api-doc:node": "npm --prefix tools/docs-gen exec -- typedoc --options ./typedoc.node.json",
195196
"api-doc:web": "npm --prefix tools/docs-gen exec -- typedoc --options ./typedoc.web.json"
196197
}

src/_helpers/mime.node.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*!
2+
This file is part of CycloneDX JavaScript Library.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache-2.0
17+
Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
20+
import { parse as parsePath } from 'node:path'
21+
22+
type MimeType = string
23+
24+
const MIMETYPE_TEXT_PLAIN: MimeType = 'text/plain'
25+
26+
const MAP_TEXT_EXTENSION_MIMETYPE: Readonly<Record<string, MimeType>> = {
27+
'': MIMETYPE_TEXT_PLAIN,
28+
// https://www.iana.org/assignments/media-types/media-types.xhtml
29+
'.csv': 'text/csv',
30+
'.htm': 'text/html',
31+
'.html': 'text/html',
32+
'.md': 'text/markdown',
33+
'.txt': MIMETYPE_TEXT_PLAIN,
34+
'.rst': 'text/prs.fallenstein.rst',
35+
'.rtf': 'application/rtf', // our scope is text, yes, but RTF is binary - so we should base64 encode it ...
36+
'.xml': 'text/xml', // not `application/xml` -- our scope is text!
37+
// add more mime types above this line. pull-requests welcome!
38+
// license-specific files
39+
'.license': MIMETYPE_TEXT_PLAIN,
40+
'.licence': MIMETYPE_TEXT_PLAIN,
41+
} as const
42+
43+
const LICENSE_FILENAME_BASE: Readonly<Set<string>> = new Set(['licence', 'license'])
44+
const LICENSE_FILENAME_EXT: Readonly<Set<string>> = new Set([
45+
'.apache',
46+
'.bsd',
47+
'.gpl',
48+
'.mit',
49+
// to be continued ... pullrequests welcome
50+
])
51+
52+
export function guessMimeTypeForLicenseFile (filename: string): MimeType | undefined {
53+
const {name, ext} = parsePath(filename.toLowerCase())
54+
return LICENSE_FILENAME_BASE.has(name) && LICENSE_FILENAME_EXT.has(ext)
55+
? MIMETYPE_TEXT_PLAIN
56+
: MAP_TEXT_EXTENSION_MIMETYPE[ext]
57+
}

src/utils/index.node.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './index.common'
2121

2222
// region node-specifics
2323

24+
export * as LicenseUtility from './licenseUtility.node'
2425
export * as NpmjsUtility from './npmjsUtility.node'
2526

2627
// endregion node-specifics

src/utils/licenseUtility.node.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*!
2+
This file is part of CycloneDX JavaScript Library.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache-2.0
17+
Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
20+
/**
21+
* This module tries to be as compatible as possible, it only uses basic methods that are known to be working in all FileSystem abstraction-layers.
22+
* In addition, we use type parameters for all `PathLike`s, so downstream users can utilize their implementations accordingly.
23+
*
24+
* @module
25+
*/
26+
27+
import type { Stats } from 'node:fs'
28+
29+
import { guessMimeTypeForLicenseFile } from '../_helpers/mime.node'
30+
import { AttachmentEncoding } from '../enums/attachmentEncoding'
31+
import { Attachment } from '../models/attachment'
32+
33+
export interface FsUtils<P extends string> {
34+
readdirSync: (path: P ) => P[]
35+
readFileSync: (path: P) => Buffer
36+
statSync: (path: P) => Stats
37+
}
38+
39+
export interface PathUtils<P extends string> {
40+
join: (...paths: P[]) => P
41+
}
42+
43+
export interface FileAttachment<P extends string> {
44+
filePath: P
45+
file: P
46+
text: Attachment
47+
}
48+
49+
const LICENSE_FILENAME_PATTERN = /^(?:UN)?LICEN[CS]E|.\.LICEN[CS]E$|^NOTICE$/i
50+
51+
export type ErrorReporter = (e:Error) => any
52+
53+
export class LicenseEvidenceGatherer<P extends string = string> {
54+
readonly #fs: FsUtils<P>
55+
readonly #path: PathUtils<P>
56+
57+
/* eslint-disable tsdoc/syntax -- we want to use the dot-syntax - https://github.com/microsoft/tsdoc/issues/19 */
58+
/**
59+
* `fs` and `path` can be supplied, if any compatibility-layer or drop-in replacement is used.
60+
*
61+
* @param options.fs - If omitted, the native `node:fs` is used.
62+
* @param options.path - If omitted, the native `node:path` is used.
63+
*/
64+
constructor (options: { fs?: FsUtils<P>, path?: PathUtils<P> } = {}) {
65+
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports -- needed */
66+
this.#fs = options.fs ?? require('node:fs')
67+
this.#path = options.path ?? require('node:path')
68+
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports */
69+
}
70+
/* eslint-enable tsdoc/syntax */
71+
72+
* getFileAttachments (prefixPath: P, onError: ErrorReporter = noop): Generator<FileAttachment<P>> {
73+
const files = this.#fs.readdirSync(prefixPath) // may throw
74+
for (const file of files) {
75+
if (!LICENSE_FILENAME_PATTERN.test(file)) {
76+
continue
77+
}
78+
const filePath = this.#path.join(prefixPath, file)
79+
if (!this.#fs.statSync(filePath).isFile()) {
80+
// Ignore all directories - they are not files :-)
81+
// Don't follow symlinks for security reasons!
82+
continue
83+
}
84+
const contentType = guessMimeTypeForLicenseFile(file)
85+
if (contentType === undefined) {
86+
continue
87+
}
88+
try {
89+
yield { filePath, file, text: new Attachment(
90+
// since we cannot be sure weather the file content is text-only, or maybe binary,
91+
// we tend to base64 everything, regardless of the detected encoding.
92+
this.#fs.readFileSync(filePath) // may throw
93+
.toString('base64'),
94+
{ contentType, encoding: AttachmentEncoding.Base64 }
95+
) }
96+
} catch (cause) {
97+
onError(new Error(`skipped license file ${filePath}`, {cause}))
98+
}
99+
}
100+
}
101+
}
102+
103+
/* eslint-disable-next-line @typescript-eslint/no-empty-function -- ack */
104+
function noop ():void {}

0 commit comments

Comments
 (0)