Skip to content

Commit 80a74b1

Browse files
committed
ARSN-557: add checksum to object metadata
1 parent 21f6a54 commit 80a74b1

File tree

5 files changed

+325
-0
lines changed

5 files changed

+325
-0
lines changed

lib/models/ObjectMD.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import ObjectMDLocation, {
99
import ObjectMDAmzRestore from './ObjectMDAmzRestore';
1010
import ObjectMDArchive from './ObjectMDArchive';
1111
import { ObjectMDAzureInfoMetadata } from './ObjectMDAzureInfo';
12+
import ObjectMDChecksum, { ChecksumAlgorithm, ChecksumType } from './ObjectMDChecksum';
1213

1314
export type ACL = {
1415
Canned: string;
@@ -102,6 +103,7 @@ export type ObjectMDData = {
102103
// This is the canonical Id for the bucket owner.
103104
// This is only set when it differs from `owner-id`.
104105
bucketOwnerId?: string;
106+
checksum?: ObjectMDChecksum;
105107
};
106108

107109
/**
@@ -275,6 +277,10 @@ export default class ObjectMD {
275277
// @ts-ignore
276278
this.setLocation([{ key: this._data.location }]);
277279
}
280+
if (this._data.checksum && !(this._data.checksum instanceof ObjectMDChecksum)) {
281+
const { checksumAlgorithm, checksumValue, checksumType } = this._data.checksum;
282+
this._data.checksum = new ObjectMDChecksum(checksumAlgorithm, checksumValue, checksumType);
283+
}
278284
}
279285

280286
/**
@@ -477,6 +483,38 @@ export default class ObjectMD {
477483
return this._data['content-md5'];
478484
}
479485

486+
/**
487+
* Set checksum
488+
*
489+
* @param checksum - ObjectMDChecksum instance
490+
* @return itself
491+
*/
492+
setChecksum(checksum: ObjectMDChecksum | {
493+
checksumAlgorithm: ChecksumAlgorithm;
494+
checksumValue: string;
495+
checksumType: ChecksumType;
496+
}) {
497+
if (checksum instanceof ObjectMDChecksum) {
498+
this._data.checksum = checksum;
499+
} else if (ObjectMDChecksum.isValid(checksum) === null) {
500+
this._data.checksum = new ObjectMDChecksum(
501+
checksum.checksumAlgorithm,
502+
checksum.checksumValue,
503+
checksum.checksumType,
504+
);
505+
} else {
506+
throw new Error('checksum must be of type ObjectMDChecksum.');
507+
}
508+
return this;
509+
}
510+
511+
/**
512+
* Returns checksum as an ObjectMDChecksum instance, or null if not set.
513+
*/
514+
getChecksum(): ObjectMDChecksum | null {
515+
return this._data.checksum ?? null;
516+
}
517+
480518
/**
481519
* Set content-language
482520
*

lib/models/ObjectMDChecksum.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
export const CHECKSUM_ALGORITHMS = ['crc32', 'crc32c', 'crc64nvme', 'sha1', 'sha256'] as const;
2+
export const CHECKSUM_TYPES = ['FULL_OBJECT', 'COMPOSITE'] as const;
3+
4+
export type ChecksumAlgorithm = typeof CHECKSUM_ALGORITHMS[number];
5+
export type ChecksumType = typeof CHECKSUM_TYPES[number];
6+
7+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
8+
9+
type AlgoSpec = {
10+
xmlTag: string;
11+
digestLength: number;
12+
};
13+
14+
const algoSpecs: Record<ChecksumAlgorithm, AlgoSpec> = {
15+
crc32: { xmlTag: 'ChecksumCRC32', digestLength: 8 },
16+
crc32c: { xmlTag: 'ChecksumCRC32C', digestLength: 8 },
17+
crc64nvme: { xmlTag: 'ChecksumCRC64NVME', digestLength: 12 },
18+
sha1: { xmlTag: 'ChecksumSHA1', digestLength: 28 },
19+
sha256: { xmlTag: 'ChecksumSHA256', digestLength: 44 },
20+
};
21+
22+
function isValidDigest(algorithm: ChecksumAlgorithm, value: string): boolean {
23+
const { digestLength } = algoSpecs[algorithm];
24+
return typeof value === 'string' && value.length === digestLength && base64Regex.test(value);
25+
}
26+
27+
/**
28+
* Represents an object checksum stored in object metadata.
29+
*
30+
* Internal representation uses plain algorithm/value/type fields.
31+
* The toGetObjectAttributesXML() method produces the wire XML fragment
32+
* expected inside a GetObjectAttributesResponse Checksum element.
33+
*/
34+
export default class ObjectMDChecksum {
35+
checksumAlgorithm: ChecksumAlgorithm;
36+
checksumValue: string;
37+
checksumType: ChecksumType;
38+
39+
static isValid(data: {
40+
checksumAlgorithm: ChecksumAlgorithm;
41+
checksumValue: string;
42+
checksumType: ChecksumType;
43+
}): string | null {
44+
if (!CHECKSUM_ALGORITHMS.includes(data.checksumAlgorithm)) {
45+
return `invalid checksumAlgorithm: ${data.checksumAlgorithm}`;
46+
}
47+
if (!CHECKSUM_TYPES.includes(data.checksumType)) {
48+
return `invalid checksumType: ${data.checksumType}`;
49+
}
50+
if (!isValidDigest(data.checksumAlgorithm, data.checksumValue)) {
51+
return `invalid checksumValue for ${data.checksumAlgorithm}: ${data.checksumValue}`;
52+
}
53+
return null;
54+
}
55+
56+
constructor(
57+
checksumAlgorithm: ChecksumAlgorithm,
58+
checksumValue: string,
59+
checksumType: ChecksumType,
60+
) {
61+
const error = ObjectMDChecksum.isValid({ checksumAlgorithm, checksumValue, checksumType });
62+
if (error !== null) {
63+
throw new Error(error);
64+
}
65+
this.checksumAlgorithm = checksumAlgorithm;
66+
this.checksumValue = checksumValue;
67+
this.checksumType = checksumType;
68+
}
69+
70+
/**
71+
* Returns the XML fragment for the Checksum element in a
72+
* GetObjectAttributes response, e.g.:
73+
* <ChecksumSHA256>abc=</ChecksumSHA256>
74+
* <ChecksumType>FULL_OBJECT</ChecksumType>
75+
*/
76+
toGetObjectAttributesXML(): string {
77+
const { xmlTag } = algoSpecs[this.checksumAlgorithm];
78+
return '<Checksum>' +
79+
`<${xmlTag}>${this.checksumValue}</${xmlTag}>` +
80+
`<ChecksumType>${this.checksumType}</ChecksumType>` +
81+
'</Checksum>';
82+
}
83+
}

lib/models/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export { default as NotificationConfiguration } from './NotificationConfiguratio
99
export { default as ObjectLockConfiguration } from './ObjectLockConfiguration';
1010
export { default as ObjectMD } from './ObjectMD';
1111
export { default as ObjectMDAmzRestore } from './ObjectMDAmzRestore';
12+
export { default as ObjectMDChecksum, CHECKSUM_ALGORITHMS, CHECKSUM_TYPES } from './ObjectMDChecksum';
13+
export type { ChecksumAlgorithm, ChecksumType } from './ObjectMDChecksum';
1214
export { default as ObjectMDArchive } from './ObjectMDArchive';
1315
export { default as ObjectMDAzureInfo } from './ObjectMDAzureInfo';
1416
export { default as ObjectMDLocation } from './ObjectMDLocation';

tests/unit/models/ObjectMD.spec.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const assert = require('assert');
22
const ObjectMD = require('../../../lib/models/ObjectMD').default;
3+
const ObjectMDChecksum = require('../../../lib/models/ObjectMDChecksum').default;
34
const constants = require('../../../lib/constants');
45
const ExternalNullVersionId = require('../../../lib/versioning/constants')
56
.VersioningConstants.ExternalNullVersionId;
@@ -860,3 +861,72 @@ describe('ObjectMD::getEncodedVersionId', () => {
860861
assert.strictEqual(objMd.getEncodedVersionId(), ExternalNullVersionId);
861862
});
862863
});
864+
865+
describe('ObjectMD checksum', () => {
866+
const sha256Digest = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
867+
868+
it('should store an ObjectMDChecksum instance when setChecksum is called with a valid plain object', () => {
869+
const md = new ObjectMD();
870+
md.setChecksum({
871+
checksumAlgorithm: 'sha256',
872+
checksumValue: sha256Digest,
873+
checksumType: 'FULL_OBJECT',
874+
});
875+
const c = md.getChecksum();
876+
assert(c instanceof ObjectMDChecksum);
877+
assert.doesNotThrow(() => c.toGetObjectAttributesXML());
878+
});
879+
880+
it('should throw when setChecksum is given an invalid object', () => {
881+
const md = new ObjectMD();
882+
assert.throws(() => {
883+
md.setChecksum({ checksumAlgorithm: 'sha256' });
884+
}, /ObjectMDChecksum/);
885+
});
886+
887+
it('should return null when no checksum is set', () => {
888+
const md = new ObjectMD();
889+
assert.strictEqual(md.getChecksum(), null);
890+
});
891+
892+
it('should preserve algorithm, value, and type through setChecksum / getChecksum', () => {
893+
const md = new ObjectMD();
894+
md.setChecksum(new ObjectMDChecksum('crc64nvme', 'HyOpGHolkII=', 'FULL_OBJECT'));
895+
const result = md.getChecksum();
896+
assert(result !== null);
897+
assert.strictEqual(result.checksumAlgorithm, 'crc64nvme');
898+
assert.strictEqual(result.checksumValue, 'HyOpGHolkII=');
899+
assert.strictEqual(result.checksumType, 'FULL_OBJECT');
900+
});
901+
902+
it('should return an ObjectMDChecksum instance after JSON round-trip', () => {
903+
const md = new ObjectMD();
904+
md.setChecksum(new ObjectMDChecksum('sha256', sha256Digest, 'FULL_OBJECT'));
905+
const { result } = ObjectMD.createFromBlob(md.getSerialized());
906+
assert(result !== undefined);
907+
assert(result.getChecksum() instanceof ObjectMDChecksum);
908+
});
909+
910+
it('should preserve algorithm, value, and type through JSON round-trip', () => {
911+
const md = new ObjectMD();
912+
md.setChecksum(new ObjectMDChecksum('sha256', sha256Digest, 'COMPOSITE'));
913+
const { result } = ObjectMD.createFromBlob(md.getSerialized());
914+
assert(result !== undefined);
915+
const c = result.getChecksum();
916+
assert.strictEqual(c.checksumAlgorithm, 'sha256');
917+
assert.strictEqual(c.checksumValue, sha256Digest);
918+
assert.strictEqual(c.checksumType, 'COMPOSITE');
919+
});
920+
921+
it('should produce valid XML from getChecksum after JSON round-trip', () => {
922+
const md = new ObjectMD();
923+
md.setChecksum(new ObjectMDChecksum('crc64nvme', 'HyOpGHolkII=', 'FULL_OBJECT'));
924+
const { result } = ObjectMD.createFromBlob(md.getSerialized());
925+
assert(result !== undefined);
926+
const xml = result.getChecksum().toGetObjectAttributesXML();
927+
assert(xml.startsWith('<Checksum>'));
928+
assert(xml.includes('<ChecksumCRC64NVME>HyOpGHolkII=</ChecksumCRC64NVME>'));
929+
assert(xml.includes('<ChecksumType>FULL_OBJECT</ChecksumType>'));
930+
assert(xml.endsWith('</Checksum>'));
931+
});
932+
});
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import assert from 'assert';
2+
3+
import ObjectMDChecksum, { ChecksumAlgorithm } from '../../../lib/models/ObjectMDChecksum';
4+
5+
// Valid base64 digest values of the correct length for each algorithm.
6+
const validDigest: Record<ChecksumAlgorithm, string> = {
7+
crc32: 'AAAAAA==', // 8 chars
8+
crc32c: 'AAAAAA==', // 8 chars
9+
crc64nvme: 'AAAAAAAAAAA=', // 12 chars
10+
sha1: 'AAAAAAAAAAAAAAAAAAAAAAAAAAA=', // 28 chars
11+
sha256: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // 44 chars
12+
};
13+
14+
describe('ObjectMDChecksum', () => {
15+
describe('constructor', () => {
16+
it('should store algorithm, value, and type', () => {
17+
const c = new ObjectMDChecksum('sha256', validDigest.sha256, 'FULL_OBJECT');
18+
assert.strictEqual(c.checksumAlgorithm, 'sha256');
19+
assert.strictEqual(c.checksumValue, validDigest.sha256);
20+
assert.strictEqual(c.checksumType, 'FULL_OBJECT');
21+
});
22+
23+
it('should throw on invalid checksumAlgorithm', () => {
24+
assert.throws(
25+
// @ts-expect-error intentionally invalid
26+
() => new ObjectMDChecksum('sha999', validDigest.sha256, 'FULL_OBJECT'),
27+
/invalid checksumAlgorithm/,
28+
);
29+
});
30+
31+
it('should throw on invalid checksumType', () => {
32+
assert.throws(
33+
// @ts-expect-error intentionally invalid
34+
() => new ObjectMDChecksum('sha256', validDigest.sha256, 'WRONG'),
35+
/invalid checksumType/,
36+
);
37+
});
38+
39+
it('should throw on invalid checksumValue (wrong length)', () => {
40+
assert.throws(
41+
() => new ObjectMDChecksum('sha256', 'tooshort=', 'FULL_OBJECT'),
42+
/invalid checksumValue/,
43+
);
44+
});
45+
46+
it('should throw on invalid checksumValue (not base64)', () => {
47+
assert.throws(
48+
() => new ObjectMDChecksum('sha256', '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!=', 'FULL_OBJECT'),
49+
/invalid checksumValue/,
50+
);
51+
});
52+
});
53+
54+
describe('toGetObjectAttributesXML', () => {
55+
const cases: [ChecksumAlgorithm, string][] = [
56+
['crc32', 'ChecksumCRC32'],
57+
['crc32c', 'ChecksumCRC32C'],
58+
['crc64nvme', 'ChecksumCRC64NVME'],
59+
['sha1', 'ChecksumSHA1'],
60+
['sha256', 'ChecksumSHA256'],
61+
];
62+
63+
for (const [algo, xmlTag] of cases) {
64+
it(`should use <${xmlTag}> for algorithm "${algo}"`, () => {
65+
const c = new ObjectMDChecksum(algo, validDigest[algo], 'FULL_OBJECT');
66+
assert(c.toGetObjectAttributesXML().includes(`<${xmlTag}>`),
67+
`expected <${xmlTag}> in XML`);
68+
});
69+
}
70+
71+
it('should wrap the checksum value inside the algorithm tag', () => {
72+
const c = new ObjectMDChecksum('crc64nvme', 'HyOpGHolkII=', 'FULL_OBJECT');
73+
assert(c.toGetObjectAttributesXML().includes('<ChecksumCRC64NVME>HyOpGHolkII=</ChecksumCRC64NVME>'));
74+
});
75+
76+
it('should wrap everything in a <Checksum> element', () => {
77+
const c = new ObjectMDChecksum('sha256', validDigest.sha256, 'FULL_OBJECT');
78+
const xml = c.toGetObjectAttributesXML();
79+
assert(xml.startsWith('<Checksum>'));
80+
assert(xml.endsWith('</Checksum>'));
81+
});
82+
83+
it('should append <ChecksumType>FULL_OBJECT</ChecksumType>', () => {
84+
const c = new ObjectMDChecksum('sha256', validDigest.sha256, 'FULL_OBJECT');
85+
assert(c.toGetObjectAttributesXML().includes('<ChecksumType>FULL_OBJECT</ChecksumType>'));
86+
});
87+
88+
it('should append <ChecksumType>COMPOSITE</ChecksumType> for composite type', () => {
89+
const c = new ObjectMDChecksum('crc32', validDigest.crc32, 'COMPOSITE');
90+
assert(c.toGetObjectAttributesXML().includes('<ChecksumType>COMPOSITE</ChecksumType>'));
91+
});
92+
});
93+
94+
describe('isValid', () => {
95+
it('should return null for a valid checksum object', () => {
96+
assert.strictEqual(ObjectMDChecksum.isValid({
97+
checksumAlgorithm: 'sha256',
98+
checksumValue: validDigest.sha256,
99+
checksumType: 'FULL_OBJECT',
100+
}), null);
101+
});
102+
103+
it('should return an error string for an invalid checksumAlgorithm', () => {
104+
const result = ObjectMDChecksum.isValid({
105+
// @ts-expect-error intentionally invalid
106+
checksumAlgorithm: 'sha999',
107+
checksumValue: validDigest.sha256,
108+
checksumType: 'FULL_OBJECT',
109+
});
110+
assert.match(result!, /invalid checksumAlgorithm/);
111+
});
112+
113+
it('should return an error string for an invalid checksumType', () => {
114+
const result = ObjectMDChecksum.isValid({
115+
checksumAlgorithm: 'sha256',
116+
checksumValue: validDigest.sha256,
117+
// @ts-expect-error intentionally invalid
118+
checksumType: 'WRONG',
119+
});
120+
assert.match(result!, /invalid checksumType/);
121+
});
122+
123+
it('should return an error string for an invalid checksumValue', () => {
124+
const result = ObjectMDChecksum.isValid({
125+
checksumAlgorithm: 'sha256',
126+
checksumValue: 'tooshort=',
127+
checksumType: 'FULL_OBJECT',
128+
});
129+
assert.match(result!, /invalid checksumValue/);
130+
});
131+
});
132+
});

0 commit comments

Comments
 (0)