1+ "use strict" ;
2+
3+ /**
4+ * validate all test data for a given version of CycloneDX.
5+ * call the script via `node <this-file> -v <CDX-version>`
6+ */
7+
8+ import { readFile , stat } from 'node:fs/promises'
9+ import { dirname , join } from 'node:path'
10+ import { fileURLToPath } from 'node:url'
11+ import { parseArgs } from 'node:util'
12+
13+ import Ajv from "ajv"
14+ import addFormats from "ajv-formats"
15+ import addFormats2019 from "ajv-formats-draft2019"
16+ import { globSync } from 'glob'
17+
18+
19+ const _thisDir = dirname ( fileURLToPath ( import . meta. url ) )
20+
21+ // region config
22+
23+ const testschemaVersion = ( parseArgs ( { options : { v : { type : 'string' , short : 'v' } } } ) . values . v ?? '' ) . trim ( )
24+ const schemaDir = join ( _thisDir , '..' , '..' , '..' , '..' , 'schema' )
25+ const schemaFile = join ( schemaDir , `bom-${ testschemaVersion } .schema.json` )
26+ const testdataDir = join ( _thisDir , '..' , 'resources' , testschemaVersion )
27+
28+ if ( testschemaVersion . length === 0 ) {
29+ throw new Error ( 'missing testschemaVersion. expected via argument' )
30+ }
31+ console . debug ( 'DEBUG | testschemaVersion = ' , testschemaVersion ) ;
32+
33+ if ( ! await stat ( schemaFile ) . then ( s => s . isFile ( ) ) . catch ( ( ) => false ) ) {
34+ throw new Error ( `missing schemaFile: ${ schemaFile } ` ) ;
35+ }
36+ console . debug ( 'DEBUG | schemaFile = ' , schemaFile ) ;
37+
38+ if ( ! await stat ( testdataDir ) . then ( s => s . isDirectory ( ) ) . catch ( ( ) => false ) ) {
39+ throw new Error ( `missing testdataDir: ${ testdataDir } ` ) ;
40+ }
41+ console . debug ( 'DEBUG | testdataDir = ' , testdataDir ) ;
42+
43+ // endregion config
44+
45+ // region validator
46+
47+ const [ spdxSchema , jsfSchema , bomSchema ] = await Promise . all ( [
48+ readFile ( join ( schemaDir , 'spdx.schema.json' ) , 'utf-8' ) . then ( JSON . parse ) ,
49+ readFile ( join ( schemaDir , 'jsf-0.82.schema.json' ) , 'utf-8' ) . then ( JSON . parse ) ,
50+ readFile ( schemaFile , 'utf-8' ) . then ( JSON . parse )
51+ ] )
52+
53+ const ajv = new Ajv ( {
54+ // not running in strict - this is done in the linter-test already
55+ strict : false ,
56+ validateFormats : true ,
57+ addUsedSchema : false ,
58+ schemas : {
59+ 'http://cyclonedx.org/schema/spdx.schema.json' : spdxSchema ,
60+ 'http://cyclonedx.org/schema/jsf-0.82.schema.json' : jsfSchema
61+ }
62+ } ) ;
63+ addFormats ( ajv )
64+ addFormats2019 ( ajv , { formats : [ 'idn-email' ] } )
65+ // there is just no working implementation for format "iri-reference"
66+ // see https://github.com/luzlab/ajv-formats-draft2019/issues/22
67+ ajv . addFormat ( 'iri-reference' , true )
68+ if ( testschemaVersion === '1.2' ) {
69+ // CycloneDX 1.2 had a wrong undefined format `string`.
70+ // Let's ignore this format only for this special version.
71+ ajv . addFormat ( 'string' , true )
72+ }
73+ const _ajvValidate = ajv . compile ( bomSchema )
74+
75+ /**
76+ * @param {string } file - file path to validate
77+ * @return {null|object }
78+ */
79+ async function validateFile ( file ) {
80+ return _ajvValidate ( await readFile ( file , 'utf-8' ) . then ( JSON . parse ) )
81+ ? null
82+ : _ajvValidate . errors
83+ }
84+
85+ // endregion validator
86+
87+ let errCnt = 0
88+
89+ for ( const file of globSync ( join ( testdataDir , 'valid-*.json' ) ) ) {
90+ console . log ( '\ntest' , file , '...' ) ;
91+ const validationErrors = await validateFile ( file )
92+ if ( validationErrors === null ) {
93+ console . log ( 'OK.' )
94+ } else {
95+ ++ errCnt ;
96+ console . error ( 'ERROR: Unexpected validation error for file:' , file ) ;
97+ console . error ( validationErrors )
98+ }
99+ }
100+
101+ for ( const file of globSync ( join ( testdataDir , 'invalid-*.json' ) ) ) {
102+ console . log ( '\ntest' , file , '...' ) ;
103+ const validationErrors = await validateFile ( file )
104+ if ( validationErrors === null ) {
105+ ++ errCnt ;
106+ console . error ( 'ERROR: Missing expected validation error for file:' , file ) ;
107+
108+ } else {
109+ console . log ( 'OK.' )
110+ }
111+ }
112+
113+
114+ // Exit statuses should be in the range 0 to 254.
115+ // The status 0 is used to terminate the program successfully.
116+ process . exitCode = Math . min ( errCnt , 254 )
0 commit comments