@ctrl/magnet-link 2.0.4 → 3.1.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.
@@ -0,0 +1,2 @@
1
+ export declare function composeRange(range: number[]): string[];
2
+ export declare function parseRange(range: string[]): number[];
@@ -0,0 +1,20 @@
1
+ export function composeRange(range) {
2
+ return range
3
+ .reduce((acc, cur, idx, arr) => {
4
+ if (idx === 0 || cur !== arr[idx - 1] + 1) {
5
+ acc.push([]);
6
+ }
7
+ acc[acc.length - 1].push(`${cur}`);
8
+ return acc;
9
+ }, [])
10
+ .map(cur => {
11
+ return cur.length > 1 ? `${cur[0]}-${cur[cur.length - 1]}` : `${cur[0]}`;
12
+ });
13
+ }
14
+ export function parseRange(range) {
15
+ const generateRange = (start, end = start) => Array.from({ length: end - start + 1 }, (_, idx) => idx + start);
16
+ return range.reduce((acc, cur) => {
17
+ const r = cur.split('-').map(cur => parseInt(cur, 10));
18
+ return acc.concat(generateRange(r[0], r[1]));
19
+ }, []);
20
+ }
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" resolution-mode="require"/>
1
2
  export interface MagnetData {
2
3
  /**
3
4
  * Is the info-hash hex encoded, for a total of 40 characters. For compatability with existing links in the wild, clients should also support the 32 character base32 encoded info-hash.
@@ -7,11 +8,14 @@ export interface MagnetData {
7
8
  * Is the multihash formatted, hex encoded full infohash for torrents in the new metadata format. 'btmh' and 'btih' exact topics may exist in the same magnet if they describe the same hybrid torrent.
8
9
  * @link http://www.bittorrent.org/beps/bep_0009.html
9
10
  */
10
- xt: string | string[];
11
+ xt?: string | string[];
11
12
  /**
12
13
  * Parsed xt= parameter see xt
13
14
  */
14
15
  infoHash?: string;
16
+ infoHashBuffer?: Buffer;
17
+ infoHashV2?: string;
18
+ infoHashV2Buffer?: Buffer;
15
19
  /**
16
20
  * The display name that may be used by the client to display while waiting for metadata
17
21
  */
@@ -44,6 +48,8 @@ export interface MagnetData {
44
48
  * "keyword topic": a more general search, specifying search terms rather than a particular file
45
49
  */
46
50
  kt?: string[];
51
+ so?: string[] | number[];
52
+ 'x.pe'?: string | string[];
47
53
  /**
48
54
  * "keyword topic": a more general search, specifying search terms rather than a particular file
49
55
  */
@@ -60,6 +66,9 @@ export interface MagnetData {
60
66
  * Combined as= and ws= parameters if they exist
61
67
  */
62
68
  urlList?: string[];
69
+ peerAddresses?: string[];
70
+ publicKey?: string;
71
+ publicKeyBuffer?: Buffer;
63
72
  }
64
73
  export declare function magnetDecode(uri: string): MagnetData;
65
74
  export declare function magnetEncode(data: MagnetData): string;
@@ -1,4 +1,5 @@
1
1
  import { base32Decode } from '@ctrl/ts-base32';
2
+ import * as bep53Range from './bep53.js';
2
3
  const start = 'magnet:?';
3
4
  export function magnetDecode(uri) {
4
5
  // Support 'stream-magnet:' as well
@@ -23,9 +24,12 @@ export function magnetDecode(uri) {
23
24
  }
24
25
  // If there are repeated parameters, return an array of values
25
26
  if (r && Array.isArray(r)) {
26
- return r.push(val);
27
+ r.push(val);
28
+ return;
27
29
  }
28
30
  result[key] = [r, val];
31
+ // eslint-disable-next-line no-useless-return
32
+ return;
29
33
  });
30
34
  if (result.xt) {
31
35
  let m;
@@ -38,8 +42,29 @@ export function magnetDecode(uri) {
38
42
  const decodedStr = base32Decode(m[1]);
39
43
  result.infoHash = Buffer.from(decodedStr).toString('hex');
40
44
  }
45
+ else if ((m = xt.match(/^urn:btmh:1220(.{64})/))) {
46
+ result.infoHashV2 = m[1].toLowerCase();
47
+ }
41
48
  });
