@majikah/majik-signature 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,134 @@
1
1
  /**
2
- * handlers/fallback.ts — Tier-2 universal trailer handler
2
+ * handlers/fallback.ts — Tier-2 universal trailer handler.
3
3
  *
4
- * Works on ANY file format. Appends a self-describing binary trailer:
5
- * [original bytes][signature_json_utf8][8-byte length LE][8-byte magic]
4
+ * The universal fallback is the final embedding strategy used when no
5
+ * format-specific handler recognizes a file. It can therefore operate on
6
+ * arbitrary binary content without needing to understand the file format.
6
7
  *
7
- * Files remain valid for their primary use — most parsers ignore trailing bytes.
8
- * The magic bytes allow detection and clean stripping from any file.
8
+ * Trailer layout:
9
+ * [original bytes]
10
+ * [signature JSON encoded as UTF-8]
11
+ * [8-byte payload length, little-endian]
12
+ * [8-byte magic marker]
13
+ *
14
+ * @remarks
15
+ * The handler intentionally treats the signature payload as opaque JSON text.
16
+ * Format awareness, cryptographic signing, and signature verification remain
17
+ * outside this handler.
18
+ *
19
+ * The fallback is designed for formats whose parsers tolerate trailing bytes.
20
+ * That is a compatibility strategy, not a universal guarantee: some formats
21
+ * or downstream tools may reject, rewrite, truncate, or otherwise normalize
22
+ * trailing data. Prefer a native format handler when one is available and
23
+ * durable cross-tool interoperability matters.
24
+ *
25
+ * Embedding is idempotent. Before writing a new trailer, any existing
26
+ * universal trailer is removed so signatures do not accumulate back-to-back.
9
27
  */
10
28
  import { FormatHandler } from "../types";
29
+ /**
30
+ * Last-resort {@link FormatHandler} that embeds signatures using the universal
31
+ * binary trailer format.
32
+ *
33
+ * @remarks
34
+ * `FallbackHandler` is intended to be registered after format-specific
35
+ * handlers. Its {@link canHandle} method always returns `true`, making this
36
+ * handler the universal catch-all for otherwise unsupported file types.
37
+ *
38
+ * The handler does not perform cryptographic operations itself. It only:
39
+ * - appends a serialized signature payload,
40
+ * - extracts that payload when the trailer is present, and
41
+ * - removes the trailer to recover the original bytes.
42
+ *
43
+ * The trailer is self-describing through its terminating magic bytes and
44
+ * length field, allowing extraction and stripping without knowing the file's
45
+ * MIME type or format.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const handler = new FallbackHandler();
50
+ *
51
+ * const signed = await handler.embed(bytes, signatureJson);
52
+ * const extracted = await handler.extract(signed);
53
+ * const original = await handler.strip(signed);
54
+ * ```
55
+ *
56
+ * @see {@link FormatHandler}
57
+ */
11
58
  export declare class FallbackHandler implements FormatHandler {
59
+ /**
60
+ * Human-readable handler name used by the embedding pipeline and diagnostics.
61
+ */
12
62
  readonly name = "Fallback (Universal Trailer)";
63
+ /**
64
+ * MIME types accepted by this handler.
65
+ *
66
+ * @remarks
67
+ * Intentionally declares a catch-all rather than a concrete media
68
+ * type. Actual selection is still governed by handler ordering and
69
+ * `canHandle()`.
70
+ */
13
71
  readonly supportedMimeTypes: readonly ["*/*"];
72
+ /**
73
+ * Determine whether this handler can process the supplied content.
74
+ *
75
+ * @remarks
76
+ * This method always returns `true` because the universal trailer is the
77
+ * Tier-2 last-resort embedding mechanism. It should therefore remain the
78
+ * final handler in the handler-selection chain; placing it earlier would
79
+ * prevent more format-aware handlers from being selected.
80
+ *
81
+ * The byte content and MIME type are intentionally ignored.
82
+ *
83
+ * @param _bytes File bytes to inspect. Ignored by the fallback handler.
84
+ * @param _mimeType Optional MIME type hint. Ignored by the fallback handler.
85
+ * @returns Always `true`.
86
+ */
14
87
  canHandle(_bytes: Uint8Array, _mimeType?: string): boolean;
88
+ /**
89
+ * Embed a serialized signature payload using the universal trailer format.
90
+ *
91
+ * @remarks
92
+ * Any existing universal trailer is stripped before the new payload is
93
+ * appended. This makes repeated embedding idempotent: re-embedding replaces
94
+ * the existing fallback signature rather than stacking multiple trailers.
95
+ *
96
+ * The supplied `signatureJson` is treated as already-serialized signature
97
+ * data. This method does not parse, validate, or cryptographically verify
98
+ * it.
99
+ *
100
+ * @param bytes Original or previously signed file bytes.
101
+ * @param signatureJson Serialized signature envelope JSON to append.
102
+ * @returns The file bytes with exactly one universal signature trailer.
103
+ */
15
104
  embed(bytes: Uint8Array, signatureJson: string): Promise<Uint8Array>;
105
+ /**
106
+ * Extract the serialized signature payload from a universal trailer.
107
+ *
108
+ * @remarks
109
+ * Extraction is structural only. A non-null return value means a well-formed
110
+ * universal trailer was found; it does not mean that the contained signature
111
+ * is cryptographically valid or trusted.
112
+ *
113
+ * @param bytes File bytes that may contain a universal trailer.
114
+ * @returns The serialized signature JSON when a valid trailer is present;
115
+ * otherwise `null`.
116
+ */
16
117
  extract(bytes: Uint8Array): Promise<string | null>;
118
+ /**
119
+ * Remove the universal trailer and recover the original file bytes.
120
+ *
121
+ * @remarks
122
+ * If no recognizable universal trailer is present, the input bytes are
123
+ * returned unchanged.
124
+ *
125
+ * This operation only removes the trailer produced by the universal
126
+ * fallback format. It does not remove signatures embedded by native,
127
+ * format-specific handlers.
128
+ *
129
+ * @param bytes File bytes that may contain a universal trailer.
130
+ * @returns The original bytes when a trailer is found; otherwise the
131
+ * unchanged input bytes.
132
+ */
17
133
  strip(bytes: Uint8Array): Promise<Uint8Array>;
18
134
  }
