712 lines
21 KiB
Haxe
712 lines
21 KiB
Haxe
package leenkx.network.torrent;
|
|
|
|
import haxe.io.Bytes;
|
|
|
|
typedef TorrentFileEntry = {
|
|
var path:String;
|
|
var length:Int;
|
|
var offset:Int;
|
|
@:optional var piecesRoot:Bytes;
|
|
}
|
|
|
|
class MetaInfo {
|
|
public var infoHash:Bytes;
|
|
public var infoHashHex:String;
|
|
public var infoHashV2:Bytes = null;
|
|
public var infoHashV2Hex:String = null;
|
|
public var metaVersion:Int = 1;
|
|
public var name:String;
|
|
public var pieceLength:Int;
|
|
public var pieces:Bytes;
|
|
public var files:Array<TorrentFileEntry>;
|
|
public var totalLength:Int = 0;
|
|
public var announce:Array<String>;
|
|
public var rawInfo:Bytes;
|
|
public var isPrivate:Bool = false;
|
|
public var comment:String = null;
|
|
public var createdBy:String = null;
|
|
public var creationDate:Int = 0;
|
|
public var urlList:Array<String> = null;
|
|
public var source:String = null;
|
|
public var fileHashes:Array<Array<Bytes>> = null;
|
|
public var storageLength:Int = 0;
|
|
public var pieceLayers:Array<{root:Bytes, hashes:Bytes}> = null;
|
|
|
|
public var numPieces(get, never):Int;
|
|
function get_numPieces():Int {
|
|
if (pieces != null && pieces.length > 0) {
|
|
return Std.int(pieces.length / TorrentInfo.HASH_LENGTH);
|
|
}
|
|
if (pieceLength > 0 && files != null) {
|
|
var end = 0;
|
|
for (f in files) {
|
|
var e = f.offset + f.length;
|
|
if (e > end) end = e;
|
|
}
|
|
return end > 0
|
|
? Std.int(Math.ceil(end / pieceLength)) : 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public var protocolVersion(get, never):Int;
|
|
function get_protocolVersion():Int {
|
|
return metaVersion == 2 ? (pieces != null ? 3 : 2) : 1;
|
|
}
|
|
|
|
public function new() {}
|
|
|
|
public static function fromTorrentBytes(data:Bytes):MetaInfo {
|
|
var dict:Dynamic = Bencode.decode(data);
|
|
if (dict == null || Reflect.field(dict, "info") == null) {
|
|
throw "MetaInfo: no info dict";
|
|
}
|
|
var m = new MetaInfo();
|
|
m.rawInfo = extractInfoBytes(data);
|
|
var info = Reflect.field(dict, "info");
|
|
m.parseInfo(info);
|
|
if (m.metaVersion == 2) {
|
|
m.infoHashV2 = haxe.crypto.Sha256.make(m.rawInfo);
|
|
m.infoHashV2Hex = Crypto.toHex(m.infoHashV2);
|
|
}
|
|
if (m.pieces != null) {
|
|
m.infoHash = haxe.crypto.Sha1.make(m.rawInfo);
|
|
m.infoHashHex = Crypto.toHex(m.infoHash);
|
|
}
|
|
m.announce = parseAnnounce(dict);
|
|
m.initFileHashes();
|
|
m.loadPieceLayers(data);
|
|
m.comment = bytesToString(Reflect.field(dict, "comment"));
|
|
m.createdBy = bytesToString(Reflect.field(dict, "created by"));
|
|
m.creationDate = Std.int(fieldFloat(dict, "creation date"));
|
|
var ul = Reflect.field(dict, "url-list");
|
|
if (Std.isOfType(ul, Array)) {
|
|
m.urlList = [for (u in (ul:Array<Dynamic>))
|
|
bytesToString(u)];
|
|
} else if (ul != null) {
|
|
m.urlList = [bytesToString(ul)];
|
|
}
|
|
return m;
|
|
}
|
|
|
|
function parseInfo(info:Dynamic):Void {
|
|
name = bytesToString(Reflect.field(info, "name"));
|
|
if (name == null) name = "";
|
|
pieceLength = Std.int(fieldFloat(info, "piece length"));
|
|
metaVersion = Std.int(fieldFloat(info, "meta version"));
|
|
if (metaVersion == 0) metaVersion = 1;
|
|
var piecesField = Reflect.field(info, "pieces");
|
|
if (piecesField == null) {
|
|
pieces = null;
|
|
} else {
|
|
pieces = Std.isOfType(piecesField, Bytes) ? piecesField
|
|
: Bytes.ofString(Std.string(piecesField));
|
|
}
|
|
isPrivate = fieldFloat(info, "private") == 1;
|
|
source = bytesToString(Reflect.field(info, "source"));
|
|
|
|
files = [];
|
|
totalLength = 0;
|
|
var fileTree = Reflect.field(info, "file tree");
|
|
if (fileTree != null) {
|
|
walkFileTree(fileTree, [name]);
|
|
if (pieces != null) {
|
|
applyV1Layout(Reflect.field(info, "files"));
|
|
}
|
|
storageLength = pieces == null
|
|
? treeCursor * pieceLength : totalLength;
|
|
return;
|
|
}
|
|
storageLength = 0;
|
|
var filesList:Array<Dynamic> = Reflect.field(info, "files");
|
|
if (filesList != null) {
|
|
for (f in filesList) {
|
|
var len = Std.int(fieldFloat(f, "length"));
|
|
var pathList:Array<Dynamic> = Reflect.field(f, "path");
|
|
var path = name;
|
|
if (pathList != null) {
|
|
var parts = [for (p in pathList) bytesToString(p)];
|
|
path = name + "/" + parts.join("/");
|
|
}
|
|
files.push({path: sanitizePath(path), length: len,
|
|
offset: totalLength});
|
|
totalLength += len;
|
|
}
|
|
} else {
|
|
var len = Std.int(fieldFloat(info, "length"));
|
|
files.push({path: sanitizePath(name), length: len,
|
|
offset: 0});
|
|
totalLength = len;
|
|
}
|
|
storageLength = totalLength;
|
|
}
|
|
|
|
public static function sanitizeComponent(c:String):String {
|
|
if (c == null || c == "" || c == "." || c == ".."
|
|
|| c.indexOf("/") >= 0 || c.indexOf("\\") >= 0
|
|
|| c.indexOf(":") >= 0
|
|
|| c.indexOf(String.fromCharCode(0)) >= 0) {
|
|
return "_";
|
|
}
|
|
return c;
|
|
}
|
|
|
|
public static function sanitizePath(p:String):String {
|
|
if (p == null) return "_";
|
|
var parts = p.split("/");
|
|
return [for (c in parts) sanitizeComponent(c)].join("/");
|
|
}
|
|
|
|
public function fileForPiece(index:Int):Int {
|
|
for (i in 0...files.length) {
|
|
var f = files[i];
|
|
var start = Std.int(f.offset / pieceLength);
|
|
var np = Std.int(Math.ceil(f.length / pieceLength));
|
|
if (index >= start && index < start + np) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
public function pieceSizeV2(index:Int):Int {
|
|
var fi = fileForPiece(index);
|
|
if (fi < 0) return 0;
|
|
var f = files[fi];
|
|
var start = Std.int(f.offset / pieceLength);
|
|
var rem = f.length - (index - start) * pieceLength;
|
|
return Std.int(Math.min(pieceLength, rem));
|
|
}
|
|
|
|
public function verifyPieceV2(index:Int, data:Bytes):Bool {
|
|
var fi = fileForPiece(index);
|
|
if (fi < 0 || fileHashes == null
|
|
|| fileHashes.length <= fi || fileHashes[fi] == null) {
|
|
return false;
|
|
}
|
|
var f = files[fi];
|
|
var start = Std.int(f.offset / pieceLength);
|
|
var expected = fileHashes[fi][index - start];
|
|
if (expected == null) return false;
|
|
var actual = f.length <= pieceLength
|
|
? merkleRoot(data) : pieceRootV2(data);
|
|
return memcmp(actual, expected);
|
|
}
|
|
|
|
public function pieceRootV2(data:Bytes):Bytes {
|
|
var base = Std.int(Math.log(pieceLength / 16384)
|
|
/ Math.log(2));
|
|
var pieceLeaves = 1 << base;
|
|
var leaves:Array<Bytes> = [];
|
|
var pos = 0;
|
|
while (pos < data.length) {
|
|
var n = Std.int(Math.min(16384, data.length - pos));
|
|
leaves.push(haxe.crypto.Sha256.make(data.sub(pos, n)));
|
|
pos += n;
|
|
}
|
|
var zero = leafZero();
|
|
while (leaves.length < pieceLeaves) leaves.push(zero);
|
|
return merkleReduce(leaves);
|
|
}
|
|
|
|
public function verifyLayer(fi:Int, hashes:Array<Bytes>):Bool {
|
|
if (fi < 0 || hashes == null) return false;
|
|
var root = files[fi].piecesRoot;
|
|
if (root == null) return false;
|
|
var base = Std.int(Math.log(pieceLength / 16384)
|
|
/ Math.log(2));
|
|
var level = hashes.copy();
|
|
var pow = 1;
|
|
while (pow < level.length) pow <<= 1;
|
|
var zero = layerZero(base);
|
|
while (level.length < pow) level.push(zero);
|
|
return memcmp(merkleReduce(level), root);
|
|
}
|
|
|
|
static var layerZeroCache:Map<Int, Bytes> = [];
|
|
public static function layerZero(level:Int):Bytes {
|
|
if (level == 0) return leafZero();
|
|
var half = layerZero(level - 1);
|
|
return hashPair(half, half);
|
|
}
|
|
|
|
public static function hashPair(a:Bytes, b:Bytes):Bytes {
|
|
var buf = new haxe.io.BytesBuffer();
|
|
buf.addBytes(a, 0, 32);
|
|
buf.addBytes(b, 0, 32);
|
|
return haxe.crypto.Sha256.make(buf.getBytes());
|
|
}
|
|
|
|
public static function merkleReduce(hashes:Array<Bytes>):Bytes {
|
|
var level = hashes;
|
|
while (level.length > 1) {
|
|
var next:Array<Bytes> = [];
|
|
for (i in 0...Std.int(level.length / 2)) {
|
|
next.push(hashPair(level[i * 2], level[i * 2 + 1]));
|
|
}
|
|
level = next;
|
|
}
|
|
return level.length > 0 ? level[0] : Bytes.alloc(32);
|
|
}
|
|
|
|
static public function memcmp(a:Bytes, b:Bytes):Bool {
|
|
if (a == null || b == null || a.length != b.length) return false;
|
|
for (i in 0...a.length) if (a.get(i) != b.get(i)) return false;
|
|
return true;
|
|
}
|
|
|
|
var treeCursor:Int = 0;
|
|
function walkFileTree(node:Dynamic, path:Array<String>):Void {
|
|
var aligned = pieces == null;
|
|
var keys = Reflect.fields(node);
|
|
keys.sort(Reflect.compare);
|
|
for (key in keys) {
|
|
var child = Reflect.field(node, key);
|
|
if (key == "") {
|
|
var len = Std.int(fieldFloat(child, "length"));
|
|
var p = path.length == 2 && path[0] == name
|
|
&& path[1] == name ? name : path.join("/");
|
|
var offset = aligned ? treeCursor * pieceLength
|
|
: totalLength;
|
|
files.push({path: sanitizePath(p), length: len,
|
|
offset: offset,
|
|
piecesRoot: toBytes(
|
|
Reflect.field(child, "pieces root"))});
|
|
if (aligned) {
|
|
treeCursor += Std.int(
|
|
Math.ceil(len / pieceLength));
|
|
}
|
|
totalLength += len;
|
|
} else {
|
|
var next = path.copy();
|
|
next.push(key);
|
|
walkFileTree(child, next);
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyV1Layout(filesList:Dynamic):Void {
|
|
if (!Std.isOfType(filesList, Array)) return;
|
|
var byPath = new Map<String, TorrentFileEntry>();
|
|
for (f in files) byPath.set(f.path, f);
|
|
var ordered:Array<TorrentFileEntry> = [];
|
|
var off = 0;
|
|
for (fl in (filesList:Array<Dynamic>)) {
|
|
var len = Std.int(fieldFloat(fl, "length"));
|
|
var pathList:Array<Dynamic> = Reflect.field(fl, "path");
|
|
var path = name;
|
|
if (pathList != null) {
|
|
var parts = [for (p in pathList) bytesToString(p)];
|
|
path = name + "/" + parts.join("/");
|
|
}
|
|
var e = byPath.get(sanitizePath(path));
|
|
if (e == null) {
|
|
e = {path: sanitizePath(path), length: len,
|
|
offset: off};
|
|
} else {
|
|
e.offset = off;
|
|
e.length = len;
|
|
}
|
|
off += len;
|
|
ordered.push(e);
|
|
}
|
|
files = ordered;
|
|
totalLength = off;
|
|
}
|
|
|
|
public function initFileHashes():Void {
|
|
if (metaVersion != 2 || files == null
|
|
|| pieceLength <= 0) return;
|
|
if (fileHashes == null) {
|
|
fileHashes = [for (f in files) null];
|
|
}
|
|
for (fi in 0...files.length) {
|
|
var f = files[fi];
|
|
if (f.length > 0 && f.length <= pieceLength
|
|
&& f.piecesRoot != null) {
|
|
fileHashes[fi] = [f.piecesRoot];
|
|
}
|
|
}
|
|
}
|
|
|
|
public function loadPieceLayers(data:Bytes):Void {
|
|
if (metaVersion != 2 || fileHashes == null) return;
|
|
var pos = findTopKey(data, "piece layers");
|
|
if (pos < 0 || data.get(pos) != 'd'.code) return;
|
|
var byRoot = new Map<String, Bytes>();
|
|
var p = pos + 1;
|
|
while (p < data.length && data.get(p) != 'e'.code) {
|
|
var key = rawString(data, p);
|
|
if (key == null) return;
|
|
p = skipValue(data, p);
|
|
var val = rawString(data, p);
|
|
if (val == null) return;
|
|
p = skipValue(data, p);
|
|
byRoot.set(Crypto.toHex(key), val);
|
|
}
|
|
for (fi in 0...files.length) {
|
|
var f = files[fi];
|
|
if (f.piecesRoot == null) continue;
|
|
var np = Std.int(Math.ceil(f.length / pieceLength));
|
|
if (np <= 1) continue;
|
|
var raw = byRoot.get(Crypto.toHex(f.piecesRoot));
|
|
if (raw == null || raw.length < np * 32) continue;
|
|
var hashes = [for (i in 0...np) raw.sub(i * 32, 32)];
|
|
if (verifyLayer(fi, hashes)) fileHashes[fi] = hashes;
|
|
}
|
|
}
|
|
|
|
static function rawString(data:Bytes, pos:Int):Bytes {
|
|
var b = data.get(pos);
|
|
if (b < '0'.code || b > '9'.code) return null;
|
|
var len = 0;
|
|
while (pos < data.length && data.get(pos) != ':'.code) {
|
|
len = len * 10 + (data.get(pos) - '0'.code);
|
|
pos++;
|
|
}
|
|
if (pos + 1 + len > data.length) return null;
|
|
return data.sub(pos + 1, len);
|
|
}
|
|
|
|
static function findTopKey(data:Bytes, want:String):Int {
|
|
if (data.length == 0 || data.get(0) != 'd'.code) return -1;
|
|
var pos = 1;
|
|
while (pos < data.length && data.get(pos) != 'e'.code) {
|
|
var keyEnd = skipValue(data, pos);
|
|
var key = data.sub(pos, keyEnd - pos).toString();
|
|
var colon = key.indexOf(":");
|
|
key = key.substr(colon + 1);
|
|
var valEnd = skipValue(data, keyEnd);
|
|
if (key == want) return keyEnd;
|
|
pos = valEnd;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static function parseAnnounce(dict:Dynamic):Array<String> {
|
|
var out:Array<String> = [];
|
|
var seen = new Map<String, Bool>();
|
|
var list:Array<Dynamic> = Reflect.field(dict, "announce-list");
|
|
if (list != null) {
|
|
for (tier in list) {
|
|
if (Std.isOfType(tier, Array)) {
|
|
for (u in (tier:Array<Dynamic>)) {
|
|
var s = bytesToString(u);
|
|
if (s != null && !seen.exists(s)) {
|
|
seen.set(s, true);
|
|
out.push(s);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var single = bytesToString(Reflect.field(dict, "announce"));
|
|
if (single != null && !seen.exists(single)) {
|
|
out.push(single);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
public static function fromInfoDict(rawInfo:Bytes, infoHash:Bytes, ?announce:Array<String>):MetaInfo {
|
|
var m = new MetaInfo();
|
|
m.rawInfo = rawInfo;
|
|
m.infoHash = infoHash;
|
|
m.infoHashHex = infoHash != null ? Crypto.toHex(infoHash) : null;
|
|
var infoDict = Bencode.decode(rawInfo);
|
|
m.parseInfo(infoDict);
|
|
if (m.metaVersion == 2) {
|
|
m.infoHashV2 = haxe.crypto.Sha256.make(rawInfo);
|
|
m.infoHashV2Hex = Crypto.toHex(m.infoHashV2);
|
|
}
|
|
m.announce = announce != null ? announce : [];
|
|
m.initFileHashes();
|
|
return m;
|
|
}
|
|
|
|
public static function create(name:String, files:Array<{path:String, data:Bytes}>, ?announce:Array<String>, ?opts:Dynamic):MetaInfo {
|
|
var m = new MetaInfo();
|
|
m.name = name;
|
|
m.announce = announce != null ? announce : [];
|
|
m.files = [];
|
|
var pieceLengthOpt = 0;
|
|
if (opts != null) {
|
|
m.isPrivate = fieldFloat(opts, "private") == 1
|
|
|| Reflect.field(opts, "private") == true;
|
|
m.source = Reflect.field(opts, "source");
|
|
m.comment = Reflect.field(opts, "comment");
|
|
m.createdBy = Reflect.field(opts, "createdBy");
|
|
var u:Dynamic = Reflect.field(opts, "urlList");
|
|
if (u != null) {
|
|
m.urlList = Std.isOfType(u, Array)
|
|
? (u:Array<String>) : [Std.string(u)];
|
|
}
|
|
pieceLengthOpt = Std.int(fieldFloat(opts, "pieceLength"));
|
|
}
|
|
|
|
var mvOpt = opts != null
|
|
? Std.int(fieldFloat(opts, "metaVersion")) : 1;
|
|
if (mvOpt < 1) mvOpt = 1;
|
|
var isV1 = mvOpt != 2;
|
|
var isV2 = mvOpt != 1;
|
|
m.metaVersion = isV2 ? 2 : 1;
|
|
|
|
var aligned = mvOpt == 2;
|
|
var total = 0;
|
|
for (f in files) total += f.data.length;
|
|
m.pieceLength = pieceLengthOpt > 0 ? pieceLengthOpt
|
|
: TorrentInfo.pieceLength(total);
|
|
|
|
var cursor = 0;
|
|
var content = new haxe.io.BytesBuffer();
|
|
for (f in files) {
|
|
var entry:TorrentFileEntry = {path: f.path,
|
|
length: f.data.length,
|
|
offset: aligned ? cursor * m.pieceLength
|
|
: m.totalLength};
|
|
if (isV2) entry.piecesRoot = merkleRoot(f.data);
|
|
m.files.push(entry);
|
|
m.totalLength += f.data.length;
|
|
cursor += Std.int(Math.ceil(f.data.length
|
|
/ m.pieceLength));
|
|
content.addBytes(f.data, 0, f.data.length);
|
|
}
|
|
var all = content.getBytes();
|
|
m.storageLength = aligned ? cursor * m.pieceLength
|
|
: m.totalLength;
|
|
m.pieces = isV1 ? TorrentInfo.pieces(all, m.pieceLength)
|
|
: null;
|
|
|
|
var info:Dynamic = {};
|
|
Reflect.setField(info, "name", name);
|
|
Reflect.setField(info, "piece length", m.pieceLength);
|
|
if (m.isPrivate) Reflect.setField(info, "private", 1);
|
|
if (m.source != null) Reflect.setField(info, "source", m.source);
|
|
if (isV1) {
|
|
Reflect.setField(info, "pieces", m.pieces);
|
|
if (files.length == 1) {
|
|
Reflect.setField(info, "length", files[0].data.length);
|
|
} else {
|
|
var fl:Array<Dynamic> = [];
|
|
for (f in files) {
|
|
var fd:Dynamic = {};
|
|
Reflect.setField(fd, "length", f.data.length);
|
|
var rel = f.path;
|
|
if (rel.indexOf(name + "/") == 0) {
|
|
rel = rel.substr(name.length + 1);
|
|
}
|
|
Reflect.setField(fd, "path", rel.split("/"));
|
|
fl.push(fd);
|
|
}
|
|
Reflect.setField(info, "files", fl);
|
|
}
|
|
}
|
|
if (isV2) {
|
|
Reflect.setField(info, "meta version", 2);
|
|
Reflect.setField(info, "file tree",
|
|
buildFileTree(name, files, m.files));
|
|
}
|
|
if (isV2) {
|
|
m.pieceLayers = [];
|
|
for (i in 0...files.length) {
|
|
var f = files[i];
|
|
var entry = m.files[i];
|
|
if (f.data.length <= m.pieceLength
|
|
|| entry.piecesRoot == null) continue;
|
|
var np = Std.int(Math.ceil(f.data.length
|
|
/ m.pieceLength));
|
|
var buf = new haxe.io.BytesBuffer();
|
|
for (p in 0...np) {
|
|
var off = p * m.pieceLength;
|
|
var sz = Std.int(Math.min(m.pieceLength,
|
|
f.data.length - off));
|
|
buf.addBytes(m.pieceRootV2(
|
|
f.data.sub(off, sz)), 0, 32);
|
|
}
|
|
m.pieceLayers.push({root: entry.piecesRoot,
|
|
hashes: buf.getBytes()});
|
|
}
|
|
if (m.pieceLayers.length == 0) m.pieceLayers = null;
|
|
}
|
|
m.rawInfo = Bencode.encode(info);
|
|
if (isV1) {
|
|
m.infoHash = haxe.crypto.Sha1.make(m.rawInfo);
|
|
m.infoHashHex = Crypto.toHex(m.infoHash);
|
|
}
|
|
if (isV2) {
|
|
m.infoHashV2 = haxe.crypto.Sha256.make(m.rawInfo);
|
|
m.infoHashV2Hex = Crypto.toHex(m.infoHashV2);
|
|
}
|
|
return m;
|
|
}
|
|
|
|
public static function merkleRoot(data:Bytes):Bytes {
|
|
if (data.length == 0) return Bytes.alloc(32);
|
|
var leaves:Array<Bytes> = [];
|
|
var pos = 0;
|
|
while (pos < data.length) {
|
|
var n = Std.int(Math.min(16384, data.length - pos));
|
|
leaves.push(haxe.crypto.Sha256.make(data.sub(pos, n)));
|
|
pos += n;
|
|
}
|
|
var pow = 1;
|
|
while (pow < leaves.length) pow <<= 1;
|
|
var zero = leafZero();
|
|
while (leaves.length < pow) leaves.push(zero);
|
|
return merkleReduce(leaves);
|
|
}
|
|
|
|
static var leafZeroCache:Bytes = null;
|
|
static function leafZero():Bytes {
|
|
if (leafZeroCache == null) leafZeroCache = Bytes.alloc(32);
|
|
return leafZeroCache;
|
|
}
|
|
|
|
static function buildFileTree(name:String, files:Array<{path:String, data:Bytes}>, entries:Array<TorrentFileEntry>):Dynamic {
|
|
var root:Dynamic = {};
|
|
for (i in 0...files.length) {
|
|
var rel = files[i].path;
|
|
if (rel.indexOf(name + "/") == 0) {
|
|
rel = rel.substr(name.length + 1);
|
|
}
|
|
var parts = rel.split("/");
|
|
var node = root;
|
|
for (p in 0...parts.length - 1) {
|
|
var nxt = Reflect.field(node, parts[p]);
|
|
if (nxt == null) {
|
|
nxt = {};
|
|
Reflect.setField(node, parts[p], nxt);
|
|
}
|
|
node = nxt;
|
|
}
|
|
var leaf:Dynamic = {};
|
|
Reflect.setField(leaf, "length", files[i].data.length);
|
|
Reflect.setField(leaf, "pieces root",
|
|
entries[i].piecesRoot);
|
|
var leafWrap:Dynamic = {};
|
|
Reflect.setField(leafWrap, "", leaf);
|
|
Reflect.setField(node, parts[parts.length - 1], leafWrap);
|
|
}
|
|
return root;
|
|
}
|
|
|
|
public function toTorrentBytes(?extra:Dynamic):Bytes {
|
|
var dict:Dynamic = extra != null ? extra : {};
|
|
Reflect.setField(dict, "info", Bencode.decode(rawInfo));
|
|
if (announce.length > 0) {
|
|
Reflect.setField(dict, "announce", announce[0]);
|
|
var tiers:Array<Dynamic> = [for (a in announce) [a]];
|
|
Reflect.setField(dict, "announce-list", tiers);
|
|
}
|
|
if (comment != null) Reflect.setField(dict, "comment", comment);
|
|
if (createdBy != null) {
|
|
Reflect.setField(dict, "created by", createdBy);
|
|
}
|
|
if (urlList != null && urlList.length > 0) {
|
|
Reflect.setField(dict, "url-list",
|
|
urlList.length == 1 ? (urlList[0]:Dynamic)
|
|
: [for (u in urlList) u]);
|
|
}
|
|
Reflect.setField(dict, "creation date",
|
|
creationDate > 0 ? creationDate
|
|
: Std.int(Date.now().getTime() / 1000));
|
|
if (pieceLayers != null && pieceLayers.length > 0) {
|
|
var pl = new Bencode.RawDict();
|
|
var sorted = pieceLayers.copy();
|
|
sorted.sort(function(a, b) {
|
|
var n = a.root.length < b.root.length
|
|
? a.root.length : b.root.length;
|
|
for (i in 0...n) {
|
|
var d = a.root.get(i) - b.root.get(i);
|
|
if (d != 0) return d;
|
|
}
|
|
return a.root.length - b.root.length;
|
|
});
|
|
for (e in sorted) {
|
|
pl.pairs.push({key: e.root, val: e.hashes});
|
|
}
|
|
Reflect.setField(dict, "piece layers", pl);
|
|
}
|
|
return Bencode.encode(dict);
|
|
}
|
|
|
|
public function pieceHash(index:Int):Bytes {
|
|
return pieces.sub(index * TorrentInfo.HASH_LENGTH,
|
|
TorrentInfo.HASH_LENGTH);
|
|
}
|
|
|
|
public function pieceSize(index:Int):Int {
|
|
if (pieces == null) return pieceSizeV2(index); // v2 aligned
|
|
if (index == numPieces - 1) {
|
|
var rem = totalLength % pieceLength;
|
|
return rem == 0 ? pieceLength : rem;
|
|
}
|
|
return pieceLength;
|
|
}
|
|
|
|
static function fieldFloat(obj:Dynamic, name:String):Float {
|
|
var v = Reflect.field(obj, name);
|
|
if (v == null) return 0;
|
|
if (Std.isOfType(v, Float) || Std.isOfType(v, Int)) return v;
|
|
return Std.parseFloat(Std.string(v));
|
|
}
|
|
|
|
static function bytesToString(v:Dynamic):String {
|
|
if (v == null) return null;
|
|
if (Std.isOfType(v, String)) return v;
|
|
if (Std.isOfType(v, Float) || Std.isOfType(v, Int)) {
|
|
return Std.string(v);
|
|
}
|
|
if (Std.isOfType(v, Bytes)) return (v:Bytes).toString();
|
|
return Std.string(v);
|
|
}
|
|
|
|
public static function toBytes(v:Dynamic):Bytes {
|
|
if (v == null) return null;
|
|
if (Std.isOfType(v, Float) || Std.isOfType(v, Int)
|
|
|| Std.isOfType(v, String) || Std.isOfType(v, Array)) {
|
|
return null;
|
|
}
|
|
if (Std.isOfType(v, Bytes)) return (v:Bytes);
|
|
return null;
|
|
}
|
|
|
|
static function extractInfoBytes(data:Bytes):Bytes {
|
|
var pos = 0;
|
|
if (data.get(pos) != 'd'.code) throw "MetaInfo: not a dict";
|
|
pos++;
|
|
while (pos < data.length && data.get(pos) != 'e'.code) {
|
|
var keyEnd = skipValue(data, pos);
|
|
var key = data.sub(pos, keyEnd - pos).toString();
|
|
var colon = key.indexOf(":");
|
|
key = key.substr(colon + 1);
|
|
var valStart = keyEnd;
|
|
var valEnd = skipValue(data, valStart);
|
|
if (key == "info") {
|
|
return data.sub(valStart, valEnd - valStart);
|
|
}
|
|
pos = valEnd;
|
|
}
|
|
throw "MetaInfo: info key not found";
|
|
}
|
|
|
|
static public function skipValue(data:Bytes, pos:Int):Int {
|
|
var b = data.get(pos);
|
|
if (b == 'i'.code) {
|
|
pos++;
|
|
while (pos < data.length && data.get(pos) != 'e'.code) pos++;
|
|
return pos + 1;
|
|
}
|
|
if (b == 'l'.code || b == 'd'.code) {
|
|
pos++;
|
|
while (pos < data.length && data.get(pos) != 'e'.code) {
|
|
pos = skipValue(data, pos);
|
|
}
|
|
return pos + 1;
|
|
}
|
|
var len = 0;
|
|
while (pos < data.length && data.get(pos) != ':'.code) {
|
|
len = len * 10 + (data.get(pos) - '0'.code);
|
|
pos++;
|
|
}
|
|
return pos + 1 + len;
|
|
}
|
|
}
|