bun-torrent 0.0.1-beta

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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/package.json +49 -0
  4. package/src/app.ts +65 -0
  5. package/src/client.error.ts +12 -0
  6. package/src/client.test.ts +117 -0
  7. package/src/client.ts +185 -0
  8. package/src/index.test.ts +7 -0
  9. package/src/index.ts +76 -0
  10. package/src/peer/__tests__/peer-id.test.ts +24 -0
  11. package/src/peer/availability.test.ts +50 -0
  12. package/src/peer/availability.ts +88 -0
  13. package/src/peer/consts.ts +11 -0
  14. package/src/peer/handshake/handshake.error.ts +15 -0
  15. package/src/peer/handshake/handshake.test.ts +125 -0
  16. package/src/peer/handshake/index.ts +83 -0
  17. package/src/peer/index.ts +31 -0
  18. package/src/peer/messages/error.ts +16 -0
  19. package/src/peer/messages/helpers.ts +36 -0
  20. package/src/peer/messages/index.ts +174 -0
  21. package/src/peer/messages/messages.test.ts +231 -0
  22. package/src/peer/messages/types.ts +76 -0
  23. package/src/peer/peer-id.ts +9 -0
  24. package/src/peer/pool/index.ts +301 -0
  25. package/src/peer/pool/pool.error.ts +17 -0
  26. package/src/peer/pool/pool.test.ts +305 -0
  27. package/src/peer/session/index.ts +276 -0
  28. package/src/peer/session/session.error.ts +19 -0
  29. package/src/peer/session/session.test.ts +110 -0
  30. package/src/peer/types.ts +6 -0
  31. package/src/torrent/bencode/__tests__/decoder.test.ts +212 -0
  32. package/src/torrent/bencode/__tests__/encoder.test.ts +138 -0
  33. package/src/torrent/bencode/__tests__/encoder.unit.test.ts +24 -0
  34. package/src/torrent/bencode/__tests__/integration.test.ts +64 -0
  35. package/src/torrent/bencode/decoder.error.ts +36 -0
  36. package/src/torrent/bencode/decoder.ts +180 -0
  37. package/src/torrent/bencode/encoder.error.ts +14 -0
  38. package/src/torrent/bencode/encoder.ts +86 -0
  39. package/src/torrent/bencode/index.ts +9 -0
  40. package/src/torrent/bencode/types.ts +15 -0
  41. package/src/torrent/bencode/utils.ts +17 -0
  42. package/src/torrent/download/index.ts +9 -0
  43. package/src/torrent/download/manager.test.ts +393 -0
  44. package/src/torrent/download/manager.ts +376 -0
  45. package/src/torrent/file-selection.ts +51 -0
  46. package/src/torrent/index.ts +61 -0
  47. package/src/torrent/parser/helpers.ts +44 -0
  48. package/src/torrent/parser/index.ts +197 -0
  49. package/src/torrent/parser/info-hash.test.ts +39 -0
  50. package/src/torrent/parser/info-hash.ts +7 -0
  51. package/src/torrent/parser/parser.error.ts +21 -0
  52. package/src/torrent/parser/parser.test.ts +286 -0
  53. package/src/torrent/pieces/DefaultPlanner.ts +257 -0
  54. package/src/torrent/pieces/index.ts +20 -0
  55. package/src/torrent/pieces/planner.error.ts +18 -0
  56. package/src/torrent/pieces/planner.test.ts +303 -0
  57. package/src/torrent/pieces/planner.ts +16 -0
  58. package/src/torrent/pieces/types.ts +59 -0
  59. package/src/torrent/pieces/utils.ts +85 -0
  60. package/src/torrent/pieces/validation.test.ts +32 -0
  61. package/src/torrent/pieces/validation.ts +27 -0
  62. package/src/torrent/session/index.ts +195 -0
  63. package/src/torrent/session/session.test.ts +279 -0
  64. package/src/torrent/storage/index.ts +161 -0
  65. package/src/torrent/storage/storage.error.ts +18 -0
  66. package/src/torrent/storage/storage.test.ts +326 -0
  67. package/src/torrent/storage/types.ts +13 -0
  68. package/src/torrent/types.ts +16 -0
  69. package/src/tracker/http.test.ts +66 -0
  70. package/src/tracker/http.ts +137 -0
  71. package/src/tracker/index.ts +93 -0
  72. package/src/tracker/tracker.error.ts +26 -0
  73. package/src/tracker/types.ts +17 -0
  74. package/src/tracker/udp.test.ts +155 -0
  75. package/src/tracker/udp.ts +234 -0
  76. package/src/utils/__tests__/sha1.test.ts +16 -0
  77. package/src/utils/buffers.ts +28 -0
  78. package/src/utils/errors.ts +9 -0
  79. package/src/utils/formats.ts +8 -0
  80. package/src/utils/sha1.ts +4 -0
