New subject-checksums input param (#198)

* new subject-checksums input param

Signed-off-by: Brian DeHamer <[email protected]>

* check for valid hex string for digest

Signed-off-by: Brian DeHamer <[email protected]>

---------

Signed-off-by: Brian DeHamer <[email protected]>
This commit is contained in:
Brian DeHamer
2025-01-21 10:32:02 -08:00
committed by GitHub
parent f03e04cc3f
commit 38bcf9b1c5
7 changed files with 391 additions and 40 deletions
+48 -6
View File
@@ -74,20 +74,25 @@ See [action.yml](action.yml)
- uses: actions/attest@v2 - uses: actions/attest@v2
with: with:
# Path to the artifact serving as the subject of the attestation. Must # Path to the artifact serving as the subject of the attestation. Must
# specify exactly one of "subject-path" or "subject-digest". May contain # specify exactly one of "subject-path", "subject-digest", or
# a glob pattern or list of paths (total subject count cannot exceed 1024). # "subject-checksums". May contain a glob pattern or list of paths
# (total subject count cannot exceed 1024).
subject-path: subject-path:
# SHA256 digest of the subject for the attestation. Must be in the form # SHA256 digest of the subject for the attestation. Must be in the form
# "sha256:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one # "sha256:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one
# of "subject-path" or "subject-digest". # of "subject-path", "subject-digest", or "subject-checksums".
subject-digest: subject-digest:
# Subject name as it should appear in the attestation. Required unless # Subject name as it should appear in the attestation. Required when
# "subject-path" is specified, in which case it will be inferred from the # identifying the subject with the "subject-digest" input.
# path.
subject-name: subject-name:
# Path to checksums file containing digest and name of subjects for
# attestation. Must specify exactly one of "subject-path", "subject-digest",
# or "subject-checksums".
subject-checksums:
# URI identifying the type of the predicate. # URI identifying the type of the predicate.
predicate-type: predicate-type:
@@ -209,6 +214,43 @@ newline delimited list:
dist/bar dist/bar
``` ```
### Identify Subjects with Checksums File
If you are using tools like
[goreleaser](https://goreleaser.com/customization/checksum/) or
[jreleaser](https://jreleaser.org/guide/latest/reference/checksum.html) which
generate a checksums file you can identify the attestation subjects by passing
the path of the checksums file to the `subject-checksums` input. Each of the
artifacts identified in the checksums file will be listed as a subject for the
attestation.
```yaml
- name: Calculate artifact digests
run: |
shasum -a 256 foo_0.0.1_* > subject.checksums.txt
- uses: actions/attest@v2
with:
subject-checksums: subject.checksums.txt
predicate-type: 'https://example.com/predicate/v1'
predicate: '{}'
```
<!-- markdownlint-disable MD038 -->
The file referenced by the `subject-checksums` input must conform to the same
format used by the shasum tools. Each subject should be listed on a separate
line including the hex-encoded digest (either SHA256 or SHA512), a space, a
single character flag indicating either binary (`*`) or text (` `) input mode,
and the filename.
<!-- markdownlint-enable MD038 -->
```text
b569bf992b287f55d78bf8ee476497e9b7e9d2bf1c338860bfb905016218c740 foo_0.0.1_darwin_amd64
a54fc515e616cac7fcf11a49d5c5ec9ec315948a5935c1e11dd610b834b14dde foo_0.0.1_darwin_arm64
```
### Container Image ### Container Image
When working with container images you can invoke the action with the When working with container images you can invoke the action with the
+4 -1
View File
@@ -43,6 +43,7 @@ const defaultInputs: main.RunInputs = {
subjectName: '', subjectName: '',
subjectDigest: '', subjectDigest: '',
subjectPath: '', subjectPath: '',
subjectChecksums: '',
pushToRegistry: false, pushToRegistry: false,
showSummary: true, showSummary: true,
githubToken: '', githubToken: '',
@@ -138,7 +139,9 @@ describe('action', () => {
expect(runMock).toHaveReturned() expect(runMock).toHaveReturned()
expect(setFailedMock).toHaveBeenCalledWith( expect(setFailedMock).toHaveBeenCalledWith(
new Error('One of subject-path or subject-digest must be provided') new Error(
'One of subject-path, subject-digest, or subject-checksums must be provided'
)
) )
}) })
}) })
+148 -4
View File
@@ -12,13 +12,14 @@ describe('subjectFromInputs', () => {
const blankInputs: SubjectInputs = { const blankInputs: SubjectInputs = {
subjectPath: '', subjectPath: '',
subjectName: '', subjectName: '',
subjectDigest: '' subjectDigest: '',
subjectChecksums: ''
} }
describe('when no inputs are provided', () => { describe('when no inputs are provided', () => {
it('throws an error', async () => { it('throws an error', async () => {
await expect(subjectFromInputs(blankInputs)).rejects.toThrow( await expect(subjectFromInputs(blankInputs)).rejects.toThrow(
/one of subject-path or subject-digest must be provided/i /one of subject-path, subject-digest, or subject-checksums must be provided/i
) )
}) })
}) })
@@ -28,11 +29,42 @@ describe('subjectFromInputs', () => {
const inputs: SubjectInputs = { const inputs: SubjectInputs = {
subjectName: 'foo', subjectName: 'foo',
subjectPath: 'path/to/subject', subjectPath: 'path/to/subject',
subjectDigest: 'digest' subjectDigest: 'digest',
subjectChecksums: ''
} }
await expect(subjectFromInputs(inputs)).rejects.toThrow( await expect(subjectFromInputs(inputs)).rejects.toThrow(
/only one of subject-path or subject-digest may be provided/i /only one of subject-path, subject-digest, or subject-checksums may be provided/i
)
})
})
describe('when both subject path and subject checksums are provided', () => {
it('throws an error', async () => {
const inputs: SubjectInputs = {
subjectName: '',
subjectPath: 'path/to/subject',
subjectDigest: '',
subjectChecksums: 'path/to/checksums'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
)
})
})
describe('when both subject digest and subject checksums are provided', () => {
it('throws an error', async () => {
const inputs: SubjectInputs = {
subjectName: 'foo',
subjectPath: '',
subjectDigest: 'digest',
subjectChecksums: 'path/to/checksums'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
) )
}) })
}) })
@@ -387,6 +419,118 @@ describe('subjectFromInputs', () => {
}) })
}) })
}) })
describe('when specifying a subject checksums file', () => {
const checksums = `
187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d demo_0.0.1_linux_amd64
badline
5d8b4751ef31f9440d843fcfa4e53ca2e25b1cb1f13fd355fdc7c24b41fe645293291ea9297ba3989078abb77ebbaac66be073618a9e4974dbd0361881d4c718 demo_0.0.1_darwin_arm64`
let dir = ''
const filename = 'checksums'
beforeEach(async () => {
// Set-up temp directory
const tmpDir = await fs.realpath(os.tmpdir())
dir = await fs.mkdtemp(tmpDir + path.sep)
// Write file to temp directory
await fs.writeFile(path.join(dir, filename), checksums)
})
afterEach(async () => {
// Clean-up temp directory
await fs.rm(dir, { recursive: true })
})
describe('when the specified path is NOT a file', () => {
it('throws an error', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: dir
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject checksums file not found/i
)
})
})
describe('when the specific path is a file', () => {
it('returns the multiple subjects', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: path.join(dir, filename)
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined()
expect(subjects).toHaveLength(2)
expect(subjects).toContainEqual({
name: 'demo_0.0.1_linux_amd64',
digest: {
sha256:
'187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d'
}
})
})
})
})
describe('when specifying a subject checksums string', () => {
const checksums = `
f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386
187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d demo_0.0.1_linux_amd64
9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5 demo_0.0.1_linux_arm64`
it('returns the multiple subjects', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined()
expect(subjects).toHaveLength(3)
expect(subjects).toContainEqual({
name: 'demo_0.0.1_linux_386',
digest: {
sha256:
'f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e'
}
})
})
})
describe('when specifying a subject checksums string with an unrecognized digest', () => {
const checksums = `f861e demo_0.0.1_linux_386`
it('throws an error', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/unknown digest algorithm/i
)
})
})
describe('when specifying a subject checksums string with an invalid digest', () => {
const checksums =
'!!!!e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386'
it('throws an error', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(/invalid digest/i)
})
})
}) })
describe('subjectDigest', () => { describe('subjectDigest', () => {
+12 -6
View File
@@ -9,20 +9,26 @@ inputs:
subject-path: subject-path:
description: > description: >
Path to the artifact serving as the subject of the attestation. Must Path to the artifact serving as the subject of the attestation. Must
specify exactly one of "subject-path" or "subject-digest". May contain a specify exactly one of "subject-path", "subject-digest", or
glob pattern or list of paths (total subject count cannot exceed 1024). "subject-checksums". May contain a glob pattern or list of paths (total
subject count cannot exceed 1024).
required: false required: false
subject-digest: subject-digest:
description: > description: >
Digest of the subject for the attestation. Must be in the form Digest of the subject for the attestation. Must be in the form
"algorithm:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one "algorithm:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one
of "subject-path" or "subject-digest". of "subject-path", "subject-digest", or "subject-checksums".
required: false required: false
subject-name: subject-name:
description: > description: >
Subject name as it should appear in the attestation. Required unless Subject name as it should appear in the attestation. Required when
"subject-path" is specified, in which case it will be inferred from the identifying the subject with the "subject-digest" input.
path. required: false
subject-checksums:
description: >
Path to checksums file containing digest and name of subjects for
attestation. Must specify exactly one of "subject-path", "subject-digest",
or "subject-checksums".
required: false required: false
predicate-type: predicate-type:
description: > description: >
Generated Vendored
+77 -12
View File
@@ -70835,6 +70835,7 @@ const inputs = {
subjectPath: core.getInput('subject-path'), subjectPath: core.getInput('subject-path'),
subjectName: core.getInput('subject-name'), subjectName: core.getInput('subject-name'),
subjectDigest: core.getInput('subject-digest'), subjectDigest: core.getInput('subject-digest'),
subjectChecksums: core.getInput('subject-checksums'),
predicateType: core.getInput('predicate-type'), predicateType: core.getInput('predicate-type'),
predicate: core.getInput('predicate'), predicate: core.getInput('predicate'),
predicatePath: core.getInput('predicate-path'), predicatePath: core.getInput('predicate-path'),
@@ -71129,23 +71130,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.formatSubjectDigest = exports.subjectFromInputs = void 0; exports.formatSubjectDigest = exports.subjectFromInputs = void 0;
const glob = __importStar(__nccwpck_require__(47206)); const glob = __importStar(__nccwpck_require__(47206));
const assert_1 = __importDefault(__nccwpck_require__(42613));
const crypto_1 = __importDefault(__nccwpck_require__(76982)); const crypto_1 = __importDefault(__nccwpck_require__(76982));
const sync_1 = __nccwpck_require__(61110); const sync_1 = __nccwpck_require__(61110);
const fs_1 = __importDefault(__nccwpck_require__(79896)); const fs_1 = __importDefault(__nccwpck_require__(79896));
const os_1 = __importDefault(__nccwpck_require__(70857));
const path_1 = __importDefault(__nccwpck_require__(16928)); const path_1 = __importDefault(__nccwpck_require__(16928));
const MAX_SUBJECT_COUNT = 1024; const MAX_SUBJECT_COUNT = 1024;
const MAX_SUBJECT_CHECKSUM_SIZE_BYTES = 512 * MAX_SUBJECT_COUNT;
const DIGEST_ALGORITHM = 'sha256'; const DIGEST_ALGORITHM = 'sha256';
const HEX_STRING_RE = /^[0-9a-fA-F]+$/;
// Returns the subject specified by the action's inputs. The subject may be // Returns the subject specified by the action's inputs. The subject may be
// specified as a path to a file or as a digest. If a path is provided, the // specified as a path to a file or as a digest. If a path is provided, the
// file's digest is calculated and returned along with the subject's name. If a // file's digest is calculated and returned along with the subject's name. If a
// digest is provided, the name must also be provided. // digest is provided, the name must also be provided.
const subjectFromInputs = async (inputs) => { const subjectFromInputs = async (inputs) => {
const { subjectPath, subjectDigest, subjectName, downcaseName } = inputs; const { subjectPath, subjectDigest, subjectName, subjectChecksums, downcaseName } = inputs;
if (!subjectPath && !subjectDigest) { const enabledInputs = [subjectPath, subjectDigest, subjectChecksums].filter(Boolean);
throw new Error('One of subject-path or subject-digest must be provided'); if (enabledInputs.length === 0) {
throw new Error('One of subject-path, subject-digest, or subject-checksums must be provided');
} }
if (subjectPath && subjectDigest) { if (enabledInputs.length > 1) {
throw new Error('Only one of subject-path or subject-digest may be provided'); throw new Error('Only one of subject-path, subject-digest, or subject-checksums may be provided');
} }
if (subjectDigest && !subjectName) { if (subjectDigest && !subjectName) {
throw new Error('subject-name must be provided when using subject-digest'); throw new Error('subject-name must be provided when using subject-digest');
@@ -71153,11 +71159,17 @@ const subjectFromInputs = async (inputs) => {
// If push-to-registry is enabled, ensure the subject name is lowercase // If push-to-registry is enabled, ensure the subject name is lowercase
// to conform to OCI image naming conventions // to conform to OCI image naming conventions
const name = downcaseName ? subjectName.toLowerCase() : subjectName; const name = downcaseName ? subjectName.toLowerCase() : subjectName;
if (subjectPath) { switch (true) {
return await getSubjectFromPath(subjectPath, name); case !!subjectPath:
} return getSubjectFromPath(subjectPath, name);
else { case !!subjectDigest:
return [getSubjectFromDigest(subjectDigest, name)]; return [getSubjectFromDigest(subjectDigest, name)];
case !!subjectChecksums:
return getSubjectFromChecksums(subjectChecksums);
/* istanbul ignore next */
default:
// This should be unreachable, but TS requires a default case
assert_1.default.fail('unreachable');
} }
}; };
exports.subjectFromInputs = subjectFromInputs; exports.subjectFromInputs = subjectFromInputs;
@@ -71173,7 +71185,7 @@ exports.formatSubjectDigest = formatSubjectDigest;
const getSubjectFromPath = async (subjectPath, subjectName) => { const getSubjectFromPath = async (subjectPath, subjectName) => {
const digestedSubjects = []; const digestedSubjects = [];
// Parse the list of subject paths // Parse the list of subject paths
const subjectPaths = parseList(subjectPath).join('\n'); const subjectPaths = parseSubjectPathList(subjectPath).join('\n');
// Expand the globbed paths to a list of actual paths // Expand the globbed paths to a list of actual paths
const paths = await glob.create(subjectPaths).then(async (g) => g.glob()); const paths = await glob.create(subjectPaths).then(async (g) => g.glob());
// Filter path list to just the files (not directories) // Filter path list to just the files (not directories)
@@ -71206,6 +71218,49 @@ const getSubjectFromDigest = (subjectDigest, subjectName) => {
digest: { [alg]: digest } digest: { [alg]: digest }
}; };
}; };
const getSubjectFromChecksums = (subjectChecksums) => {
if (fs_1.default.existsSync(subjectChecksums)) {
return getSubjectFromChecksumsFile(subjectChecksums);
}
else {
return getSubjectFromChecksumsString(subjectChecksums);
}
};
const getSubjectFromChecksumsFile = (checksumsPath) => {
const stats = fs_1.default.statSync(checksumsPath);
if (!stats.isFile()) {
throw new Error(`subject checksums file not found: ${checksumsPath}`);
}
/* istanbul ignore next */
if (stats.size > MAX_SUBJECT_CHECKSUM_SIZE_BYTES) {
throw new Error(`subject checksums file exceeds maximum allowed size: ${MAX_SUBJECT_CHECKSUM_SIZE_BYTES} bytes`);
}
const checksums = fs_1.default.readFileSync(checksumsPath, 'utf-8');
return getSubjectFromChecksumsString(checksums);
};
const getSubjectFromChecksumsString = (checksums) => {
const subjects = [];
const records = checksums.split(os_1.default.EOL).filter(Boolean);
for (const record of records) {
// Find the space delimiter following the digest
const delimIndex = record.indexOf(' ');
// Skip any line that doesn't have a delimiter
if (delimIndex === -1) {
continue;
}
// Swallow the type identifier character at the beginning of the name
const name = record.slice(delimIndex + 2);
const digest = record.slice(0, delimIndex);
if (!HEX_STRING_RE.test(digest)) {
throw new Error(`Invalid digest: ${digest}`);
}
subjects.push({
name,
digest: { [digestAlgorithm(digest)]: digest }
});
}
return subjects;
};
// Calculates the digest of a file using the specified algorithm. The file is // Calculates the digest of a file using the specified algorithm. The file is
// streamed into the digest function to avoid loading the entire file into // streamed into the digest function to avoid loading the entire file into
// memory. The returned digest is a hex string. // memory. The returned digest is a hex string.
@@ -71218,7 +71273,7 @@ const digestFile = async (algorithm, filePath) => {
.once('finish', () => resolve(hash.read())); .once('finish', () => resolve(hash.read()));
}); });
}; };
const parseList = (input) => { const parseSubjectPathList = (input) => {
const res = []; const res = [];
const records = (0, sync_1.parse)(input, { const records = (0, sync_1.parse)(input, {
columns: false, columns: false,
@@ -71231,6 +71286,16 @@ const parseList = (input) => {
} }
return res.filter(item => item).map(pat => pat.trim()); return res.filter(item => item).map(pat => pat.trim());
}; };
const digestAlgorithm = (digest) => {
switch (digest.length) {
case 64:
return 'sha256';
case 128:
return 'sha512';
default:
throw new Error(`Unknown digest algorithm: ${digest}`);
}
};
/***/ }), /***/ }),
+1
View File
@@ -8,6 +8,7 @@ const inputs: RunInputs = {
subjectPath: core.getInput('subject-path'), subjectPath: core.getInput('subject-path'),
subjectName: core.getInput('subject-name'), subjectName: core.getInput('subject-name'),
subjectDigest: core.getInput('subject-digest'), subjectDigest: core.getInput('subject-digest'),
subjectChecksums: core.getInput('subject-checksums'),
predicateType: core.getInput('predicate-type'), predicateType: core.getInput('predicate-type'),
predicate: core.getInput('predicate'), predicate: core.getInput('predicate'),
predicatePath: core.getInput('predicate-path'), predicatePath: core.getInput('predicate-path'),
+101 -11
View File
@@ -1,18 +1,23 @@
import * as glob from '@actions/glob' import * as glob from '@actions/glob'
import assert from 'assert'
import crypto from 'crypto' import crypto from 'crypto'
import { parse } from 'csv-parse/sync' import { parse } from 'csv-parse/sync'
import fs from 'fs' import fs from 'fs'
import os from 'os'
import path from 'path' import path from 'path'
import type { Subject } from '@actions/attest' import type { Subject } from '@actions/attest'
const MAX_SUBJECT_COUNT = 1024 const MAX_SUBJECT_COUNT = 1024
const MAX_SUBJECT_CHECKSUM_SIZE_BYTES = 512 * MAX_SUBJECT_COUNT
const DIGEST_ALGORITHM = 'sha256' const DIGEST_ALGORITHM = 'sha256'
const HEX_STRING_RE = /^[0-9a-fA-F]+$/
export type SubjectInputs = { export type SubjectInputs = {
subjectPath: string subjectPath: string
subjectName: string subjectName: string
subjectDigest: string subjectDigest: string
subjectChecksums: string
downcaseName?: boolean downcaseName?: boolean
} }
// Returns the subject specified by the action's inputs. The subject may be // Returns the subject specified by the action's inputs. The subject may be
@@ -22,15 +27,26 @@ export type SubjectInputs = {
export const subjectFromInputs = async ( export const subjectFromInputs = async (
inputs: SubjectInputs inputs: SubjectInputs
): Promise<Subject[]> => { ): Promise<Subject[]> => {
const { subjectPath, subjectDigest, subjectName, downcaseName } = inputs const {
subjectPath,
subjectDigest,
subjectName,
subjectChecksums,
downcaseName
} = inputs
if (!subjectPath && !subjectDigest) { const enabledInputs = [subjectPath, subjectDigest, subjectChecksums].filter(
throw new Error('One of subject-path or subject-digest must be provided') Boolean
)
if (enabledInputs.length === 0) {
throw new Error(
'One of subject-path, subject-digest, or subject-checksums must be provided'
)
} }
if (subjectPath && subjectDigest) { if (enabledInputs.length > 1) {
throw new Error( throw new Error(
'Only one of subject-path or subject-digest may be provided' 'Only one of subject-path, subject-digest, or subject-checksums may be provided'
) )
} }
@@ -42,10 +58,17 @@ export const subjectFromInputs = async (
// to conform to OCI image naming conventions // to conform to OCI image naming conventions
const name = downcaseName ? subjectName.toLowerCase() : subjectName const name = downcaseName ? subjectName.toLowerCase() : subjectName
if (subjectPath) { switch (true) {
return await getSubjectFromPath(subjectPath, name) case !!subjectPath:
} else { return getSubjectFromPath(subjectPath, name)
return [getSubjectFromDigest(subjectDigest, name)] case !!subjectDigest:
return [getSubjectFromDigest(subjectDigest, name)]
case !!subjectChecksums:
return getSubjectFromChecksums(subjectChecksums)
/* istanbul ignore next */
default:
// This should be unreachable, but TS requires a default case
assert.fail('unreachable')
} }
} }
@@ -65,7 +88,7 @@ const getSubjectFromPath = async (
const digestedSubjects: Subject[] = [] const digestedSubjects: Subject[] = []
// Parse the list of subject paths // Parse the list of subject paths
const subjectPaths = parseList(subjectPath).join('\n') const subjectPaths = parseSubjectPathList(subjectPath).join('\n')
// Expand the globbed paths to a list of actual paths // Expand the globbed paths to a list of actual paths
const paths = await glob.create(subjectPaths).then(async g => g.glob()) const paths = await glob.create(subjectPaths).then(async g => g.glob())
@@ -119,6 +142,62 @@ const getSubjectFromDigest = (
} }
} }
const getSubjectFromChecksums = (subjectChecksums: string): Subject[] => {
if (fs.existsSync(subjectChecksums)) {
return getSubjectFromChecksumsFile(subjectChecksums)
} else {
return getSubjectFromChecksumsString(subjectChecksums)
}
}
const getSubjectFromChecksumsFile = (checksumsPath: string): Subject[] => {
const stats = fs.statSync(checksumsPath)
if (!stats.isFile()) {
throw new Error(`subject checksums file not found: ${checksumsPath}`)
}
/* istanbul ignore next */
if (stats.size > MAX_SUBJECT_CHECKSUM_SIZE_BYTES) {
throw new Error(
`subject checksums file exceeds maximum allowed size: ${MAX_SUBJECT_CHECKSUM_SIZE_BYTES} bytes`
)
}
const checksums = fs.readFileSync(checksumsPath, 'utf-8')
return getSubjectFromChecksumsString(checksums)
}
const getSubjectFromChecksumsString = (checksums: string): Subject[] => {
const subjects: Subject[] = []
const records: string[] = checksums.split(os.EOL).filter(Boolean)
for (const record of records) {
// Find the space delimiter following the digest
const delimIndex = record.indexOf(' ')
// Skip any line that doesn't have a delimiter
if (delimIndex === -1) {
continue
}
// Swallow the type identifier character at the beginning of the name
const name = record.slice(delimIndex + 2)
const digest = record.slice(0, delimIndex)
if (!HEX_STRING_RE.test(digest)) {
throw new Error(`Invalid digest: ${digest}`)
}
subjects.push({
name,
digest: { [digestAlgorithm(digest)]: digest }
})
}
return subjects
}
// Calculates the digest of a file using the specified algorithm. The file is // Calculates the digest of a file using the specified algorithm. The file is
// streamed into the digest function to avoid loading the entire file into // streamed into the digest function to avoid loading the entire file into
// memory. The returned digest is a hex string. // memory. The returned digest is a hex string.
@@ -135,7 +214,7 @@ const digestFile = async (
}) })
} }
const parseList = (input: string): string[] => { const parseSubjectPathList = (input: string): string[] => {
const res: string[] = [] const res: string[] = []
const records: string[][] = parse(input, { const records: string[][] = parse(input, {
@@ -151,3 +230,14 @@ const parseList = (input: string): string[] => {
return res.filter(item => item).map(pat => pat.trim()) return res.filter(item => item).map(pat => pat.trim())
} }
const digestAlgorithm = (digest: string): string => {
switch (digest.length) {
case 64:
return 'sha256'
case 128:
return 'sha512'
default:
throw new Error(`Unknown digest algorithm: ${digest}`)
}
}