@@ -1,29 +1,145 @@
1
1
  /**
2
- * handlers/fallback.ts — Tier-2 universal trailer handler
2
+ * handlers/fallback.ts — Tier-2 universal trailer handler.
3
3
  *
4
- * Works on ANY file format. Appends a self-describing binary trailer:
5
- * [original bytes][signature_json_utf8][8-byte length LE][8-byte magic]
4
+ * The universal fallback is the final embedding strategy used when no
5
+ * format-specific handler recognizes a file. It can therefore operate on
6
+ * arbitrary binary content without needing to understand the file format.
6
7
  *
7
- * Files remain valid for their primary use — most parsers ignore trailing bytes.
8
- * The magic bytes allow detection and clean stripping from any file.
8
+ * Trailer layout:
9
+ * [original bytes]
10
+ * [signature JSON encoded as UTF-8]
11
+ * [8-byte payload length, little-endian]
12
+ * [8-byte magic marker]
13
+ *
14
+ * @remarks
15
+ * The handler intentionally treats the signature payload as opaque JSON text.
16
+ * Format awareness, cryptographic signing, and signature verification remain
17
+ * outside this handler.
18
+ *
19
+ * The fallback is designed for formats whose parsers tolerate trailing bytes.
20
+ * That is a compatibility strategy, not a universal guarantee: some formats
21
+ * or downstream tools may reject, rewrite, truncate, or otherwise normalize
22
+ * trailing data. Prefer a native format handler when one is available and
23
+ * durable cross-tool interoperability matters.
24
+ *
25
+ * Embedding is idempotent. Before writing a new trailer, any existing
26
+ * universal trailer is removed so signatures do not accumulate back-to-back.
9
27
  */
10
28
  import { appendTrailer, extractTrailer } from "./utils";