@@ -0,0 +1,88 @@
1
+ export type PeerPieceAvailabilityOptions = {
2
+ bitfield?: Uint8Array;
3
+ };
4
+
5
+ /**
6
+ * Mutable view of which torrent pieces a peer claims to have.
7
+ */
8
+ export class PeerPieceAvailability {
9
+ private readonly pieces: boolean[];
10
+
11
+ /**
12
+ * Create availability state from an optional peer bitfield.
13
+ *
14
+ * @param totalPieces - Number of pieces in the torrent.
15
+ * @param options - Optional initial bitfield from the peer.
16
+ */
17
+ public constructor(
18
+ public readonly totalPieces: number,
19
+ options: PeerPieceAvailabilityOptions = {},
20
+ ) {
21
+ this.pieces = Array.from({ length: totalPieces }, (_, pieceIndex) =>
22
+ options.bitfield ? hasBitfieldPiece(options.bitfield, pieceIndex) : false,
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Return true when the peer claims to have a piece.
28
+ *
29
+ * @param pieceIndex - Zero-based piece index.
30
+ * @returns Whether the peer has the piece. Invalid indexes return false.
31
+ */
32
+ public hasPiece(pieceIndex: number): boolean {
33
+ return this.pieces[pieceIndex] ?? false;
34
+ }
35
+
36
+ /**
37
+ * Mark one piece as available after receiving a peer `have` message.
38
+ *
39
+ * @param pieceIndex - Zero-based piece index.
40
+ */
41
+ public markHave(pieceIndex: number): void {
42
+ if (!Number.isInteger(pieceIndex) || pieceIndex < 0 || pieceIndex >= this.totalPieces) {
43
+ return;
44
+ }
45
+
46
+ this.pieces[pieceIndex] = true;
47
+ }
48
+
49
+ /**
50
+ * Replace availability from a peer `bitfield` message.
51
+ *
52
+ * @param bitfield - Raw bitfield payload from the peer.
53
+ */
54
+ public setBitfield(bitfield: Uint8Array): void {
55
+ for (let pieceIndex = 0; pieceIndex < this.totalPieces; pieceIndex += 1) {
56
+ this.pieces[pieceIndex] = hasBitfieldPiece(bitfield, pieceIndex);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Return available piece indexes in ascending order.
62
+ *
63
+ * @returns Piece indexes currently marked as available.
64
+ */
65
+ public toPieceIndexes(): number[] {
66
+ return this.pieces.flatMap((available, pieceIndex) => (available ? [pieceIndex] : []));
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Create mutable availability state from an optional peer bitfield.
72
+ *
73
+ * @param totalPieces - Number of pieces in the torrent.
74
+ * @param bitfield - Optional raw bitfield payload from the peer.
75
+ * @returns Peer piece availability state.
76
+ */
77
+ export const createPeerPieceAvailability = (
78
+ totalPieces: number,
79
+ bitfield?: Uint8Array,
80
+ ): PeerPieceAvailability => new PeerPieceAvailability(totalPieces, { bitfield });
81
+
82
+ const hasBitfieldPiece = (bitfield: Uint8Array, pieceIndex: number): boolean => {
83
+ const byte = bitfield[Math.floor(pieceIndex / 8)];
84
+ if (byte === undefined) return false;
85
+
86
+ const mask = 0x80 >> (pieceIndex % 8);
87
+ return (byte & mask) !== 0;
88
+ };
@@ -0,0 +1,11 @@
1
+ export const PROTOCOL = 'BitTorrent protocol';
2
+ export const PROTOCOL_BYTES = new TextEncoder().encode(PROTOCOL);
3
+
4
+ export const PEER_ID_PREFIX = '-BT0001-';
5
+
6
+ export const HANDSHAKE_LENGTH = 68;
7
+ export const RESERVED_LENGTH = 8;
8
+ export const INFO_HASH_LENGTH = 20;
9
+ export const PEER_ID_LENGTH = 20;
10
+
11
+ export const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
@@ -0,0 +1,15 @@
1
+ import { BunTorrentError } from '@utils/errors';
2
+
3
+ export enum HandshakeErrorCode {
4
+ INFOHASH_INVALID_LENGTH = 'HANDSHK_INFOHASH_INVALID_LENGTH',
5
+ INVALID_LENGTH = 'HANDSHK_INVALID_LENGTH',
6
+ INVALID_PROTOCOL = 'HANDSHK_INVALID_PROTOCOL',
7
+ PEERID_INVALID_LENGTH = 'HANDSHK_PEERID_INVALID_LENGTH',
8
+ RESERVED_INVALID_LENGTH = 'HANDSHK_RESERVED_INVALID_LENGTH',
9
+ }
10
+
11
+ export class PeerHandshakeError extends BunTorrentError {
12
+ constructor(code: HandshakeErrorCode, message: string) {
13
+ super(message, code);
14
+ }
15
+ }
@@ -0,0 +1,125 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { BunTorrentError } from '@utils/errors';
4
+ import {
5
+ HANDSHAKE_LENGTH,
6
+ INFO_HASH_LENGTH,
7
+ PEER_ID_LENGTH,
8
+ PROTOCOL,
9
+ PROTOCOL_BYTES,
10
+ RESERVED_LENGTH,
11
+ } from '../consts';
12
+ import { decodeHandshake, encodeHandshake } from '.';
13
+ import { HandshakeErrorCode, PeerHandshakeError } from '../handshake/handshake.error';
14
+
15
+ const makeBytes = (length: number, start = 0): Uint8Array => {
16
+ const bytes = new Uint8Array(length);
17
+
18
+ for (let i = 0; i < length; i++) {
19
+ bytes[i] = start + i;
20
+ }
21
+
22
+ return bytes;
23
+ };
24
+
25
+ const expectHandshakeError = (callback: () => unknown, code: HandshakeErrorCode): void => {
26
+ try {
27
+ callback();
28
+ throw new Error('Expected handshake operation to throw');
29
+ } catch (error) {
30
+ expect(error).toBeInstanceOf(PeerHandshakeError);
31
+ expect(error).toBeInstanceOf(BunTorrentError);
32
+ expect((error as PeerHandshakeError).code).toBe(code);
33
+ }
34
+ };
35
+
36
+ describe('encodeHandshake', () => {
37
+ test('encodes a 68-byte BitTorrent handshake', () => {
38
+ const infoHash = makeBytes(INFO_HASH_LENGTH, 1);
39
+ const peerId = makeBytes(PEER_ID_LENGTH, 101);
40
+ const handshake = encodeHandshake({ infoHash, peerId });
41
+
42
+ expect(handshake.byteLength).toBe(HANDSHAKE_LENGTH);
43
+ expect(handshake[0]).toBe(PROTOCOL_BYTES.byteLength);
44
+ expect([...handshake.subarray(1, 20)]).toEqual([...PROTOCOL_BYTES]);
45
+ expect([...handshake.subarray(20, 28)]).toEqual([...new Uint8Array(RESERVED_LENGTH)]);
46
+ expect([...handshake.subarray(28, 48)]).toEqual([...infoHash]);
47
+ expect([...handshake.subarray(48, 68)]).toEqual([...peerId]);
48
+ });
49
+
50
+ test('encodes custom reserved bytes', () => {
51
+ const reserved = makeBytes(RESERVED_LENGTH, 201);
52
+ const handshake = encodeHandshake({
53
+ infoHash: makeBytes(INFO_HASH_LENGTH, 1),
54
+ peerId: makeBytes(PEER_ID_LENGTH, 101),
55
+ reserved,
56
+ });
57
+
58
+ expect([...handshake.subarray(20, 28)]).toEqual([...reserved]);
59
+ });
60
+
61
+ test('rejects invalid info hash length', () => {
62
+ expectHandshakeError(
63
+ () =>
64
+ encodeHandshake({
65
+ infoHash: new Uint8Array(19),
66
+ peerId: makeBytes(PEER_ID_LENGTH),
67
+ }),
68
+ HandshakeErrorCode.INFOHASH_INVALID_LENGTH,
69
+ );
70
+ });
71
+
72
+ test('rejects invalid peer id length', () => {
73
+ expectHandshakeError(
74
+ () =>
75
+ encodeHandshake({
76
+ infoHash: makeBytes(INFO_HASH_LENGTH),
77
+ peerId: new Uint8Array(19),
78
+ }),
79
+ HandshakeErrorCode.PEERID_INVALID_LENGTH,
80
+ );
81
+ });
82
+
83
+ test('rejects invalid reserved length', () => {
84
+ expectHandshakeError(
85
+ () =>
86
+ encodeHandshake({
87
+ infoHash: makeBytes(INFO_HASH_LENGTH),
88
+ peerId: makeBytes(PEER_ID_LENGTH),
89
+ reserved: new Uint8Array(7),
90
+ }),
91
+ HandshakeErrorCode.RESERVED_INVALID_LENGTH,
92
+ );
93
+ });
94
+ });
95
+
96
+ describe('decodeHandshake', () => {
97
+ test('decodes an encoded handshake', () => {
98
+ const infoHash = makeBytes(INFO_HASH_LENGTH, 1);
99
+ const peerId = makeBytes(PEER_ID_LENGTH, 101);
100
+ const reserved = makeBytes(RESERVED_LENGTH, 201);
101
+ const decoded = decodeHandshake(encodeHandshake({ infoHash, peerId, reserved }));
102
+
103
+ expect(decoded.protocol).toBe(PROTOCOL);
104
+ expect([...decoded.reserved]).toEqual([...reserved]);
105
+ expect([...decoded.infoHash]).toEqual([...infoHash]);
106
+ expect([...decoded.peerId]).toEqual([...peerId]);
107
+ });
108
+
109
+ test('rejects invalid handshake length', () => {
110
+ expectHandshakeError(
111
+ () => decodeHandshake(new Uint8Array(HANDSHAKE_LENGTH - 1)),
112
+ HandshakeErrorCode.INVALID_LENGTH,
113
+ );
114
+ });
115
+
116
+ test('rejects invalid protocol', () => {
117
+ const handshake = encodeHandshake({
118
+ infoHash: makeBytes(INFO_HASH_LENGTH),
119
+ peerId: makeBytes(PEER_ID_LENGTH),
120
+ });
121
+ handshake[1] = 0x78;
122
+
123
+ expectHandshakeError(() => decodeHandshake(handshake), HandshakeErrorCode.INVALID_PROTOCOL);
124
+ });
125
+ });
@@ -0,0 +1,83 @@
1
+ import {
2
+ HANDSHAKE_LENGTH,
3
+ INFO_HASH_LENGTH,
4
+ PEER_ID_LENGTH,
5
+ PROTOCOL,
6
+ PROTOCOL_BYTES,
7
+ RESERVED_LENGTH,
8
+ } from '../consts';
9
+ import { HandshakeErrorCode, PeerHandshakeError } from './handshake.error';
10
+ import type { PeerHandshake } from '../types';
11
+
12
+ const textDecoder = new TextDecoder('utf-8', { fatal: true });
13
+
14
+ export const encodeHandshake = (input: {
15
+ infoHash: Uint8Array;
16
+ peerId: Uint8Array;
17
+ reserved?: Uint8Array;
18
+ }): Uint8Array => {
19
+ if (input.infoHash.byteLength !== INFO_HASH_LENGTH)
20
+ return fail(
21
+ HandshakeErrorCode.INFOHASH_INVALID_LENGTH,
22
+ 'Torrent infohash is invalid length',
23
+ );
24
+ if (input.peerId.byteLength !== PEER_ID_LENGTH)
25
+ return fail(HandshakeErrorCode.PEERID_INVALID_LENGTH, 'Torrent peerId is invalid length');
26
+
27
+ let offset = 0;
28
+ const handshake = new Uint8Array(HANDSHAKE_LENGTH);
29
+
30
+ handshake[offset++] = PROTOCOL_BYTES.byteLength;
31
+ handshake.set(PROTOCOL_BYTES, offset);
32
+ offset += PROTOCOL_BYTES.byteLength;
33
+
34
+ const reserved = input.reserved ?? new Uint8Array(RESERVED_LENGTH);
35
+ if (reserved.byteLength !== RESERVED_LENGTH)
36
+ return fail(
37
+ HandshakeErrorCode.RESERVED_INVALID_LENGTH,
38
+ 'Handshake reserved is invalid length',
39
+ );
40
+
41
+ handshake.set(reserved, offset);
42
+ offset += RESERVED_LENGTH;
43
+
44
+ handshake.set(input.infoHash, offset);
45
+ offset += INFO_HASH_LENGTH;
46
+
47
+ handshake.set(input.peerId, offset);
48
+
49
+ return handshake;
50
+ };
51
+
52
+ export const decodeHandshake = (handshake: Uint8Array): PeerHandshake => {
53
+ if (handshake.byteLength !== HANDSHAKE_LENGTH)
54
+ return fail(HandshakeErrorCode.INVALID_LENGTH, 'Handshake is invalid length');
55
+
56
+ let offset = 0;
57
+ const protocolLength = handshake[offset++]!;
58
+ const protocolEnd = offset + protocolLength;
59
+ const protocol = textDecoder.decode(handshake.subarray(offset, protocolEnd));
60
+ offset = protocolEnd;
61
+
62
+ if (protocol !== PROTOCOL)
63
+ return fail(HandshakeErrorCode.INVALID_PROTOCOL, 'Handshake protocol is invalid');
64
+
65
+ const reserved = handshake.subarray(offset, offset + RESERVED_LENGTH);
66
+ offset += RESERVED_LENGTH;
67
+
68
+ const infoHash = handshake.subarray(offset, offset + INFO_HASH_LENGTH);
69
+ offset += INFO_HASH_LENGTH;
70
+
71
+ const peerId = handshake.subarray(offset, offset + PEER_ID_LENGTH);
72
+
73
+ return {
74
+ protocol,
75
+ reserved,
76
+ infoHash,
77
+ peerId,
78
+ };
79
+ };
80
+
81
+ const fail = (code: HandshakeErrorCode, message: string): never => {
82
+ throw new PeerHandshakeError(code, message);
83
+ };
@@ -0,0 +1,31 @@
1
+ export { createPeerId } from './peer-id';
2
+ export { createPeerPieceAvailability, PeerPieceAvailability } from './availability';
3
+ export { PeerSession, PeerSessionError, PeerSessionErrorCode } from './session';
4
+ export { encodeHandshake, decodeHandshake } from './handshake';
5
+ export { HandshakeErrorCode, PeerHandshakeError } from './handshake/handshake.error';
6
+ export {
7
+ decodePeerMessage,
8
+ encodePeerMessage,
9
+ PeerMessageError,
10
+ PeerMessageErrorCode,
11
+ PeerMessageId,
12
+ } from './messages';
13
+ export { connectToPeers, openPeerPool, PeerPool, PeerPoolError, PeerPoolErrorCode } from './pool';
14
+
15
+ export type { PeerHandshake } from './types';
16
+ export type { PeerPieceAvailabilityOptions } from './availability';
17
+ export type { PeerSessionConnectOptions } from './session';
18
+ export type { PeerConnectionSession, PeerPoolOptions } from './pool';
19
+ export type {
20
+ BitfieldMessage,
21
+ CancelMessage,
22
+ ChokeMessage,
23
+ HaveMessage,
24
+ InterestedMessage,
25
+ KeepAliveMessage,
26
+ NotInterestedMessage,
27
+ PeerMessage,
28
+ PieceMessage,
29
+ RequestMessage,
30
+ UnchokeMessage,
31
+ } from './messages/types';
@@ -0,0 +1,16 @@
1
+ import { BunTorrentError } from '@utils/errors';
2
+
3
+ export enum PeerMessageErrorCode {
4
+ INVALID_LENGTH_PREFIX = 'PEER_MSG_INVALID_LENGTH_PREFIX',
5
+ INVALID_MESSAGE_LENGTH = 'PEER_MSG_INVALID_MESSAGE_LENGTH',
6
+ INVALID_PAYLOAD_LENGTH = 'PEER_MSG_INVALID_PAYLOAD_LENGTH',
7
+ INVALID_UINT32 = 'PEER_MSG_INVALID_UINT32',
8
+ UNKNOWN_MESSAGE_ID = 'PEER_MSG_UNKNOWN_MESSAGE_ID',
9
+ UNSUPPORTED_MESSAGE = 'PEER_MSG_UNSUPPORTED_MESSAGE',
10
+ }
11
+
12
+ export class PeerMessageError extends BunTorrentError {
13
+ constructor(code: PeerMessageErrorCode, message: string) {
14
+ super(message, code);
15
+ }
16
+ }
@@ -0,0 +1,36 @@
1
+ import { concatBytes } from '@utils/buffers';
2
+ import { PeerMessageError, PeerMessageErrorCode } from './error';
3
+ import type { PeerMessageId } from './types';
4
+
5
+ const UINT32_MAX = 0xffffffff;
6
+
7
+ export const encodeFrame = (
8
+ id: PeerMessageId,
9
+ payload: Uint8Array<ArrayBufferLike> = new Uint8Array(),
10
+ ): Uint8Array => concatBytes([writeUInt32(1 + payload.byteLength), new Uint8Array([id]), payload]);
11
+
12
+ export const writeUInt32 = (value: number): Uint8Array => {
13
+ assertUInt32(value);
14
+
15
+ const bytes = new Uint8Array(4);
16
+ new DataView(bytes.buffer).setUint32(0, value, false);
17
+ return bytes;
18
+ };
19
+
20
+ export const readUInt32 = (input: Uint8Array, offset: number): number => {
21
+ if (offset < 0 || offset + 4 > input.byteLength) {
22
+ return fail(PeerMessageErrorCode.INVALID_LENGTH_PREFIX, 'Not enough bytes to read uint32');
23
+ }
24
+
25
+ return new DataView(input.buffer, input.byteOffset + offset, 4).getUint32(0, false);
26
+ };
27
+
28
+ export const assertUInt32 = (value: number): void => {
29
+ if (!Number.isInteger(value) || value < 0 || value > UINT32_MAX) {
30
+ return fail(PeerMessageErrorCode.INVALID_UINT32, `Invalid uint32: ${value}`);
31
+ }
32
+ };
33
+
34
+ const fail = (code: PeerMessageErrorCode, message: string): never => {
35
+ throw new PeerMessageError(code, message);
36
+ };
@@ -0,0 +1,174 @@
1
+ import { PeerMessageError, PeerMessageErrorCode } from './error';
2
+ import { encodeFrame, writeUInt32 } from './helpers';
3
+ import { PeerMessageId, type PeerMessage } from './types';
4
+ import { concatBytes } from '@utils/buffers';
5
+
6
+ export const encodePeerMessage = (message: PeerMessage): Uint8Array => {
7
+ switch (message.type) {
8
+ case 'keep-alive':
9
+ return writeUInt32(0);
10
+ case 'choke':
11
+ return encodeFrame(PeerMessageId.Choke);
12
+ case 'unchoke':
13
+ return encodeFrame(PeerMessageId.Unchoke);
14
+ case 'interested':
15
+ return encodeFrame(PeerMessageId.Interested);
16
+ case 'not-interested':
17
+ return encodeFrame(PeerMessageId.NotInterested);
18
+ case 'have':
19
+ return encodeFrame(PeerMessageId.Have, writeUInt32(message.pieceIndex));
20
+ case 'bitfield':
21
+ return encodeFrame(PeerMessageId.Bitfield, message.bitfield);
22
+ case 'request':
23
+ return encodeFrame(
24
+ PeerMessageId.Request,
25
+ concatBytes([
26
+ writeUInt32(message.pieceIndex),
27
+ writeUInt32(message.offset),
28
+ writeUInt32(message.length),
29
+ ]),
30
+ );
31
+ case 'piece':
32
+ return encodeFrame(
33
+ PeerMessageId.Piece,
34
+ concatBytes([
35
+ writeUInt32(message.pieceIndex),
36
+ writeUInt32(message.offset),
37
+ message.block,
38
+ ]),
39
+ );
40
+ case 'cancel':
41
+ return encodeFrame(
42
+ PeerMessageId.Cancel,
43
+ concatBytes([
44
+ writeUInt32(message.pieceIndex),
45
+ writeUInt32(message.offset),
46
+ writeUInt32(message.length),
47
+ ]),
48
+ );
49
+ default:
50
+ return assertNever(message);
51
+ }
52
+ };
53
+
54
+ export const decodePeerMessage = (input: Uint8Array): PeerMessage => {
55
+ if (input.byteLength < 4) {
56
+ return fail(
57
+ PeerMessageErrorCode.INVALID_LENGTH_PREFIX,
58
+ 'Peer message is missing length prefix',
59
+ );
60
+ }
61
+
62
+ const length = readUInt32(input, 0);
63
+ if (input.byteLength !== 4 + length) {
64
+ return fail(
65
+ PeerMessageErrorCode.INVALID_MESSAGE_LENGTH,
66
+ 'Peer message length does not match input size',
67
+ );
68
+ }
69
+
70
+ if (length === 0) {
71
+ return { type: 'keep-alive' };
72
+ }
73
+
74
+ const id = input[4];
75
+ const payload = input.subarray(5);
76
+
77
+ switch (id) {
78
+ case PeerMessageId.Choke:
79
+ assertPayloadLength(payload, 0, 'choke');
80
+ return { type: 'choke' };
81
+ case PeerMessageId.Unchoke:
82
+ assertPayloadLength(payload, 0, 'unchoke');
83
+ return { type: 'unchoke' };
84
+ case PeerMessageId.Interested:
85
+ assertPayloadLength(payload, 0, 'interested');
86
+ return { type: 'interested' };
87
+ case PeerMessageId.NotInterested:
88
+ assertPayloadLength(payload, 0, 'not-interested');
89
+ return { type: 'not-interested' };
90
+ case PeerMessageId.Have:
91
+ assertPayloadLength(payload, 4, 'have');
92
+ return { type: 'have', pieceIndex: readUInt32(payload, 0) };
93
+ case PeerMessageId.Bitfield:
94
+ return { type: 'bitfield', bitfield: payload };
95
+ case PeerMessageId.Request:
96
+ assertPayloadLength(payload, 12, 'request');
97
+ return {
98
+ type: 'request',
99
+ pieceIndex: readUInt32(payload, 0),
100
+ offset: readUInt32(payload, 4),
101
+ length: readUInt32(payload, 8),
102
+ };
103
+ case PeerMessageId.Piece:
104
+ assertMinPayloadLength(payload, 8, 'piece');
105
+ return {
106
+ type: 'piece',
107
+ pieceIndex: readUInt32(payload, 0),
108
+ offset: readUInt32(payload, 4),
109
+ block: payload.subarray(8),
110
+ };
111
+ case PeerMessageId.Cancel:
112
+ assertPayloadLength(payload, 12, 'cancel');
113
+ return {
114
+ type: 'cancel',
115
+ pieceIndex: readUInt32(payload, 0),
116
+ offset: readUInt32(payload, 4),
117
+ length: readUInt32(payload, 8),
118
+ };
119
+ default:
120
+ return fail(PeerMessageErrorCode.UNKNOWN_MESSAGE_ID, `Unknown peer message id: ${id}`);
121
+ }
122
+ };
123
+
124
+ export { PeerMessageError, PeerMessageErrorCode } from './error';
125
+ export { PeerMessageId } from './types';
126
+ export type {
127
+ BitfieldMessage,
128
+ CancelMessage,
129
+ ChokeMessage,
130
+ HaveMessage,
131
+ InterestedMessage,
132
+ KeepAliveMessage,
133
+ NotInterestedMessage,
134
+ PeerMessage,
135
+ PieceMessage,
136
+ RequestMessage,
137
+ UnchokeMessage,
138
+ } from './types';
139
+
140
+ const readUInt32 = (input: Uint8Array, offset: number): number => {
141
+ if (offset < 0 || offset + 4 > input.byteLength) {
142
+ return fail(
143
+ PeerMessageErrorCode.INVALID_PAYLOAD_LENGTH,
144
+ 'Not enough payload bytes to read uint32',
145
+ );
146
+ }
147
+
148
+ return new DataView(input.buffer, input.byteOffset + offset, 4).getUint32(0, false);
149
+ };
150
+
151
+ const assertPayloadLength = (payload: Uint8Array, expected: number, type: string): void => {
152
+ if (payload.byteLength !== expected) {
153
+ return fail(
154
+ PeerMessageErrorCode.INVALID_PAYLOAD_LENGTH,
155
+ `${type} payload must be ${expected} bytes, got ${payload.byteLength}`,
156
+ );
157
+ }
158
+ };
159
+
160
+ const assertMinPayloadLength = (payload: Uint8Array, minimum: number, type: string): void => {
161
+ if (payload.byteLength < minimum) {
162
+ return fail(
163
+ PeerMessageErrorCode.INVALID_PAYLOAD_LENGTH,
164
+ `${type} payload must be at least ${minimum} bytes, got ${payload.byteLength}`,
165
+ );
166
+ }
167
+ };
168
+
169
+ const assertNever = (value: never): never =>
170
+ fail(PeerMessageErrorCode.UNSUPPORTED_MESSAGE, `Unsupported peer message: ${String(value)}`);
171
+
172
+ const fail = (code: PeerMessageErrorCode, message: string): never => {
173
+ throw new PeerMessageError(code, message);
174
+ };