file.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.TargetFile = exports.MetaFile = void 0;
  7. const crypto_1 = __importDefault(require("crypto"));
  8. const util_1 = __importDefault(require("util"));
  9. const error_1 = require("./error");
  10. const utils_1 = require("./utils");
  11. // A container with information about a particular metadata file.
  12. //
  13. // This class is used for Timestamp and Snapshot metadata.
  14. class MetaFile {
  15. constructor(opts) {
  16. if (opts.version <= 0) {
  17. throw new error_1.ValueError('Metafile version must be at least 1');
  18. }
  19. if (opts.length !== undefined) {
  20. validateLength(opts.length);
  21. }
  22. this.version = opts.version;
  23. this.length = opts.length;
  24. this.hashes = opts.hashes;
  25. this.unrecognizedFields = opts.unrecognizedFields || {};
  26. }
  27. equals(other) {
  28. if (!(other instanceof MetaFile)) {
  29. return false;
  30. }
  31. return (this.version === other.version &&
  32. this.length === other.length &&
  33. util_1.default.isDeepStrictEqual(this.hashes, other.hashes) &&
  34. util_1.default.isDeepStrictEqual(this.unrecognizedFields, other.unrecognizedFields));
  35. }
  36. verify(data) {
  37. // Verifies that the given data matches the expected length.
  38. if (this.length !== undefined) {
  39. if (data.length !== this.length) {
  40. throw new error_1.LengthOrHashMismatchError(`Expected length ${this.length} but got ${data.length}`);
  41. }
  42. }
  43. // Verifies that the given data matches the supplied hashes.
  44. if (this.hashes) {
  45. Object.entries(this.hashes).forEach(([key, value]) => {
  46. let hash;
  47. try {
  48. hash = crypto_1.default.createHash(key);
  49. }
  50. catch (e) {
  51. throw new error_1.LengthOrHashMismatchError(`Hash algorithm ${key} not supported`);
  52. }
  53. const observedHash = hash.update(data).digest('hex');
  54. if (observedHash !== value) {
  55. throw new error_1.LengthOrHashMismatchError(`Expected hash ${value} but got ${observedHash}`);
  56. }
  57. });
  58. }
  59. }
  60. toJSON() {
  61. const json = {
  62. version: this.version,
  63. ...this.unrecognizedFields,
  64. };
  65. if (this.length !== undefined) {
  66. json.length = this.length;
  67. }
  68. if (this.hashes) {
  69. json.hashes = this.hashes;
  70. }
  71. return json;
  72. }
  73. static fromJSON(data) {
  74. const { version, length, hashes, ...rest } = data;
  75. if (typeof version !== 'number') {
  76. throw new TypeError('version must be a number');
  77. }
  78. if (utils_1.guard.isDefined(length) && typeof length !== 'number') {
  79. throw new TypeError('length must be a number');
  80. }
  81. if (utils_1.guard.isDefined(hashes) && !utils_1.guard.isStringRecord(hashes)) {
  82. throw new TypeError('hashes must be string keys and values');
  83. }
  84. return new MetaFile({
  85. version,
  86. length,
  87. hashes,
  88. unrecognizedFields: rest,
  89. });
  90. }
  91. }
  92. exports.MetaFile = MetaFile;
  93. // Container for info about a particular target file.
  94. //
  95. // This class is used for Target metadata.
  96. class TargetFile {
  97. constructor(opts) {
  98. validateLength(opts.length);
  99. this.length = opts.length;
  100. this.path = opts.path;
  101. this.hashes = opts.hashes;
  102. this.unrecognizedFields = opts.unrecognizedFields || {};
  103. }
  104. get custom() {
  105. const custom = this.unrecognizedFields['custom'];
  106. if (!custom || Array.isArray(custom) || !(typeof custom === 'object')) {
  107. return {};
  108. }
  109. return custom;
  110. }
  111. equals(other) {
  112. if (!(other instanceof TargetFile)) {
  113. return false;
  114. }
  115. return (this.length === other.length &&
  116. this.path === other.path &&
  117. util_1.default.isDeepStrictEqual(this.hashes, other.hashes) &&
  118. util_1.default.isDeepStrictEqual(this.unrecognizedFields, other.unrecognizedFields));
  119. }
  120. async verify(stream) {
  121. let observedLength = 0;
  122. // Create a digest for each hash algorithm
  123. const digests = Object.keys(this.hashes).reduce((acc, key) => {
  124. try {
  125. acc[key] = crypto_1.default.createHash(key);
  126. }
  127. catch (e) {
  128. throw new error_1.LengthOrHashMismatchError(`Hash algorithm ${key} not supported`);
  129. }
  130. return acc;
  131. }, {});
  132. // Read stream chunk by chunk
  133. for await (const chunk of stream) {
  134. // Keep running tally of stream length
  135. observedLength += chunk.length;
  136. // Append chunk to each digest
  137. Object.values(digests).forEach((digest) => {
  138. digest.update(chunk);
  139. });
  140. }
  141. // Verify length matches expected value
  142. if (observedLength !== this.length) {
  143. throw new error_1.LengthOrHashMismatchError(`Expected length ${this.length} but got ${observedLength}`);
  144. }
  145. // Verify each digest matches expected value
  146. Object.entries(digests).forEach(([key, value]) => {
  147. const expected = this.hashes[key];
  148. const actual = value.digest('hex');
  149. if (actual !== expected) {
  150. throw new error_1.LengthOrHashMismatchError(`Expected hash ${expected} but got ${actual}`);
  151. }
  152. });
  153. }
  154. toJSON() {
  155. return {
  156. length: this.length,
  157. hashes: this.hashes,
  158. ...this.unrecognizedFields,
  159. };
  160. }
  161. static fromJSON(path, data) {
  162. const { length, hashes, ...rest } = data;
  163. if (typeof length !== 'number') {
  164. throw new TypeError('length must be a number');
  165. }
  166. if (!utils_1.guard.isStringRecord(hashes)) {
  167. throw new TypeError('hashes must have string keys and values');
  168. }
  169. return new TargetFile({
  170. length,
  171. path,
  172. hashes,
  173. unrecognizedFields: rest,
  174. });
  175. }
  176. }
  177. exports.TargetFile = TargetFile;
  178. // Check that supplied length if valid
  179. function validateLength(length) {
  180. if (length < 0) {
  181. throw new error_1.ValueError('Length must be at least 0');
  182. }
  183. }