42
49
  }
50
+ if (result.xs) {
51
+ let m;
52
+ const xss = Array.isArray(result.xs) ? result.xs : [result.xs];
53
+ xss.forEach(xs => {
54
+ if ((m = /^urn:btpk:(.{64})/.exec(xs))) {
55
+ result.publicKey = m[1]?.toLowerCase();
56
+ }
57
+ });
58
+ }
59
+ if (result.infoHash) {
60
+ result.infoHashBuffer = Buffer.from(result.infoHash, 'hex');
61
+ }
62
+ if (result.infoHashV2) {
63
+ result.infoHashV2Buffer = Buffer.from(result.infoHashV2, 'hex');
64
+ }
65
+ if (result.publicKey) {
66
+ result.publicKeyBuffer = Buffer.from(result.publicKey, 'hex');
67
+ }
43
68
  if (result.dn) {
44
69
  result.name = result.dn;
45
70
  }
@@ -62,8 +87,13 @@ export function magnetDecode(uri) {
62
87
  if (typeof result.ws === 'string' || Array.isArray(result.ws)) {
63
88
  result.urlList = result.urlList.concat(result.ws);
64
89
  }
90
+ result.peerAddresses = [];
91
+ if (typeof result['x.pe'] === 'string' || Array.isArray(result['x.pe'])) {
92
+ result.peerAddresses = result.peerAddresses.concat(result['x.pe']);
93
+ }
65
94
  result.announce = [...new Set(result.announce)].sort((a, b) => a.localeCompare(b));
66
95
  result.urlList = [...new Set(result.urlList)].sort((a, b) => a.localeCompare(b));
96
+ result.peerAddresses = [...new Set(result.peerAddresses)];
67
97
  return result;
68
98
  }
