nx_magnet.nx source
↩ module page · 217 lines · 8922 B
1// nx_magnet.nx -- BEP-9 magnet-URI parser (the live-client ENTRY point: add_magnet's first step,
2// X-TORRENT-LIVE-001). Pure/deterministic, no network: parse "magnet:?xt=urn:btih:<hash>&dn=..&tr=.."
3// into the 20-byte info_hash + tracker URL list + display name. The info_hash is the identity used
4// by tracker announces (nx_udp_tracker) + peer handshakes (BEP-3); trackers seed peer discovery;
5// the name is the NAS target. Handles BOTH btih forms: 40-hex and 32-char base32 (RFC4648), and
6// %XX / '+' URL-decoding of tr/dn. Sovereign (raw syscalls only); BitTorrent magnet = last-mile
7// interop, the parse is Nishi-native. Self-gating (main = baked KAT). license_tier: ORIGINAL
8//
9// module: nishi-core.torrent.magnet
10// depends: nishi-core.sys.syscalls
11// capability: TORRENT_MAGNET_PARSE
12import "nx_syscalls.nx"
13const MG_MAGIC_1024: i64 = 1024
14
15const MG_NTRK_MAX: i64 = 32
16
17func mg_w(fd: i64, s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(fd,s,n); return 0 }
18func mg_wn(fd: i64, v: i64) -> i64 {
19 let t: *u8=sys_mmap(28); var m: i64=v; if m<0 {m=0-m; sys_write(fd,"-" as *u8,1)}
20 var k: i64=0; if m==0 {t[0]=48 as u8;k=1}; while m>0 {t[k]=(48+(m%10)) as u8; m=m/10; k=k+1}
21 let b: *u8=sys_mmap(28); var i: i64=0; while i<k {b[i]=t[k-1-i]; i=i+1}; sys_write(fd,b,k); return 0
22}
23
24func mg_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
25
26// hex char -> 0..15, else -1
27func mg_hexval(c: i64) -> i64 {
28 if c >= 48 { if c <= 57 { return c - 48 } } // 0-9
29 if c >= 97 { if c <= 102 { return c - 97 + 10 } } // a-f
30 if c >= 65 { if c <= 70 { return c - 65 + 10 } } // A-F
31 return 0 - 1
32}
33// base32 char (RFC4648) -> 0..31, else -1
34func mg_b32val(c: i64) -> i64 {
35 if c >= 65 { if c <= 90 { return c - 65 } } // A-Z = 0..25
36 if c >= 97 { if c <= 122 { return c - 97 } } // a-z = 0..25 (case-insensitive)
37 if c >= 50 { if c <= 55 { return c - 50 + 26 } } // 2-7 = 26..31
38 return 0 - 1
39}
40
41// decode the btih value buf[a,b) (40-hex OR 32-base32) into out[0..20). returns 20 ok / 0 fail.
42func mg_decode_btih(buf: *u8, a: i64, b: i64, out: *u8) -> i64 {
43 let n: i64 = b - a
44 if n == 40 {
45 var i: i64 = 0
46 while i < 20 {
47 let hi: i64 = mg_hexval(buf[a + 2*i] as i64)
48 let lo: i64 = mg_hexval(buf[a + 2*i + 1] as i64)
49 if hi < 0 { return 0 }
50 if lo < 0 { return 0 }
51 out[i] = (hi * 16 + lo) as u8
52 i = i + 1
53 }
54 return 20
55 }
56 if n == 32 {
57 var acc: i64 = 0
58 var bits: i64 = 0
59 var oi: i64 = 0
60 var i: i64 = 0
61 while i < 32 {
62 let v: i64 = mg_b32val(buf[a + i] as i64)
63 if v < 0 { return 0 }
64 acc = acc * 32 + v
65 bits = bits + 5
66 if bits >= 8 {
67 bits = bits - 8
68 // emit the top byte: (acc >> bits) & 0xff
69 var sh: i64 = acc
70 var s: i64 = 0
71 while s < bits { sh = sh / 2; s = s + 1 }
72 out[oi] = (sh % 256) as u8
73 oi = oi + 1
74 // drop the emitted top bits from acc: acc = acc % (1<<bits)
75 var modv: i64 = 1
76 var s2: i64 = 0
77 while s2 < bits { modv = modv * 2; s2 = s2 + 1 }
78 acc = acc % modv
79 }
80 i = i + 1
81 }
82 if oi == 20 { return 20 }
83 return 0
84 }
85 return 0
86}
87
88// URL-decode src[a,b) into out (null-terminated): %XX -> byte, '+' -> space, else copy.
89func mg_urldecode(src: *u8, a: i64, b: i64, out: *u8) -> i64 {
90 var i: i64 = a
91 var o: i64 = 0
92 while i < b {
93 let c: i64 = src[i] as i64
94 if c == 37 { // '%'
95 if i + 2 < b {
96 let hi: i64 = mg_hexval(src[i+1] as i64)
97 let lo: i64 = mg_hexval(src[i+2] as i64)
98 if hi >= 0 { if lo >= 0 { out[o] = (hi*16+lo) as u8; o = o + 1; i = i + 3 } else { out[o]=src[i] as u8; o=o+1; i=i+1 } } else { out[o]=src[i] as u8; o=o+1; i=i+1 }
99 } else { out[o] = src[i] as u8; o = o + 1; i = i + 1 }
100 } else {
101 if c == 43 { out[o] = 32 as u8 } else { out[o] = src[i] as u8 } // '+' -> space
102 o = o + 1; i = i + 1
103 }
104 }
105 out[o] = 0 as u8
106 return o
107}
108
109// key buf[a,kb) equals lit? (lit null-terminated)
110func mg_keyeq(buf: *u8, a: i64, kb: i64, lit: *u8) -> i64 {
111 let ll: i64 = mg_len(lit)
112 if kb - a != ll { return 0 }
113 var i: i64 = 0
114 while i < ll { if buf[a+i] != lit[i] { return 0 } i = i + 1 }
115 return 1
116}
117
118// buf[a,..) starts with lit? 1/0
119func mg_starts(buf: *u8, a: i64, b: i64, lit: *u8) -> i64 {
120 let ll: i64 = mg_len(lit)
121 if a + ll > b { return 0 }
122 var i: i64 = 0
123 while i < ll { if buf[a+i] != lit[i] { return 0 } i = i + 1 }
124 return 1
125}
126
127// parse a magnet URI. ihash=20B out; trk=ptr array (trkcap); ntrk[0]=tracker count out; name=out.
128// returns 1 iff a valid btih info_hash was decoded, else 0.
129func mg_parse(uri: *u8, ihash: *u8, trk: *i64, trkcap: i64, ntrk: *i64, name: *u8) -> i64 {
130 ntrk[0] = 0
131 name[0] = 0 as u8
132 let n: i64 = mg_len(uri)
133 if mg_starts(uri, 0, n, "magnet:?" as *u8) == 0 { return 0 }
134 var found: i64 = 0
135 var p: i64 = 8 // after "magnet:?"
136 while p < n {
137 // param = [p, e) where e = next '&' or n
138 var e: i64 = p
139 var g: i64 = 1
140 while g == 1 { if e >= n { g = 0 } else { if uri[e] == (38 as u8) { g = 0 } else { e = e + 1 } } }
141 // '=' splits key=[p,eq) value=[eq+1,e)
142 var eq: i64 = p
143 var ge: i64 = 1
144 while ge == 1 { if eq >= e { ge = 0 } else { if uri[eq] == (61 as u8) { ge = 0 } else { eq = eq + 1 } } }
145 if eq < e {
146 let vstart: i64 = eq + 1
147 if mg_keyeq(uri, p, eq, "xt" as *u8) == 1 {
148 if mg_starts(uri, vstart, e, "urn:btih:" as *u8) == 1 {
149 if mg_decode_btih(uri, vstart + 9, e, ihash) == 20 { found = 1 }
150 }
151 }
152 if mg_keyeq(uri, p, eq, "tr" as *u8) == 1 {
153 if ntrk[0] < trkcap {
154 let tb: *u8 = sys_mmap(MG_MAGIC_1024)
155 mg_urldecode(uri, vstart, e, tb)
156 trk[ntrk[0]] = tb as i64
157 ntrk[0] = ntrk[0] + 1
158 }
159 }
160 if mg_keyeq(uri, p, eq, "dn" as *u8) == 1 {
161 mg_urldecode(uri, vstart, e, name)
162 }
163 }
164 p = e + 1
165 }
166 return found
167}
168
169// ===================== BAKED SELF-TEST (KAT) =====================
170func main() -> i64 {
171 let ihash: *u8 = sys_mmap(32)
172 let trk: *i64 = sys_mmap(8 * MG_NTRK_MAX) as *i64
173 let ntrk: *i64 = sys_mmap(16) as *i64
174 let name: *u8 = sys_mmap(MG_MAGIC_1024)
175
176 // ---- pos1: 40-hex btih + url-encoded trackers + '+'-encoded name ----
177 let u1: *u8 = "magnet:?dn=Test+File&xt=urn:btih:0123456789ABCDEF0123456789ABCDEF01234567&tr=http%3A%2F%2Ftracker.example%2Fannounce&tr=udp%3A%2F%2Ftr2%3A80" as *u8
178 let r1: i64 = mg_parse(u1, ihash, trk, MG_NTRK_MAX, ntrk, name)
179 var pos1: i64 = 0
180 if r1 == 1 {
181 if (ihash[0] as i64) == 1 { if (ihash[1] as i64) == 35 { if (ihash[19] as i64) == 103 {
182 if ntrk[0] == 2 {
183 let t0: *u8 = trk[0] as *u8
184 if mg_starts(t0, 0, mg_len(t0), "http://tracker.example/announce" as *u8) == 1 {
185 // name == "Test File"
186 if name[0]==(84 as u8) { if name[4]==(32 as u8) { if name[5]==(70 as u8) { pos1 = 1 } } }
187 }
188 }
189 } } }
190 }
191
192 // ---- pos2: 32-char base32 btih, all-'A' -> 20 zero bytes ----
193 let u2: *u8 = "magnet:?xt=urn:btih:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" as *u8
194 let r2: i64 = mg_parse(u2, ihash, trk, MG_NTRK_MAX, ntrk, name)
195 var pos2: i64 = 0
196 if r2 == 1 { if (ihash[0] as i64)==0 { if (ihash[10] as i64)==0 { if (ihash[19] as i64)==0 { pos2 = 1 } } } }
197
198 // ---- neg1: non-btih xt -> reject ----
199 let u3: *u8 = "magnet:?xt=urn:sha1:HELLO&dn=x" as *u8
200 var neg1: i64 = 0
201 if mg_parse(u3, ihash, trk, MG_NTRK_MAX, ntrk, name) == 0 { neg1 = 1 }
202
203 // ---- neg2: btih wrong length (4 hex) -> reject ----
204 let u4: *u8 = "magnet:?xt=urn:btih:0123" as *u8
205 var neg2: i64 = 0
206 if mg_parse(u4, ihash, trk, MG_NTRK_MAX, ntrk, name) == 0 { neg2 = 1 }
207
208 mg_w(1, "MAGNET-GATE authored=organ pos1_hex=" as *u8); mg_wn(1, pos1)
209 mg_w(1, " pos2_base32=" as *u8); mg_wn(1, pos2)
210 mg_w(1, " neg1_nonbtih=" as *u8); mg_wn(1, neg1)
211 mg_w(1, " neg2_badlen=" as *u8); mg_wn(1, neg2)
212 var ok: i64 = 0
213 if pos1 == 1 { if pos2 == 1 { if neg1 == 1 { if neg2 == 1 { ok = 1 } } } }
214 if ok == 1 { mg_w(1, " verdict=GREEN\n" as *u8); sys_exit(0); return 0 }
215 mg_w(1, " verdict=RED\n" as *u8); sys_exit(1)
216 return 1
217}