///|
fn create_hybrid_torrent(
files : Array[SourceFile],
options : CreateOptions,
read : (Int32, Int64, Int32) -> Bytes raise CrateError,
control : CreateControl?,
) -> Bytes raise CrateError {
let order = sorted_source_indices(files)
let v1_files : Array[BNode] = []
let pad_after : Array[Bool] = Array::make(files.length(), false)
for pos, index in order {
let file = files[index]
v1_files.push(v1_file_node(file))
let remainder = file.length % options.piece_length.to_int64()
if pos + 1 < order.length() && remainder != 0L {
let size = options.piece_length.to_int64() - remainder
pad_after[index] = true
v1_files.push(
bn_dict([
("attr", bn_text("p")),
("length", bn_int(size)),
("path", bn(Sequence([bn_text(".pad"), bn_text("\{size}-\{pos}")]))),
]),
)
}
}
let sha1 = new_sha1_pieces(options.piece_length)
// V2 reads in deterministic file order. Feed the very same real bytes into
// SHA-1 and synthesize alignment padding only after a non-final short file.
let (tree, layers) = v2_components(files, options, (index, offset, n) => {
let data = read_exact(read, index, offset, n)
push_sha1(sha1, data)
if offset + n.to_int64() == files[index].length && pad_after[index] {
flush_sha1(sha1, true)
}
data
})
flush_sha1(sha1, false)
let pieces = Bytes::from_array(sha1.hashes)
let fields = [
("name", bn_text(options.name)),
("piece length", bn_int(options.piece_length.to_int64())),
("pieces", bn(Blob(pieces))),
("meta version", bn_int(2L)),
("file tree", tree),
]
if options.single_file {
fields.push(("length", bn_int(files[0].length)))
} else {
fields.push(("files", bn(Sequence(v1_files))))
}
if options.private_torrent {
fields.push(("private", bn_int(1L)))
}
let top = top_fields(bn_dict(fields), options)
top.push(("piece layers", layers))
emit_creation_progress(control, "before_encode")
encode_bencode(bn_dict(top))
}
///|
fn validate_hybrid(
info : BNode,
files : Array[SourceFile],
single : Bool,
piece : Int32,
) -> (Bytes, Array[DataEntry]) raise CrateError {
let entries : Array[DataEntry] = []
let mut total = 0L
if single {
if lookup(info, "files") is Some(_) ||
files.length() != 1 ||
files[0].path != [as_text(require_key(info, "name"))] ||
as_int(require_key(info, "length")) != files[0].length {
reject("hybrid", "single-file layouts disagree")
}
total = files[0].length
entries.push({ source: 0, length: total, padding: false, })
} else {
let mut index = 0
let mut previous_padding = false
for f in as_list(require_key(info, "files")) {
let length = as_int(require_key(f, "length"))
if length < 0L || length > 9007199254740991L - total {
reject("limit", "invalid hybrid storage length")
}
let path : Array[String] = []
for p in as_list(require_key(f, "path")) {
path.push(as_text(p))
}
let attr = match lookup(f, "attr") {
Some(a) => as_text(a)
None => ""
}
if attr == "p" {
if index == 0 ||
previous_padding ||
length == 0L ||
length >= piece.to_int64() ||
total % piece.to_int64() + length != piece.to_int64() ||
path.length() != 2 ||
path[0] != ".pad" {
reject("hybrid", "invalid virtual padding alignment")
}
check_component(path[1])
entries.push({ source: -1, length, padding: true, })
previous_padding = true
} else {
if attr != "" ||
index >= files.length() ||
path != files[index].path ||
length != files[index].length ||
(length > 0L && total % piece.to_int64() != 0L) {
reject(
"hybrid", "v1/v2 paths order lengths or piece alignment disagree",
)
}
entries.push({ source: index, length, padding: false, })
index += 1
previous_padding = false
}
total += length
}
if index != files.length() {
reject("hybrid", "missing v1 files")
}
}
let pieces = as_blob(require_key(info, "pieces"))
if (total + piece.to_int64() - 1L) / piece.to_int64() > 400000L ||
pieces.length().to_int64() !=
(total + piece.to_int64() - 1L) / piece.to_int64() * 20L {
reject("hybrid", "v1 piece count does not match aligned storage")
}
(pieces, entries)
}