From 0c309fedf079d8cbc3234766d305f36bf506555d Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Fri, 10 Jun 2022 22:40:12 +0200 Subject: [PATCH] bring `sorted()` to model repositories Signed-off-by: Jan Kowalleck --- src/helpers/sortableSet.ts | 28 ++++++++ src/models/component.ts | 8 +-- src/models/externalReference.ts | 8 +-- src/models/hash.ts | 10 ++- src/models/license.ts | 6 +- src/models/organizationalContact.ts | 9 ++- src/models/tool.ts | 8 +-- src/serialize/json/normalize.ts | 98 +++++++++++++-------------- src/serialize/xml/normalize.ts | 100 ++++++++++++++-------------- 9 files changed, 152 insertions(+), 123 deletions(-) create mode 100644 src/helpers/sortableSet.ts diff --git a/src/helpers/sortableSet.ts b/src/helpers/sortableSet.ts new file mode 100644 index 000000000..a1766d991 --- /dev/null +++ b/src/helpers/sortableSet.ts @@ -0,0 +1,28 @@ +/*! +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. +*/ + +export interface Comparable { + compare: (other: any) => number +} + +export abstract class SortableSet extends Set { + sorted (): T[] { + return Array.from(this).sort((a, b) => a.compare(b)) + } +} diff --git a/src/models/component.ts b/src/models/component.ts index d66ba8de5..cc502604f 100644 --- a/src/models/component.ts +++ b/src/models/component.ts @@ -27,6 +27,7 @@ import { OrganizationalEntity } from './organizationalEntity' import { ExternalReferenceRepository } from './externalReference' import { LicenseRepository } from './license' import { SWID } from './swid' +import { Comparable, SortableSet } from '../helpers/sortableSet' interface OptionalProperties { bomRef?: BomRef['value'] @@ -47,7 +48,7 @@ interface OptionalProperties { cpe?: Component['cpe'] } -export class Component { +export class Component implements Comparable { type: ComponentType name: string author?: string @@ -129,8 +130,5 @@ export class Component { } } -export class ComponentRepository extends Set { - static compareItems (a: Component, b: Component): number { - return a.compare(b) - } +export class ComponentRepository extends SortableSet { } diff --git a/src/models/externalReference.ts b/src/models/externalReference.ts index 039c8c52c..b270b97ce 100644 --- a/src/models/externalReference.ts +++ b/src/models/externalReference.ts @@ -18,12 +18,13 @@ Copyright (c) OWASP Foundation. All Rights Reserved. */ import { ExternalReferenceType } from '../enums' +import { Comparable, SortableSet } from '../helpers/sortableSet' interface OptionalProperties { comment?: ExternalReference['comment'] } -export class ExternalReference { +export class ExternalReference implements Comparable { url: URL | string type: ExternalReferenceType comment?: string @@ -41,8 +42,5 @@ export class ExternalReference { } } -export class ExternalReferenceRepository extends Set { - static compareItems (a: ExternalReference, b: ExternalReference): number { - return a.compare(b) - } +export class ExternalReferenceRepository extends SortableSet { } diff --git a/src/models/hash.ts b/src/models/hash.ts index 40261514a..7d2342dc2 100644 --- a/src/models/hash.ts +++ b/src/models/hash.ts @@ -30,9 +30,13 @@ export type Hash = readonly [ ] export class HashRepository extends Map { - static compareItems (a: Hash, b: Hash): number { + #compareItems ([a1, c1]: Hash, [a2, c2]: Hash): number { /* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions -- run compares in weighted order */ - return a[0].localeCompare(b[0]) || - a[1].localeCompare(b[1]) + return a1.localeCompare(a2) || + c1.localeCompare(c2) + } + + sorted (): Hash[] { + return Array.from(this.entries()).sort(this.#compareItems) } } diff --git a/src/models/license.ts b/src/models/license.ts index 808eb42d6..89ab00a49 100644 --- a/src/models/license.ts +++ b/src/models/license.ts @@ -123,11 +123,15 @@ export type DisjunctiveLicense = NamedLicense | SpdxLicense export type License = DisjunctiveLicense | LicenseExpression export class LicenseRepository extends Set { - static compareItems (a: License, b: License): number { + #compareItems (a: License, b: License): number { if (a.constructor === b.constructor) { // @ts-expect-error -- classes are from same type -> they are comparable return a.compare(b) } return a.constructor.name.localeCompare(b.constructor.name) } + + sorted (): License[] { + return Array.from(this).sort(this.#compareItems) + } } diff --git a/src/models/organizationalContact.ts b/src/models/organizationalContact.ts index f5f04e32a..6d847608b 100644 --- a/src/models/organizationalContact.ts +++ b/src/models/organizationalContact.ts @@ -17,13 +17,15 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ +import { Comparable, SortableSet } from '../helpers/sortableSet' + interface OptionalProperties { name?: OrganizationalContact['name'] email?: OrganizationalContact['email'] phone?: OrganizationalContact['phone'] } -export class OrganizationalContact { +export class OrganizationalContact implements Comparable { name?: string email?: string phone?: string @@ -42,8 +44,5 @@ export class OrganizationalContact { } } -export class OrganizationalContactRepository extends Set { - static compareItems (a: OrganizationalContact, b: OrganizationalContact): number { - return a.compare(b) - } +export class OrganizationalContactRepository extends SortableSet { } diff --git a/src/models/tool.ts b/src/models/tool.ts index 0ced0a7a8..c425c5aae 100644 --- a/src/models/tool.ts +++ b/src/models/tool.ts @@ -19,6 +19,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. import { HashRepository } from './hash' import { ExternalReferenceRepository } from './externalReference' +import { Comparable, SortableSet } from '../helpers/sortableSet' interface OptionalProperties { vendor?: Tool['vendor'] @@ -28,7 +29,7 @@ interface OptionalProperties { externalReferences?: Tool['externalReferences'] } -export class Tool { +export class Tool implements Comparable { vendor?: string name?: string version?: string @@ -51,8 +52,5 @@ export class Tool { } } -export class ToolRepository extends Set { - static compareItems (a: Tool, b: Tool): number { - return a.compare(b) - } +export class ToolRepository extends SortableSet { } diff --git a/src/serialize/json/normalize.ts b/src/serialize/json/normalize.ts index 73d4c286b..28a96d86b 100644 --- a/src/serialize/json/normalize.ts +++ b/src/serialize/json/normalize.ts @@ -93,7 +93,7 @@ const schemaUrl: ReadonlyMap = new Map([ interface Normalizer { normalize: (data: object, options: NormalizerOptions) => object | undefined - normalizeIter?: (data: Iterable, options: NormalizerOptions) => object[] + normalizeRepository?: (data: Iterable, options: NormalizerOptions) => object[] } abstract class Base implements Normalizer { @@ -120,7 +120,7 @@ export class BomNormalizer extends Base { serialNumber: data.serialNumber, metadata: this._factory.makeForMetadata().normalize(data.metadata, options), components: data.components.size > 0 - ? this._factory.makeForComponent().normalizeIter(data.components, options) + ? this._factory.makeForComponent().normalizeRepository(data.components, options) // spec < 1.4 requires `component` to be array : [], dependencies: this._factory.spec.supportsDependencyGraph @@ -136,10 +136,10 @@ export class MetadataNormalizer extends Base { return { timestamp: data.timestamp?.toISOString(), tools: data.tools.size > 0 - ? this._factory.makeForTool().normalizeIter(data.tools, options) + ? this._factory.makeForTool().normalizeRepository(data.tools, options) : undefined, authors: data.authors.size > 0 - ? this._factory.makeForOrganizationalContact().normalizeIter(data.authors, options) + ? this._factory.makeForOrganizationalContact().normalizeRepository(data.authors, options) : undefined, component: data.component === undefined ? undefined @@ -161,20 +161,20 @@ export class ToolNormalizer extends Base { name: data.name || undefined, version: data.version || undefined, hashes: data.hashes.size > 0 - ? this._factory.makeForHash().normalizeIter(data.hashes, options) + ? this._factory.makeForHash().normalizeRepository(data.hashes, options) : undefined, externalReferences: this._factory.spec.supportsToolReferences && data.externalReferences.size > 0 - ? this._factory.makeForExternalReference().normalizeIter(data.externalReferences, options) + ? this._factory.makeForExternalReference().normalizeRepository(data.externalReferences, options) : undefined } } - normalizeIter (data: Iterable, options: NormalizerOptions): Normalized.Tool[] { - const tools = Array.from(data) - if (options.sortLists ?? false) { - tools.sort(Models.ToolRepository.compareItems) - } - return tools.map(t => this.normalize(t, options)) + normalizeRepository (data: Models.ToolRepository, options: NormalizerOptions): Normalized.Tool[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(t => this.normalize(t, options)) } } @@ -189,13 +189,13 @@ export class HashNormalizer extends Base { : undefined } - normalizeIter (data: Iterable, options: NormalizerOptions): Normalized.Hash[] { - const hashes = Array.from(data) - if (options.sortLists ?? false) { - hashes.sort(Models.HashRepository.compareItems) - } - return hashes.map(h => this.normalize(h, options)) - .filter(isNotUndefined) + normalizeRepository (data: Models.HashRepository, options: NormalizerOptions): Normalized.Hash[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(h => this.normalize(h, options) + ).filter(isNotUndefined) } } @@ -210,12 +210,12 @@ export class OrganizationalContactNormalizer extends Base { } } - normalizeIter (data: Iterable, options: NormalizerOptions): Normalized.OrganizationalContact[] { - const contacts = Array.from(data) - if (options.sortLists ?? false) { - contacts.sort(Models.OrganizationalContactRepository.compareItems) - } - return contacts.map(c => this.normalize(c, options)) + normalizeRepository (data: Models.OrganizationalContactRepository, options: NormalizerOptions): Normalized.OrganizationalContact[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(c => this.normalize(c, options)) } } @@ -230,7 +230,7 @@ export class OrganizationalEntityNormalizer extends Base { ? urls : undefined, contact: data.contact.size > 0 - ? this._factory.makeForOrganizationalContact().normalizeIter(data.contact, options) + ? this._factory.makeForOrganizationalContact().normalizeRepository(data.contact, options) : undefined } } @@ -254,10 +254,10 @@ export class ComponentNormalizer extends Base { description: data.description || undefined, scope: data.scope, hashes: data.hashes.size > 0 - ? this._factory.makeForHash().normalizeIter(data.hashes, options) + ? this._factory.makeForHash().normalizeRepository(data.hashes, options) : undefined, licenses: data.licenses.size > 0 - ? this._factory.makeForLicense().normalizeIter(data.licenses, options) + ? this._factory.makeForLicense().normalizeRepository(data.licenses, options) : undefined, copyright: data.copyright || undefined, cpe: data.cpe || undefined, @@ -266,19 +266,19 @@ export class ComponentNormalizer extends Base { ? undefined : this._factory.makeForSWID().normalize(data.swid, options), externalReferences: data.externalReferences.size > 0 - ? this._factory.makeForExternalReference().normalizeIter(data.externalReferences, options) + ? this._factory.makeForExternalReference().normalizeRepository(data.externalReferences, options) : undefined } : undefined } - normalizeIter (data: Iterable, options: NormalizerOptions): Normalized.Component[] { - const components = Array.from(data) - if (options.sortLists ?? false) { - components.sort(Models.ComponentRepository.compareItems) - } - return components.map(c => this.normalize(c, options)) - .filter(isNotUndefined) + normalizeRepository (data: Models.ComponentRepository, options: NormalizerOptions): Normalized.Component[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(c => this.normalize(c, options) + ).filter(isNotUndefined) } } @@ -327,12 +327,12 @@ export class LicenseNormalizer extends Base { } } - normalizeIter (data: Iterable, options: NormalizerOptions): Normalized.License[] { - const licenses = Array.from(data) - if (options.sortLists ?? false) { - licenses.sort(Models.LicenseRepository.compareItems) - } - return licenses.map(c => this.normalize(c, options)) + normalizeRepository (data: Models.LicenseRepository, options: NormalizerOptions): Normalized.License[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(c => this.normalize(c, options)) } } @@ -366,13 +366,13 @@ export class ExternalReferenceNormalizer extends Base { : undefined } - normalizeIter (data: Iterable, options: NormalizerOptions): Normalized.ExternalReference[] { - const refs = Array.from(data) - if (options.sortLists ?? false) { - refs.sort(Models.ExternalReferenceRepository.compareItems) - } - return refs.map(r => this.normalize(r, options)) - .filter(isNotUndefined) + normalizeRepository (data: Models.ExternalReferenceRepository, options: NormalizerOptions): Normalized.ExternalReference[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(r => this.normalize(r, options) + ).filter(isNotUndefined) } } diff --git a/src/serialize/xml/normalize.ts b/src/serialize/xml/normalize.ts index 2eb4b7db6..985e10de5 100644 --- a/src/serialize/xml/normalize.ts +++ b/src/serialize/xml/normalize.ts @@ -93,7 +93,7 @@ const xmlNamespace: ReadonlyMap = new Map([ interface Normalizer { normalize: (data: object, options: NormalizerOptions, elementName?: string) => object | undefined - normalizeIter?: (data: Iterable, options: NormalizerOptions, elementName: string) => object[] + normalizeRepository?: (data: Iterable, options: NormalizerOptions, elementName: string) => object[] } abstract class Base implements Normalizer { @@ -122,7 +122,7 @@ export class BomNormalizer extends Base { type: 'element', name: 'components', children: data.components.size > 0 - ? this._factory.makeForComponent().normalizeIter(data.components, options, 'component') + ? this._factory.makeForComponent().normalizeRepository(data.components, options, 'component') : undefined } return { @@ -161,7 +161,7 @@ export class MetadataNormalizer extends Base { ? { type: 'element', name: 'tools', - children: this._factory.makeForTool().normalizeIter(data.tools, options, 'tool') + children: this._factory.makeForTool().normalizeRepository(data.tools, options, 'tool') } : undefined const authors: SimpleXml.Element | undefined = data.authors.size > 0 @@ -169,7 +169,7 @@ export class MetadataNormalizer extends Base { type: 'element', name: 'authors', children: this._factory.makeForOrganizationalContact() - .normalizeIter(data.authors, options, 'author') + .normalizeRepository(data.authors, options, 'author') } : undefined return { @@ -199,7 +199,7 @@ export class ToolNormalizer extends Base { ? { type: 'element', name: 'hashes', - children: this._factory.makeForHash().normalizeIter(data.hashes, options, 'hash') + children: this._factory.makeForHash().normalizeRepository(data.hashes, options, 'hash') } : undefined const externalReferences: SimpleXml.Element | undefined = @@ -208,7 +208,7 @@ export class ToolNormalizer extends Base { type: 'element', name: 'externalReferences', children: this._factory.makeForExternalReference() - .normalizeIter(data.externalReferences, options, 'reference') + .normalizeRepository(data.externalReferences, options, 'reference') } : undefined return { @@ -224,12 +224,12 @@ export class ToolNormalizer extends Base { } } - normalizeIter (data: Iterable, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { - const tools = Array.from(data) - if (options.sortLists) { - tools.sort(Models.ToolRepository.compareItems) - } - return tools.map(t => this.normalize(t, options, elementName)) + normalizeRepository (data: Models.ToolRepository, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(t => this.normalize(t, options, elementName)) } } @@ -246,13 +246,13 @@ export class HashNormalizer extends Base { : undefined } - normalizeIter (data: Iterable, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { - const hashes = Array.from(data) - if (options.sortLists ?? false) { - hashes.sort(Models.HashRepository.compareItems) - } - return hashes.map(h => this.normalize(h, options, elementName)) - .filter(isNotUndefined) + normalizeRepository (data: Models.HashRepository, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(h => this.normalize(h, options, elementName) + ).filter(isNotUndefined) } } @@ -269,12 +269,12 @@ export class OrganizationalContactNormalizer extends Base { } } - normalizeIter (data: Iterable, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { - const contacts = Array.from(data) - if (options.sortLists ?? false) { - contacts.sort(Models.OrganizationalContactRepository.compareItems) - } - return contacts.map(c => this.normalize(c, options, elementName)) + normalizeRepository (data: Models.OrganizationalContactRepository, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(c => this.normalize(c, options, elementName)) } } @@ -287,7 +287,7 @@ export class OrganizationalEntityNormalizer extends Base { makeOptionalTextElement(data.name, 'name'), ...makeTextElementIter(data.url, options, 'url') .filter(({ children: u }) => XmlSchema.isAnyURI(u)), - ...this._factory.makeForOrganizationalContact().normalizeIter(data.contact, options, 'contact') + ...this._factory.makeForOrganizationalContact().normalizeRepository(data.contact, options, 'contact') ].filter(isNotUndefined) } } @@ -305,14 +305,14 @@ export class ComponentNormalizer extends Base { ? { type: 'element', name: 'hashes', - children: this._factory.makeForHash().normalizeIter(data.hashes, options, 'hash') + children: this._factory.makeForHash().normalizeRepository(data.hashes, options, 'hash') } : undefined const licenses: SimpleXml.Element | undefined = data.licenses.size > 0 ? { type: 'element', name: 'licenses', - children: this._factory.makeForLicense().normalizeIter(data.licenses, options) + children: this._factory.makeForLicense().normalizeRepository(data.licenses, options) } : undefined const swid: SimpleXml.Element | undefined = data.swid === undefined @@ -323,7 +323,7 @@ export class ComponentNormalizer extends Base { type: 'element', name: 'externalReferences', children: this._factory.makeForExternalReference() - .normalizeIter(data.externalReferences, options, 'reference') + .normalizeRepository(data.externalReferences, options, 'reference') } : undefined return { @@ -357,13 +357,13 @@ export class ComponentNormalizer extends Base { } } - normalizeIter (data: Iterable, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { - const components = Array.from(data) - if (options.sortLists ?? false) { - components.sort(Models.ComponentRepository.compareItems) - } - return components.map(c => this.normalize(c, options, elementName)) - .filter(isNotUndefined) + normalizeRepository (data: Models.ComponentRepository, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(c => this.normalize(c, options, elementName) + ).filter(isNotUndefined) } } @@ -420,12 +420,12 @@ export class LicenseNormalizer extends Base { return makeTextElement(data.expression, 'expression') } - normalizeIter (data: Models.LicenseRepository, options: NormalizerOptions): SimpleXml.Element[] { - const licenses = Array.from(data) - if (options.sortLists ?? false) { - licenses.sort(Models.LicenseRepository.compareItems) - } - return licenses.map(c => this.normalize(c, options)) + normalizeRepository (data: Models.LicenseRepository, options: NormalizerOptions): SimpleXml.Element[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(c => this.normalize(c, options)) } } @@ -460,7 +460,7 @@ export class ExternalReferenceNormalizer extends Base { normalize (data: Models.ExternalReference, options: NormalizerOptions, elementName: string): SimpleXml.Element | undefined { const url = data.url.toString() return this._factory.spec.supportsExternalReferenceType(data.type) && - XmlSchema.isAnyURI(url) + XmlSchema.isAnyURI(url) ? { type: 'element', name: elementName, @@ -475,13 +475,13 @@ export class ExternalReferenceNormalizer extends Base { : undefined } - normalizeIter (data: Iterable, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { - const references = Array.from(data) - if (options.sortLists ?? false) { - references.sort(Models.ExternalReferenceRepository.compareItems) - } - return references.map(r => this.normalize(r, options, elementName)) - .filter(isNotUndefined) + normalizeRepository (data: Models.ExternalReferenceRepository, options: NormalizerOptions, elementName: string): SimpleXml.Element[] { + return ( + options.sortLists ?? false + ? data.sorted() + : Array.from(data) + ).map(r => this.normalize(r, options, elementName) + ).filter(isNotUndefined) } }