diff --git a/src/_helpers/mime.ts b/src/_helpers/mime.ts new file mode 100644 index 000000000..5dbdc26b2 --- /dev/null +++ b/src/_helpers/mime.ts @@ -0,0 +1,44 @@ +/*! +This file is part of CycloneDX JavaScript Library. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +Copyright (c) OWASP Foundation. All Rights Reserved. +*/ + + +import {extname} from "path"; + +import type {MimeType} from "../types/mimeType"; + +const MAP_TEXT_EXTENSION_MIMETYPE: Readonly> = { + '': 'text/plain', // our scope is text! + '.csv': 'text/csv', + '.htm': 'text/html', + '.html': 'text/html', + '.licence': 'text/plain', + '.license': 'text/plain', + '.md': 'text/markdown', + '.rst': 'text/prs.fallenstein.rst', + '.txt': 'text/plain', + '.xml': 'text/xml' // not `application/xml` -- our scope is text! +} as const + +/** + * Returns the guessed mime type of file, based on file name extension. + * Returns undefined if tile extension is unknown. + */ +export function getMimeTypeForTextFile (filename: string): MimeType | undefined { + return MAP_TEXT_EXTENSION_MIMETYPE[extname(filename).toLowerCase()] +} diff --git a/src/builders/fromPath.node.ts b/src/builders/fromPath.node.ts new file mode 100644 index 000000000..2dea23a14 --- /dev/null +++ b/src/builders/fromPath.node.ts @@ -0,0 +1,111 @@ +/*! +This file is part of CycloneDX JavaScript Library. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +Copyright (c) OWASP Foundation. All Rights Reserved. +*/ + +/** + * Node-specifics. + */ + +import {readdirSync} from "fs"; +import {join, relative} from "path"; + +import type * as Factories from "../factories/index.node"; +import {NamedLicense} from "../models/license"; + + + +/** + * Node-specific LicenseEvidenceBuilder. + */ +export class LicenseEvidenceBuilder { + + readonly #attachmentFactory: Factories.FromPath.AttachmentFactory + + constructor(attachmentFactory: LicenseEvidenceBuilder['attachmentFactory']) { + this.#attachmentFactory = attachmentFactory + } + + get attachmentFactory (): Factories.FromPath.AttachmentFactory { + return this.#attachmentFactory + } + + /** + * Return a license on success. + * Returns undefined if it appears to bes no known text file. + * Throws error, if license attachment content could not be fetched. + * + * If `relativeFrom` is given, the file is displayed relative from there, + * else the file is displayed unmodified. + * + * @param file - path to file + * @param relativeFrom - relative path reference for file display + */ + public fromFile(file: string, relativeFrom: string | undefined = undefined): NamedLicense | undefined { + let name + if ( relativeFrom === undefined) { + name = `file: ${file}` + } else { + // `file` could be absolute or relative path - lets resolve it anyway + name = `file: ${relative(relativeFrom, file)}` + } + const text = this.#attachmentFactory.fromTextFile(file) + if (text === undefined) { + return undefined + } + return new NamedLicense(name, {text}) + } + + readonly #LICENSE_FILENAME_PATTERN = /^(?:UN)?LICEN[CS]E|.\.LICEN[CS]E$|^NOTICE$/i + + /** + * Returns a generator for license evidences in a directory. + * Throws error, if directory content could not be inspected. + * + * Unreadable files will be omitted. + * + * @param dir - path to inspect + * @param relativeFrom - relative path reference for file display + * + * @remarks + * + * Utilizes {@link fromFile}. + */ + public * fromDir(dir: string, relativeFrom: string | undefined = undefined): Generator { + // may throw if `readdirSync()` fails + const dcis = readdirSync(dir, { withFileTypes: true }) + for (const dci of dcis) { + if ( + !dci.isFile() || + !this.#LICENSE_FILENAME_PATTERN.test(dci.name.toLowerCase()) + ) { + continue + } + + let le + try { + le = this.fromFile( join(dir, dci.name), relativeFrom) + } catch (e) { + continue + } + if (le !== undefined) { + yield le + } + } + } + +} diff --git a/src/builders/index.node.ts b/src/builders/index.node.ts index 12bdce998..2f3f9ef8f 100644 --- a/src/builders/index.node.ts +++ b/src/builders/index.node.ts @@ -18,3 +18,4 @@ Copyright (c) OWASP Foundation. All Rights Reserved. */ export * as FromNodePackageJson from './fromNodePackageJson.node' +export * as FromPath from './fromPath.node' diff --git a/src/factories/fromPath.node.ts b/src/factories/fromPath.node.ts new file mode 100644 index 000000000..5a35cfd63 --- /dev/null +++ b/src/factories/fromPath.node.ts @@ -0,0 +1,68 @@ +/*! +This file is part of CycloneDX JavaScript Library. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +Copyright (c) OWASP Foundation. All Rights Reserved. +*/ + +/** + * Node-specifics. + */ + +import {readFileSync} from "fs"; + +import {getMimeTypeForTextFile} from "../_helpers/mime"; +import {AttachmentEncoding} from "../enums/attachmentEncoding"; +import {Attachment} from "../models/attachment"; +import type {MimeType} from "../types/mimeType"; + + + +/** + * Node-specific AttachmentFactory. + */ +export class AttachmentFactory { + + /** + * Throws error, if file content could not be read. + * + * Content will be base64 encoded. + */ + public fromFile(file: string, contentType: MimeType | undefined = undefined): Attachment { + return new Attachment( + // may throw if `readFileSync()` fails + readFileSync(file, {encoding: 'base64'}), + { + contentType, + encoding: AttachmentEncoding.Base64 + }) + } + + /** + * Return an attachment on success. + * Returns undefined if it appears to be no known text file. + * Throws error, if content could not be fetched. + * + * Tries to guess the file's mime-type. + */ + public fromTextFile(file: string): Attachment | undefined { + const contentType = getMimeTypeForTextFile(file) + if (contentType === undefined) { + return undefined + } + return this.fromFile(file, contentType) + } + +} diff --git a/src/factories/index.node.ts b/src/factories/index.node.ts index 872de4f2f..0cfe916a3 100644 --- a/src/factories/index.node.ts +++ b/src/factories/index.node.ts @@ -22,5 +22,6 @@ export * from './index.common' // region node-specifics export * as FromNodePackageJson from './fromNodePackageJson.node' +export * as FromPath from './fromPath.node' // endregion node-specifics