Skip to content

Commit 2188ac3

Browse files
committed
feat: license file gathering
Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 6fad129 commit 2188ac3

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed

src/_helpers/mime.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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} from 'node:path'
21+
22+
type MimeType = string
23+
24+
const MIME_TEXT_PLAIN: MimeType = 'text/plain'
25+
26+
const MAP_TEXT_EXTENSION_MIME: Readonly<Record<string, MimeType>> = {
27+
'': MIME_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': MIME_TEXT_PLAIN,
34+
'.rst': 'text/prs.fallenstein.rst',
35+
'.xml': 'text/xml', // not `application/xml` -- our scope is text!
36+
// add more mime types above this line. pull-requests welcome!
37+
// license-specific files
38+
'.license': MIME_TEXT_PLAIN,
39+
'.licence': MIME_TEXT_PLAIN
40+
} as const
41+
42+
const LICENSE_FILENAME_BASE = new Set(['licence', 'license'])
43+
const LICENSE_FILENAME_EXT = new Set([
44+
'.apache',
45+
'.bsd',
46+
'.gpl',
47+
'.mit'
48+
])
49+
50+
export function getMimeForLicenseFile(filename: string): MimeType | undefined {
51+
const {name, ext} = parse(filename.toLowerCase())
52+
return LICENSE_FILENAME_BASE.has(name) && LICENSE_FILENAME_EXT.has(ext)
53+
? MIME_TEXT_PLAIN
54+
: MAP_TEXT_EXTENSION_MIME[ext]
55+
}

src/builders/index.node.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
1818
*/
1919

2020
export * as FromNodePackageJson from './fromNodePackageJson.node'
21+
export * as License from './license'

src/builders/license.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
import type NATIVE_FS from 'node:fs'
22+
import type NATIVE_PATH from "node:path";
23+
import {Attachment} from '../models/attachment'
24+
import {AttachmentEncoding} from '../enums/attachmentEncoding'
25+
import {getMimeForLicenseFile} from '../_helpers/mime'
26+
27+
28+
interface FsUtils {
29+
readdirSync: typeof NATIVE_FS.readdirSync
30+
readFileSync: typeof NATIVE_FS.readFileSync
31+
statSync: typeof NATIVE_FS.statSync
32+
}
33+
34+
interface PathUtils {
35+
join: typeof NATIVE_PATH.join
36+
}
37+
38+
export interface FetchResult {
39+
file: string
40+
filePath: string
41+
text: Attachment
42+
}
43+
44+
const LICENSE_FILENAME_PATTERN = /^(?:UN)?LICEN[CS]E|.\.LICEN[CS]E$|^NOTICE$/i
45+
46+
type ErrorReporter = (e:Error) => void
47+
48+
export class LicenseEvidenceFetcher {
49+
readonly #fs: FsUtils
50+
readonly #path: PathUtils
51+
52+
/**
53+
* `fs` and `path` can be supplied, if any compatibility-layer or drop-in replacement is used.
54+
*
55+
* @param options.fs - If omitted, the native `node:fs` is used.
56+
* @param options.path - If omitted, the native `node:path` is used.
57+
*/
58+
constructor(options: { fs?: FsUtils, path?: PathUtils } = {}) {
59+
this.#fs = options.fs ?? require('node:fs')
60+
this.#path = options.path ?? require('node:path')
61+
}
62+
63+
public* fetch(prefixPath: string, onError: ErrorReporter = function () {}): Generator<FetchResult> {
64+
const files = this.#fs.readdirSync(prefixPath)
65+
for (const file of files) {
66+
if (!LICENSE_FILENAME_PATTERN.test(file)) {
67+
continue
68+
}
69+
const filePath = this.#path.join(prefixPath, file)
70+
if (!this.#fs.statSync(filePath).isFile()) {
71+
// Ignore all directories - they are not files :-)
72+
// Don't follow symlinks for security reasons!
73+
continue
74+
}
75+
const contentType = getMimeForLicenseFile(file)
76+
if (contentType === undefined) {
77+
continue
78+
}
79+
try {
80+
yield {
81+
file,
82+
filePath,
83+
text: new Attachment(
84+
this.#fs.readFileSync(filePath).toString('base64'),
85+
{
86+
contentType,
87+
encoding: AttachmentEncoding.Base64
88+
}
89+
)
90+
}
91+
} catch (e) {
92+
onError(new Error(`skipped license file ${filePath}`, {cause: e}))
93+
}
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)