29
+ /**
30
+ * Last-resort {@link FormatHandler} that embeds signatures using the universal
31
+ * binary trailer format.
32
+ *
33
+ * @remarks
34
+ * `FallbackHandler` is intended to be registered after format-specific
35
+ * handlers. Its {@link canHandle} method always returns `true`, making this
36
+ * handler the universal catch-all for otherwise unsupported file types.
37
+ *
38
+ * The handler does not perform cryptographic operations itself. It only:
39
+ * - appends a serialized signature payload,
40
+ * - extracts that payload when the trailer is present, and
41
+ * - removes the trailer to recover the original bytes.
42
+ *
43
+ * The trailer is self-describing through its terminating magic bytes and
44
+ * length field, allowing extraction and stripping without knowing the file's
45
+ * MIME type or format.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const handler = new FallbackHandler();
50
+ *
51
+ * const signed = await handler.embed(bytes, signatureJson);
52
+ * const extracted = await handler.extract(signed);
53
+ * const original = await handler.strip(signed);
54
+ * ```
55
+ *
56
+ * @see {@link FormatHandler}
57
+ */
11
58
  export class FallbackHandler {
59
+ /**
60
+ * Human-readable handler name used by the embedding pipeline and diagnostics.
61
+ */
12
62
  name = "Fallback (Universal Trailer)";
63
+ /**
64
+ * MIME types accepted by this handler.
65
+ *
66
+ * @remarks
67
+ * Intentionally declares a catch-all rather than a concrete media
68
+ * type. Actual selection is still governed by handler ordering and
69
+ * `canHandle()`.
70
+ */
13
71
  supportedMimeTypes = ["*/*"];
72
+ /**
73
+ * Determine whether this handler can process the supplied content.
74
+ *
75
+ * @remarks
76
+ * This method always returns `true` because the universal trailer is the
77
+ * Tier-2 last-resort embedding mechanism. It should therefore remain the
78
+ * final handler in the handler-selection chain; placing it earlier would
79
+ * prevent more format-aware handlers from being selected.
80
+ *
81
+ * The byte content and MIME type are intentionally ignored.
82
+ *
83
+ * @param _bytes File bytes to inspect. Ignored by the fallback handler.
84
+ * @param _mimeType Optional MIME type hint. Ignored by the fallback handler.
85
+ * @returns Always `true`.
86
+ */
14
87
  canHandle(_bytes, _mimeType) {
15
88
  // Always handles — it's the last resort
16
89
  return true;
17
90
  }
91
+ /**
92
+ * Embed a serialized signature payload using the universal trailer format.
93
+ *
94
+ * @remarks
95
+ * Any existing universal trailer is stripped before the new payload is
96
+ * appended. This makes repeated embedding idempotent: re-embedding replaces
97
+ * the existing fallback signature rather than stacking multiple trailers.
98
+ *
99
+ * The supplied `signatureJson` is treated as already-serialized signature
100
+ * data. This method does not parse, validate, or cryptographically verify
101
+ * it.
102
+ *
103
+ * @param bytes Original or previously signed file bytes.
104
+ * @param signatureJson Serialized signature envelope JSON to append.
105
+ * @returns The file bytes with exactly one universal signature trailer.
106
+ */
18
107
  async embed(bytes, signatureJson) {
19
108
  // Strip any existing trailer first (idempotent)
20
109
  const stripped = await this.strip(bytes);
21
110
  return appendTrailer(stripped, signatureJson);
22
111
  }
112
+ /**
113
+ * Extract the serialized signature payload from a universal trailer.
114
+ *
115
+ * @remarks
116
+ * Extraction is structural only. A non-null return value means a well-formed
117
+ * universal trailer was found; it does not mean that the contained signature
118
+ * is cryptographically valid or trusted.
119
+ *
120
+ * @param bytes File bytes that may contain a universal trailer.
121
+ * @returns The serialized signature JSON when a valid trailer is present;
122
+ * otherwise `null`.
123
+ */
23
124
  async extract(bytes) {
24
125
  const result = extractTrailer(bytes);
25
126
  return result ? result.signatureJson : null;
26
127
  }
128
+ /**
129
+ * Remove the universal trailer and recover the original file bytes.
130
+ *
131
+ * @remarks
132
+ * If no recognizable universal trailer is present, the input bytes are
133
+ * returned unchanged.
134
+ *
135
+ * This operation only removes the trailer produced by the universal
136
+ * fallback format. It does not remove signatures embedded by native,
137
+ * format-specific handlers.
138
+ *
139
+ * @param bytes File bytes that may contain a universal trailer.
140
+ * @returns The original bytes when a trailer is found; otherwise the
141
+ * unchanged input bytes.
142
+ */
27
143
  async strip(bytes) {
28
144
  const result = extractTrailer(bytes);
29
145
  return result ? result.original : bytes;
@@ -1,17 +1,13 @@
1
1
  /**
2
2
  * handlers/flac.ts — FLAC handler
3
3
  *
4
- * FLAC files start with "fLaC" and a series of metadata blocks before audio frames.
5
- * We embed in a VORBIS_COMMENT block (block type 4) using a standard key=value pair:
6
- * MAJIK-SIGNATURE=<json>
4
+ * FLAC files start with "fLaC" (4 bytes) followed by one or more metadata blocks.
5
+ * Each metadata block header (4 bytes):
6
+ * [1B: is_last (1 bit) + block_type (7 bits)]
7
+ * [3B: 24-bit big-endian length]
7
8
  *
8
- * FLAC metadata block header:
9
- * [1B: last-block flag (7) | block_type (7)][3B: block_length]
10
- *
11
- * VORBIS_COMMENT format (all LE):
12
- * [4B vendor_length][vendor_string]
13
- * [4B comment_count]
14
- * [4B comment_length][comment_utf8] × comment_count
9
+ * We embed signatures using an APPLICATION block (type = 2) with 4-byte
10
+ * application ID "MAJK" followed by the signature JSON payload.
15
11
  */
16
12
  import { FormatHandler } from "../../types";
17
13
  export declare class FlacHandler implements FormatHandler {
@@ -21,9 +17,7 @@ export declare class FlacHandler implements FormatHandler {
21
17
  embed(bytes: Uint8Array, signatureJson: string): Promise<Uint8Array>;
22
18
  extract(bytes: Uint8Array): Promise<string | null>;
23
19
  strip(bytes: Uint8Array): Promise<Uint8Array>;
24
- private _parseBlocks;
25
- private _serializeBlocks;
26
- private _buildVorbisComment;
27
- private _serializeVorbisComment;
28
- private _readVorbisComment;
20
+ private _parseMetadata;
21
+ private _buildMetadataBlock;
22
+ private _validateAudioStream;
29
23
  }
@@ -1,24 +1,27 @@
1
1
  /**
2
2
  * handlers/flac.ts — FLAC handler
3
3
  *
4
- * FLAC files start with "fLaC" and a series of metadata blocks before audio frames.
5
- * We embed in a VORBIS_COMMENT block (block type 4) using a standard key=value pair:
6
- * MAJIK-SIGNATURE=<json>
4
+ * FLAC files start with "fLaC" (4 bytes) followed by one or more metadata blocks.
5
+ * Each metadata block header (4 bytes):
6
+ * [1B: is_last (1 bit) + block_type (7 bits)]
7
+ * [3B: 24-bit big-endian length]
7
8
  *
8
- * FLAC metadata block header:
9
- * [1B: last-block flag (7) | block_type (7)][3B: block_length]
10
- *
11
- * VORBIS_COMMENT format (all LE):
12
- * [4B vendor_length][vendor_string]
13
- * [4B comment_count]
14
- * [4B comment_length][comment_utf8] × comment_count
9
+ * We embed signatures using an APPLICATION block (type = 2) with 4-byte
10
+ * application ID "MAJK" followed by the signature JSON payload.
15
11
  */
16
- import { concatBytes, readUint32LE, textDecode, textEncode, writeUint32LE, } from "../utils";
12
+ import { concatBytes, textDecode, textEncode } from "../utils";
17
13
  const FLAC_MAGIC = textEncode("fLaC");
18
- const BLOCK_TYPE_STREAMINFO = 0;
19
- const BLOCK_TYPE_VORBIS_COMMENT = 4;
20
- const VENDOR_STRING = "MajikSignatureEmbed/1.0";
21
- const COMMENT_KEY = "MAJIK-SIGNATURE";
14
+ const APPLICATION_ID = textEncode("MAJK"); // 4-byte custom Application ID
15
+ var FlacBlockType;
16
+ (function (FlacBlockType) {
17
+ FlacBlockType[FlacBlockType["STREAMINFO"] = 0] = "STREAMINFO";
18
+ FlacBlockType[FlacBlockType["PADDING"] = 1] = "PADDING";
19
+ FlacBlockType[FlacBlockType["APPLICATION"] = 2] = "APPLICATION";
20
+ FlacBlockType[FlacBlockType["SEEKTABLE"] = 3] = "SEEKTABLE";
21
+ FlacBlockType[FlacBlockType["VORBIS_COMMENT"] = 4] = "VORBIS_COMMENT";
22
+ FlacBlockType[FlacBlockType["CUESHEET"] = 5] = "CUESHEET";
23
+ FlacBlockType[FlacBlockType["PICTURE"] = 6] = "PICTURE";
24
+ })(FlacBlockType || (FlacBlockType = {}));
22
25
  export class FlacHandler {
23
26
  name = "FLAC";
24
27
  supportedMimeTypes = ["audio/flac", "audio/x-flac"];
@@ -27,40 +30,44 @@ export class FlacHandler {
27
30
  return true;
28
31
  if (bytes.length < 4)
29
32
  return false;
30
- return textDecode(bytes.slice(0, 4)) === "fLaC";
33
+ return (bytes[0] === 0x66 && // 'f'
34
+ bytes[1] === 0x4c && // 'L'
35
+ bytes[2] === 0x61 && // 'a'
36
+ bytes[3] === 0x43 // 'C'
37
+ );
31
38
  }
32
39
  async embed(bytes, signatureJson) {
33
40
  const clean = await this.strip(bytes);
34
- const blocks = this._parseBlocks(clean);
35
- // Build a new VORBIS_COMMENT block with our signature
36
- const commentBlockData = this._buildVorbisComment(signatureJson);
37
- // Find existing VORBIS_COMMENT block index, or insert after STREAMINFO
38
- const vcIdx = blocks.findIndex((b) => b.type === BLOCK_TYPE_VORBIS_COMMENT);
39
- const insertAt = vcIdx >= 0 ? vcIdx : 1; // after STREAMINFO
40
- const newBlocks = [...blocks];
41
- const newBlock = {
42
- type: BLOCK_TYPE_VORBIS_COMMENT,
43
- data: commentBlockData,
44
- };
45
- if (vcIdx >= 0) {
46
- newBlocks[vcIdx] = newBlock;
47
- }
48
- else {
49
- newBlocks.splice(insertAt, 0, newBlock);
50
- }
51
- return this._serializeBlocks(newBlocks, clean);
41
+ const { blocks, audioData } = this._parseMetadata(clean);
42
+ // Build signature APPLICATION payload: [4B App ID "MAJK"][signature JSON]
43
+ const appPayload = concatBytes(APPLICATION_ID, textEncode(signatureJson));
44
+ // Safely clear the isLast flag bit on all existing metadata blocks
45
+ const updatedBlocks = blocks.map((b) => {
46
+ const newBlock = new Uint8Array(b.raw);
47
+ newBlock[0] &= 0x7f; // Clear bit 7 (isLast)
48
+ return newBlock;
49
+ });
50
+ // Append our new APPLICATION metadata block marked as isLast = true
51
+ const sigBlock = this._buildMetadataBlock(FlacBlockType.APPLICATION, appPayload, true);
52
+ return concatBytes(FLAC_MAGIC, ...updatedBlocks, sigBlock, audioData);
52
53
  }
53
54
  async extract(bytes) {
54
55
  if (!this.canHandle(bytes))
55
56
  return null;
56
57
  try {
57
- const blocks = this._parseBlocks(bytes);
58
+ const { blocks, audioData, endOffset } = this._parseMetadata(bytes);
59
+ // Verify overall stream integrity and ensure no trailing garbage was appended
60
+ if (!this._validateAudioStream(audioData, bytes.length - endOffset)) {
61
+ return null;
62
+ }
58
63
  for (const block of blocks) {
59
- if (block.type === BLOCK_TYPE_VORBIS_COMMENT) {
60
- const comments = this._readVorbisComment(block.data);
61
- for (const comment of comments) {
62
- if (comment.key === COMMENT_KEY)
63
- return comment.value;
64
+ if (block.type === FlacBlockType.APPLICATION &&
65
+ block.data.length >= 4) {
66
+ const appId = textDecode(block.data.slice(0, 4));
67
+ // Inside FlacHandler.extract()
68
+ if (appId === "MAJK") {
69
+ const jsonStr = textDecode(block.data.slice(4));
70
+ return jsonStr.replace(/\0+$/, ""); // Strip trailing null padding if present
64
71
  }
65
72
  }
66
73
  }
@@ -74,114 +81,91 @@ export class FlacHandler {
74
81
  if (!this.canHandle(bytes))
75
82
  return bytes;
76
83
  try {
77
- const blocks = this._parseBlocks(bytes);
78
- const filtered = blocks.map((b) => {
79
- if (b.type !== BLOCK_TYPE_VORBIS_COMMENT)
80
- return b;
81
- // Rebuild without our comment
82
- const comments = this._readVorbisComment(b.data);
83
- const kept = comments.filter((c) => c.key !== COMMENT_KEY);
84
- return {
85
- type: BLOCK_TYPE_VORBIS_COMMENT,
86
- data: this._serializeVorbisComment(kept),
87
- };
84
+ const { blocks, audioData } = this._parseMetadata(bytes);
85
+ // Filter out MAJK application metadata blocks
86
+ const kept = blocks.filter((b) => {
87
+ if (b.type !== FlacBlockType.APPLICATION)
88
+ return true;
89
+ if (b.data.length < 4)
90
+ return true;
91
+ const appId = textDecode(b.data.slice(0, 4));
92
+ return appId !== "MAJK";
93
+ });
94
+ // Return original buffer unmodified if no signature block was found
95
+ if (kept.length === blocks.length) {
96
+ return bytes;
97
+ }
98
+ // Reconstruct metadata blocks and restore is_last bit on the final block
99
+ const rebuiltBlocks = kept.map((b, index) => {
100
+ const isLast = index === kept.length - 1;
101
+ const header = new Uint8Array(b.raw.slice(0, 4));
102
+ if (isLast) {
103
+ header[0] |= 0x80; // Set bit 7 to 1
104
+ }
105
+ else {
106
+ header[0] &= 0x7f; // Clear bit 7 to 0
107
+ }
108
+ return concatBytes(header, b.data);
88
109
  });
89
- return this._serializeBlocks(filtered, bytes);
110
+ return concatBytes(FLAC_MAGIC, ...rebuiltBlocks, audioData);
90
111
  }
91
112
  catch {
92
113
  return bytes;
93
114
  }
94
115
  }
95
- // ─── Block Parser ────────────────────────────────────────────────────────────
96
- _parseBlocks(bytes) {
116
+ // ─── Helpers ─────────────────────────────────────────────────────────────
117
+ _parseMetadata(bytes) {
118
+ if (bytes.length < 4 || textDecode(bytes.slice(0, 4)) !== "fLaC") {
119
+ throw new Error("Invalid FLAC magic header");
120
+ }
97
121
  const blocks = [];
98
- let offset = 4; // skip "fLaC"
99
- while (offset + 4 <= bytes.length) {
100
- const header = bytes[offset];
101
- const isLast = (header & 0x80) !== 0;
102
- const blockType = header & 0x7f;
122
+ let offset = 4;
123
+ while (offset < bytes.length) {
124
+ if (offset + 4 > bytes.length) {
125
+ throw new Error("Truncated metadata block header");
126
+ }
127
+ const headerByte = bytes[offset];
128
+ const isLast = (headerByte & 0x80) !== 0;
129
+ const type = headerByte & 0x7f;
130
+ // 24-bit big-endian integer reading
103
131
  const length = (bytes[offset + 1] << 16) |
104
132
  (bytes[offset + 2] << 8) |
105
133
  bytes[offset + 3];
134
+ if (offset + 4 + length > bytes.length) {
135
+ throw new Error("Truncated metadata block payload");
136
+ }
106
137
  const data = bytes.slice(offset + 4, offset + 4 + length);
107
- blocks.push({ type: blockType, data });
138
+ const raw = bytes.slice(offset, offset + 4 + length);
139
+ blocks.push({ isLast, type, data, raw });
108
140
  offset += 4 + length;
109
141
  if (isLast)
110
142
  break;
111
143
  }
112
- return blocks;
113
- }
114
- _serializeBlocks(blocks, originalBytes) {
115
- // Find where audio data starts in original
116
- let audioOffset = 4;
117
- let offset = 4;
118
- while (offset + 4 <= originalBytes.length) {
119
- const header = originalBytes[offset];
120
- const isLast = (header & 0x80) !== 0;
121
- const length = (originalBytes[offset + 1] << 16) |
122
- (originalBytes[offset + 2] << 8) |
123
- originalBytes[offset + 3];
124
- audioOffset = offset + 4 + length;
125
- offset = audioOffset;
126
- if (isLast)
127
- break;
128
- }
129
- const audioData = originalBytes.slice(audioOffset);
130
- const serialized = [FLAC_MAGIC];
131
- for (let i = 0; i < blocks.length; i++) {
132
- const isLast = i === blocks.length - 1;
133
- const { type, data } = blocks[i];
134
- const header = (isLast ? 0x80 : 0x00) | (type & 0x7f);
135
- const length = data.length;
136
- const headerBytes = new Uint8Array([
137
- header,
138
- (length >> 16) & 0xff,
139
- (length >> 8) & 0xff,
140
- length & 0xff,
141
- ]);
142
- serialized.push(concatBytes(headerBytes, data));
143
- }
144
- serialized.push(audioData);
145
- return concatBytes(...serialized);
146
- }
147
- // ─── Vorbis Comment Helpers ──────────────────────────────────────────────────
148
- _buildVorbisComment(signatureJson) {
149
- const comments = [{ key: COMMENT_KEY, value: signatureJson }];
150
- return this._serializeVorbisComment(comments);
144
+ const audioData = bytes.slice(offset);
145
+ return { blocks, audioData, endOffset: offset };
151
146
  }
152
- _serializeVorbisComment(comments) {
153
- const vendor = textEncode(VENDOR_STRING);
154
- const vendorLength = writeUint32LE(vendor.length);
155
- const commentCount = writeUint32LE(comments.length);
156
- const commentEntries = comments.map(({ key, value }) => {
157
- const entry = textEncode(`${key}=${value}`);
158
- return concatBytes(writeUint32LE(entry.length), entry);
159
- });
160
- return concatBytes(vendorLength, vendor, commentCount, ...commentEntries);
147
+ _buildMetadataBlock(type, data, isLast) {
148
+ const header = new Uint8Array(4);
149
+ // Byte 0: [is_last (1 bit)][block_type (7 bits)]
150
+ header[0] = (isLast ? 0x80 : 0x00) | (type & 0x7f);
151
+ // Bytes 1–3: 24-bit big-endian payload size
152
+ header[1] = (data.length >> 16) & 0xff;
153
+ header[2] = (data.length >> 8) & 0xff;
154
+ header[3] = data.length & 0xff;
155
+ return concatBytes(header, data);
161
156
  }
162
- _readVorbisComment(data) {
163
- const comments = [];
164
- let offset = 0;
165
- const vendorLength = readUint32LE(data, offset);
166
- offset += 4 + vendorLength;
167
- const count = readUint32LE(data, offset);
168
- offset += 4;
169
- for (let i = 0; i < count; i++) {
170
- if (offset + 4 > data.length)
171
- break;
172
- const length = readUint32LE(data, offset);
173
- offset += 4;
174
- const comment = textDecode(data.slice(offset, offset + length));
175
- offset += length;
176
- const eqIdx = comment.indexOf("=");
177
- if (eqIdx >= 0) {
178
- comments.push({
179
- key: comment.slice(0, eqIdx).toUpperCase(),
180
- value: comment.slice(eqIdx + 1),
181
- });
157
+ _validateAudioStream(audioData, expectedAudioBytes) {
158
+ if (audioData.length !== expectedAudioBytes)
159
+ return false;
160
+ if (audioData.length === 0)
161
+ return true;
162
+ // Validate initial audio frame sync code (14 bits: 11111111 111110xx)
163
+ if (audioData.length >= 2) {
164
+ if (audioData[0] !== 0xff || (audioData[1] & 0xfc) !== 0xf8) {
165
+ return false;
182
166
  }
183
167
  }
184
- return comments;
168
+ return true;
185
169
  }
186
170
  }
187
171
  // Freeze static methods
@@ -0,0 +1,10 @@
1
+ export { PdfHandler } from "./pdf";
2
+ export { PngHandler } from "./png";
3
+ export { JpegHandler } from "./jpeg";
4
+ export { WavHandler } from "./wav";
5
+ export { Mp3Handler } from "./mp3";
6
+ export { Mp4Handler } from "./mp4";
7
+ export { FlacHandler } from "./flac";
8
+ export { MkvHandler } from "./mkv";
9
+ export { OfficeHandler } from "./office";
10
+ export { TextHandler } from "./text";
@@ -0,0 +1,10 @@
1
+ export { PdfHandler } from "./pdf";
2
+ export { PngHandler } from "./png";
3
+ export { JpegHandler } from "./jpeg";
4
+ export { WavHandler } from "./wav";
5
+ export { Mp3Handler } from "./mp3";
6
+ export { Mp4Handler } from "./mp4";
7
+ export { FlacHandler } from "./flac";
8
+ export { MkvHandler } from "./mkv";
9
+ export { OfficeHandler } from "./office";
10
+ export { TextHandler } from "./text";