strex: added `detectUrl()`
[iv.d.git] / btenc.d
blobbff342c8e20d0210953eb4b481f53e3acc8110a2
1 /* Invisible Vector Library
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 // some code to process bittorrent files
18 module iv.btenc /*is aliced*/;
20 import iv.alice;
21 import iv.vfs;
22 import iv.strex : quote;
25 struct BTField {
26 enum Type { UInt, Str, List, Dict }
27 Type type = Type.UInt;
28 string vstr;
29 ulong vuint;
30 BTField[] vlist;
31 BTField[string] vdict;
33 this (ulong v) @safe nothrow @nogc { type = Type.UInt; vuint = v; }
34 this (string v) @safe nothrow @nogc { type = Type.Str; vstr = v; }
35 this (const(ubyte[]) v) @safe nothrow { type = Type.Str; vstr = cast(string)(v.idup); }
37 const @safe pure nothrow @nogc {
38 @property const(ubyte)[] bytes () { return cast(const(ubyte)[])vstr; }
39 @property bool isUInt () { return (type == Type.UInt); }
40 @property bool isStr () { return (type == Type.Str); }
41 @property bool isList () { return (type == Type.List); }
42 @property bool isDict () { return (type == Type.Dict); }
45 static BTField load (string fname) {
46 import std.stdio : File;
47 return load(File(fname));
50 static BTField load(ST) (auto ref ST st) if (isReadableStream!ST) {
51 char tch;
52 st.rawReadExact((&tch)[0..1]);
53 return loadElement(st, tch);
56 void save (string fname) {
57 import std.stdio : File;
58 return save(File(fname, "w"));
61 void save(ST) (auto ref ST st) const if (isWriteableStream!ST) {
62 void putNum (ulong n) {
63 // 4294967295
64 // 18446744073709551615
65 char[32] str;
66 usize pos = str.length;
67 do str[--pos] = cast(char)('0'+n%10); while ((n /= 10) != 0);
68 st.rawWriteExact(str[pos..$]);
71 void putChar (char ch) {
72 st.rawWriteExact((&ch)[0..1]);
75 //{ import std.conv : to; import iv.writer; writeln(to!string(type)); }
76 if (type == Type.UInt) {
77 putChar('i');
78 putNum(vuint);
79 putChar('e');
80 } else if (type == Type.Str) {
81 putNum(vstr.length);
82 putChar(':');
83 st.rawWriteExact(vstr);
84 } else if (type == Type.List) {
85 putChar('l');
86 foreach (const ref BTField fld; vlist) fld.save(st);
87 putChar('e');
88 } else if (type == Type.Dict) {
89 // we have to sort keys by standard
90 import std.algorithm : sort;
91 putChar('d');
92 foreach (string key; sort(vdict.keys)) {
93 putNum(key.length);
94 putChar(':');
95 st.rawWriteExact(key);
96 vdict[key].save(st);
98 putChar('e');
99 } else {
100 assert(0);
104 ref BTField opIndex (string path) {
105 auto res = byPath!"throw"(path);
106 return *res;
109 BTField* opBinaryRight(string op="in") (string path) { return byPath(path); }
111 private:
112 // opt: "throw"
113 BTField* byPath(string opt="") (string path) {
114 static assert(opt.length == 0 || opt == "throw", "invalid mode; only 'throw' allowed");
116 static if (opt == "throw") {
117 auto origpath = path;
119 void error (string msg=null) {
120 //import iv.strex : quote;
121 if (msg.length == 0) {
122 throw new Exception("invalid path: "~quote(origpath));
123 } else {
124 throw new Exception(msg~" at path: "~quote(origpath));
129 BTField* cf = &this;
130 while (path.length > 0) {
131 while (path.length > 0 && path[0] == '/') path = path[1..$];
132 if (path.length == 0) break;
133 // extract component
134 usize epos = 1;
135 while (epos < path.length && path[epos] != '/') ++epos;
136 auto part = path[0..epos];
137 path = path[epos..$];
138 if (cf.type == Type.List) {
139 // this should be number
140 bool neg = false;
141 if (part[0] == '-') { neg = true; part = part[1..$]; }
142 else if (part[0] == '+') part = part[1..$];
143 if (part.length == 0) {
144 static if (opt == "throw") error("invalid number");
145 else return null;
147 usize idx = 0;
148 while (part.length > 0) {
149 if (part[0] < '0' || part[0] > '9') {
150 static if (opt == "throw") error("invalid number");
151 else return null;
153 usize n1 = idx*10;
154 if (n1 < idx) {
155 static if (opt == "throw") error("numeric overflow");
156 else return null;
158 n1 += part[0]-'0';
159 if (n1 < idx) {
160 static if (opt == "throw") error("numeric overflow");
161 else return null;
163 idx = n1;
164 part = part[1..$];
166 if (idx >= cf.vlist.length) {
167 static if (opt == "throw") error("index out of range");
168 else return null;
170 cf = &cf.vlist[idx];
171 } else if (cf.type == Type.Dict) {
172 cf = part in cf.vdict;
173 if (cf is null) {
174 static if (opt == "throw") {
175 //import iv.strex : quote;
176 error("dictionary item not found ("~quote(part)~")");
177 } else {
178 return null;
181 } else {
182 static if (opt == "throw") error();
183 else return null;
186 return cf;
190 static BTField loadElement(ST) (auto ref ST st, char tch) if (isReadableStream!ST) {
191 ulong loadInteger (char ech, char fch=0) {
192 ulong n = 0;
193 if (fch >= '0' && fch <= '9') n = fch-'0';
194 for (;;) {
195 char ch;
196 st.rawReadExact((&ch)[0..1]);
197 if (ch == ech) break;
198 if (ch >= '0' && ch <= '9') {
199 ulong n1 = n*10;
200 if (n1 < n) throw new Exception("integer overflow");
201 n1 += ch-'0';
202 if (n1 < n) throw new Exception("integer overflow");
203 n = n1;
204 } else {
205 //{ import iv.writer; writeln(ch); }
206 throw new Exception("invalid file format");
209 return n;
212 string loadStr (char fch=0) {
213 auto len = loadInteger(':', fch);
214 if (len > 1024*1024*8) throw new Exception("string overflow");
215 if (len > 0) {
216 import std.exception : assumeUnique;
217 auto res = new char[](cast(usize)len);
218 st.rawReadExact(res);
219 return res.assumeUnique;
220 } else {
221 return "";
225 void loadUInt (ref BTField fld) {
226 fld.type = Type.UInt;
227 fld.vuint = loadInteger('e');
230 void loadList (ref BTField fld) {
231 fld.type = Type.List;
232 for (;;) {
233 char tch;
234 st.rawReadExact((&tch)[0..1]);
235 if (tch == 'e') break;
236 fld.vlist ~= loadElement(st, tch);
240 void loadDict (ref BTField fld) {
241 fld.type = Type.Dict;
242 for (;;) {
243 char tch;
244 st.rawReadExact((&tch)[0..1]);
245 if (tch == 'e') break;
246 if (tch < '0' || tch > '9') throw new Exception("string overflow");
247 auto name = loadStr(tch);
248 st.rawReadExact((&tch)[0..1]);
249 fld.vdict[name] = loadElement(st, tch);
253 BTField res;
254 if (tch == 'l') {
255 debug { import iv.writer; writeln("LIST"); }
256 loadList(res);
257 } else if (tch == 'd') {
258 debug { import iv.writer; writeln("DICT"); }
259 loadDict(res);
260 } else if (tch == 'i') {
261 debug { import iv.writer; writeln("UINT"); }
262 loadUInt(res);
263 } else if (tch >= '0' && tch <= '9') {
264 debug { import iv.writer; writeln("STR"); }
265 res.type = Type.Str;
266 res.vstr = loadStr(tch);
267 } else {
268 debug { import iv.writer; writeln(tch); }
269 throw new Exception("invalid file format");
271 return res;
276 ubyte[20] btInfoHash (string fname) {
277 import std.stdio : File;
278 return btInfoHash(File(fname));
282 ubyte[20] btInfoHash(ST) (auto ref ST st) if (isReadableStream!ST) {
283 import std.digest.sha : SHA1;
285 SHA1 dig;
286 dig.start();
287 bool doHash = false;
289 char getChar () {
290 char ch;
291 st.rawReadExact((&ch)[0..1]);
292 if (doHash) dig.put(cast(ubyte)ch);
293 return ch;
296 ulong getUInt (char ech, char fch=0) {
297 ulong n = 0;
298 char ch;
299 if (fch >= '0' && fch <= '9') n = fch-'0';
300 for (;;) {
301 ch = getChar();
302 if (ch == ech) break;
303 if (ch >= '0' && ch <= '9') {
304 ulong n1 = n*10;
305 if (n1 < n) throw new Exception("integer overflow");
306 n1 += ch-'0';
307 if (n1 < n) throw new Exception("integer overflow");
308 n = n1;
309 } else {
310 throw new Exception("invalid file format");
313 return n;
316 void skipBytes (ulong n) {
317 ubyte[256] buf = void;
318 while (n > 0) {
319 usize rd = cast(usize)(n > buf.length ? buf.length : n);
320 st.rawReadExact(buf[0..rd]);
321 if (doHash) dig.put(buf[0..rd]);
322 n -= rd;
326 void skipElement (char ch) {
327 if (ch == 'i') {
328 // skip until 'e'
329 do {
330 ch = getChar();
331 if (ch != 'e' && (ch < '0' || ch > '9')) throw new Exception("invalid torrent file element");
332 } while (ch != 'e');
333 } else if (ch == 'l') {
334 // list
335 for (;;) {
336 ch = getChar();
337 if (ch == 'e') break;
338 skipElement(ch);
340 } else if (ch == 'd') {
341 // dictionary
342 for (;;) {
343 ch = getChar();
344 if (ch == 'e') break;
345 if (ch < '0' || ch > '9') throw new Exception("invalid torrent file element");
346 // skip key name
347 auto n = getUInt(':', ch);
348 skipBytes(n);
349 // skip key value
350 ch = getChar();
351 skipElement(ch);
353 } else if (ch >= '0' && ch <= '9') {
354 auto n = getUInt(':', ch);
355 // skip string
356 skipBytes(n);
357 } else {
358 throw new Exception("invalid torrent file element");
362 // torrent file must be dictionary
363 char ch = getChar();
364 if (ch != 'd') throw new Exception("invalid torrent file: not a dictionary");
365 // now find "info" key
366 for (;;) {
367 ch = getChar();
368 if (ch < '0' || ch > '9') break;
369 // key name
370 auto n = getUInt(':', ch);
371 if (n == 4) {
372 char[4] buf;
373 st.rawReadExact(buf[]);
374 if (buf[] == "info") {
375 // wow! this is "info" section, hash it
376 doHash = true;
377 ch = getChar();
378 if (ch != 'd') throw new Exception("invalid torrent file: \"info\" section is not a dictionary");
379 skipElement(ch);
380 return dig.finish();
382 } else {
383 // skip name
384 skipBytes(n);
386 // skip data
387 ch = getChar();
388 skipElement(ch);
390 throw new Exception("invalid torrent file: no \"info\" section");