69
99
  /**
@@ -83,6 +113,10 @@ function parseQueryParamValue(key, val) {
83
113
  if (key === 'kt') {
84
114
  return decodeURIComponent(val).split('+');
85
115
  }
116
+ // bep53
117
+ if (key === 'so') {
118
+ return bep53Range.parseRange(decodeURIComponent(val).split(','));
119
+ }
86
120
  // Cast file index (ix) to a number
87
121
  if (key === 'ix') {
88
122
  return Number(val);
@@ -91,11 +125,44 @@ function parseQueryParamValue(key, val) {
91
125
  }
92
126
  export function magnetEncode(data) {
93
127
  const obj = { ...data }; // Shallow clone object
128
+ // Deduplicate xt by using a set
129
+ let xts = new Set();
130
+ if (obj.xt && typeof obj.xt === 'string') {
131
+ xts.add(obj.xt);
132
+ }
133
+ if (obj.xt && Array.isArray(obj.xt)) {
134
+ xts = new Set(obj.xt);
135
+ }
136
+ if (obj.infoHashBuffer) {
137
+ xts.add(`urn:btih:${obj.infoHashBuffer.toString('hex')}`);
138
+ }
139
+ if (obj.infoHash) {
140
+ xts.add(`urn:btih:${obj.infoHash}`);
141
+ }
142
+ if (obj.infoHashV2Buffer) {
143
+ xts.add((obj.xt = `urn:btmh:1220${obj.infoHashV2Buffer.toString('hex')}`));
144
+ }
145
+ if (obj.infoHashV2) {
146
+ xts.add(`urn:btmh:1220${obj.infoHashV2}`);
147
+ }
148
+ const xtsDeduped = Array.from(xts);
149
+ if (xtsDeduped.length === 1) {
150
+ obj.xt = xtsDeduped[0];
151
+ }
152
+ if (xtsDeduped.length > 1) {
153
+ obj.xt = xtsDeduped;
154
+ }
94
155
  // Support using convenience names, in addition to spec names
95
156
  // (example: `infoHash` for `xt`, `name` for `dn`)
96
157
  if (obj.infoHash) {
97
158
  obj.xt = `urn:btih:${obj.infoHash}`;
98
159
  }
160
+ if (obj.publicKeyBuffer) {
161
+ obj.xs = `urn:btpk:${obj.publicKeyBuffer.toString('hex')}`;
162
+ }
163
+ if (obj.publicKey) {
164
+ obj.xs = `urn:btpk:${obj.publicKey}`;
165
+ }
99
166
  if (obj.name) {
100
167
  obj.dn = obj.name;
101
168
  }
@@ -109,32 +176,44 @@ export function magnetEncode(data) {
109
176
  obj.ws = obj.urlList;
110
177
  delete obj.as;
111
178
  }
179
+ if (obj.peerAddresses) {
180
+ obj['x.pe'] = obj.peerAddresses;
181
+ }
112
182
  return Object.keys(obj)
113
- .filter(key => key.length === 2)
183
+ .filter(key => key.length === 2 || key === 'x.pe')
114
184
  .reduce((prev, key, i) => {
115
185
  let acc = prev;
116
186
  const values = Array.isArray(obj[key]) ? obj[key] : [obj[key]];
117
187
  values.forEach((val, j) => {
118
- if ((i > 0 || j > 0) && (key !== 'kt' || j === 0)) {
119
- acc = `${acc}&`;
188
+ if ((i > 0 || j > 0) && ((key !== 'kt' && key !== 'so') || j === 0)) {
189
+ acc += '&';
120
190
  }
121
- let res = val;
122
191
  if (key === 'dn') {
123
- res = encodeURIComponent(val).replace(/%20/g, '+');
192
+ val = encodeURIComponent(val).replace(/%20/g, '+');
124
193
  }
125
- if (key === 'tr' || key === 'xs' || key === 'as' || key === 'ws') {
126
- res = encodeURIComponent(val);
194
+ if (key === 'tr' || key === 'as' || key === 'ws') {
195
+ val = encodeURIComponent(val);
196
+ }
197
+ // Don't URI encode BEP46 keys
198
+ if (key === 'xs' && !val.startsWith('urn:btpk:')) {
199
+ val = encodeURIComponent(val);
127
200
  }
128
201
  if (key === 'kt') {
129
- res = encodeURIComponent(val);
202
+ val = encodeURIComponent(val);
203
+ }
204
+ if (key === 'so') {
205
+ return;
130
206
  }
131
207
  if (key === 'kt' && j > 0) {
132
- acc = `${acc}+${res}`;
208
+ acc += `+${val}`;
133
209
  }
134
210
  else {
135
- acc = `${acc}${key}=${res}`;
211
+ acc += `${key}=${val}`;
136
212
  }
137
213
  });
214
+ if (key === 'so') {
215
+ acc += `${key}=${bep53Range.composeRange(values)}`;
216
+ }
138
217
  return acc;
139
218
  }, `${start}`);
140
219
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl/magnet-link",
3
- "version": "2.0.4",
3
+ "version": "3.1.1",
4
4
  "description": "Parse a magnet URI into an object",
5
5
  "author": "Scott Cooper <scttcper@gmail.com>",
6
6
  "homepage": "https://magnet-link.vercel.app",
@@ -12,63 +12,48 @@
12
12
  "link",
13
13
  "magnet-uri"
14
14
  ],
15
- "main": "dist/index.js",
16
- "module": "dist/module/index.js",
17
- "typings": "dist/index.d.ts",
15
+ "type": "module",
16
+ "exports": "./dist/src/index.js",
17
+ "types": "./dist/src/index.d.ts",
18
18
  "files": [
19
- "dist"
19
+ "dist/src"
20
20
  ],
21
21
  "sideEffects": false,
22
- "publishConfig": {
23
- "access": "public"
24
- },
25
22
  "scripts": {
26
- "build:demo": "rollup -c rollup.demo.js",
27
- "watch:demo": "rollup -c rollup.demo.js -w",
28
- "lint": "eslint --ext .js,.ts, .",
29
- "lint:fix": "eslint --fix --ext .js,.ts, .",
23
+ "demo:build": "npm run build --workspace=demo",
24
+ "demo:watch": "npm run dev --workspace=demo",
25
+ "lint": "eslint --ext .ts .",
26
+ "lint:fix": "eslint --fix --ext .ts .",
30
27
  "prepare": "npm run build",
31
- "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.module.json",
32
- "test": "jest",
33
- "test:watch": "jest --watch",
34
- "test:ci": "jest --coverage --no-cache"
28
+ "build": "tsc",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "test:ci": "vitest run --coverage --reporter=junit --outputFile=./junit.xml"
35
32
  },
36
33
  "dependencies": {
37
- "@ctrl/ts-base32": "^1.2.4"
34
+ "@ctrl/ts-base32": "^2.1.1"
38
35
  },
39
36
  "devDependencies": {
40
- "@babel/plugin-transform-modules-commonjs": "7.12.1",
41
- "@babel/preset-typescript": "7.12.7",
42
- "@ctrl/eslint-config": "1.2.8",
43
- "@jest/globals": "26.6.2",
44
- "@rollup/plugin-node-resolve": "11.0.0",
45
- "@types/node": "14.14.13",
37
+ "@ctrl/eslint-config": "3.4.4",
38
+ "@sindresorhus/tsconfig": "3.0.1",
39
+ "@types/node": "17.0.39",
46
40
  "buffer": "6.0.3",
47
- "jest": "26.6.3",
48
- "rollup": "2.34.2",
49
- "rollup-plugin-livereload": "2.0.0",
50
- "rollup-plugin-node-builtins": "2.1.2",
51
- "rollup-plugin-node-globals": "1.4.0",
52
- "rollup-plugin-serve": "1.1.0",
53
- "rollup-plugin-terser": "7.0.2",
54
- "rollup-plugin-typescript2": "0.29.0",
55
- "typescript": "4.1.3"
56
- },
57
- "release": {
58
- "branch": "master"
41
+ "c8": "7.11.3",
42
+ "typescript": "4.7.3",
43
+ "vitest": "0.13.1"
59
44
  },
60
- "jest": {
61
- "testEnvironment": "node"
45
+ "workspaces": [
46
+ "demo"
47
+ ],
48
+ "publishConfig": {
49
+ "access": "public"
62
50
  },
63
- "babel": {
64
- "presets": [
65
- "@babel/preset-typescript"
66
- ],
67
- "plugins": [
68
- "@babel/plugin-transform-modules-commonjs"
51
+ "release": {
52
+ "branches": [
53
+ "master"
69
54
  ]
70
55
  },
71
56
  "engines": {
72
- "node": ">=10.19.0"
57
+ "node": ">=14.16"
73
58
  }
74
59
  }
package/dist/index.js DELETED
@@ -1,145 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.magnetEncode = exports.magnetDecode = void 0;
4
- const ts_base32_1 = require("@ctrl/ts-base32");
5
- const start = 'magnet:?';
6
- function magnetDecode(uri) {
7
- // Support 'stream-magnet:' as well
8
- const data = uri.substr(uri.indexOf(start) + start.length);
9
- const params = data && data.length >= 0 ? data.split('&') : [];
10
- const result = {};
11
- params.forEach(param => {
12
- const keyval = param.split('=');
13
- // This keyval is invalid, skip it
14
- if (keyval.length !== 2) {
15
- return;
16
- }
17
- const key = keyval[0];
18
- const val = parseQueryParamValue(key, keyval[1]);
19
- if (val === undefined) {
20
- return;
21
- }
22
- const r = result[key];
23
- if (!r) {
24
- result[key] = val;
25
- return result;
26
- }
27
- // If there are repeated parameters, return an array of values
28
- if (r && Array.isArray(r)) {
29
- return r.push(val);
30
- }
31
- result[key] = [r, val];
32
- });
33
- if (result.xt) {
34
- let m;
35
- const xts = Array.isArray(result.xt) ? result.xt : [result.xt];
36
- xts.forEach((xt) => {
37
- if ((m = xt.match(/^urn:btih:(.{40})/))) {
38
- result.infoHash = m[1].toLowerCase();
39
- }
40
- else if ((m = xt.match(/^urn:btih:(.{32})/))) {
41
- const decodedStr = ts_base32_1.base32Decode(m[1]);
42
- result.infoHash = Buffer.from(decodedStr).toString('hex');
43
- }
44
- });
45
- }
46
- if (result.dn) {
47
- result.name = result.dn;
48
- }
49
- if (result.kt) {
50
- result.keywords = result.kt;
51
- }
52
- if (typeof result.tr === 'string') {
53
- result.announce = [result.tr];
54
- }
55
- else if (Array.isArray(result.tr)) {
56
- result.announce = result.tr;
57
- }
58
- else {
59
- result.announce = [];
60
- }
61
- result.urlList = [];
62
- if (typeof result.as === 'string' || Array.isArray(result.as)) {
63
- result.urlList = result.urlList.concat(result.as);
64
- }
65
- if (typeof result.ws === 'string' || Array.isArray(result.ws)) {
66
- result.urlList = result.urlList.concat(result.ws);
67
- }
68
- result.announce = [...new Set(result.announce)].sort((a, b) => a.localeCompare(b));
69
- result.urlList = [...new Set(result.urlList)].sort((a, b) => a.localeCompare(b));
70
- return result;
71
- }
72
- exports.magnetDecode = magnetDecode;
73
- /**
74
- * Specific query parameters have expected formats, this attempts to parse them in the correct way
75
- */
76
- function parseQueryParamValue(key, val) {
77
- // Clean up torrent name
78
- if (key === 'dn') {
79
- return decodeURIComponent(val).replace(/\+/g, ' ');
80
- }
81
- // Address tracker (tr), exact source (xs), and acceptable source (as) are encoded
82
- // URIs, so decode them
83
- if (key === 'tr' || key === 'xs' || key === 'as' || key === 'ws') {
84
- return decodeURIComponent(val);
85
- }
86
- // Return keywords as an array
87
- if (key === 'kt') {
88
- return decodeURIComponent(val).split('+');
89
- }
90
- // Cast file index (ix) to a number
91
- if (key === 'ix') {
92
- return Number(val);
93
- }
94
- return val;
95
- }
96
- function magnetEncode(data) {
97
- const obj = { ...data }; // Shallow clone object
98
- // Support using convenience names, in addition to spec names
99
- // (example: `infoHash` for `xt`, `name` for `dn`)
100
- if (obj.infoHash) {
101
- obj.xt = `urn:btih:${obj.infoHash}`;
102
- }
103
- if (obj.name) {
104
- obj.dn = obj.name;
105
- }
106
- if (obj.keywords) {
107
- obj.kt = obj.keywords;
108
- }
109
- if (obj.announce) {
110
- obj.tr = obj.announce;
111
- }
112
- if (obj.urlList) {
113
- obj.ws = obj.urlList;
114
- delete obj.as;
115
- }
116
- return Object.keys(obj)
117
- .filter(key => key.length === 2)
118
- .reduce((prev, key, i) => {
119
- let acc = prev;
120
- const values = Array.isArray(obj[key]) ? obj[key] : [obj[key]];
121
- values.forEach((val, j) => {
122
- if ((i > 0 || j > 0) && (key !== 'kt' || j === 0)) {
123
- acc = `${acc}&`;
124
- }
125
- let res = val;
126
- if (key === 'dn') {
127
- res = encodeURIComponent(val).replace(/%20/g, '+');
128
- }
129
- if (key === 'tr' || key === 'xs' || key === 'as' || key === 'ws') {
130
- res = encodeURIComponent(val);
131
- }
132
- if (key === 'kt') {
133
- res = encodeURIComponent(val);
134
- }
135
- if (key === 'kt' && j > 0) {
136
- acc = `${acc}+${res}`;
137
- }
138
- else {
139
- acc = `${acc}${key}=${res}`;
140
- }
141
- });
142
- return acc;
143
- }, `${start}`);
144
- }
145
- exports.magnetEncode = magnetEncode;