429 lines
11 KiB
JavaScript
429 lines
11 KiB
JavaScript
//
|
|
// Jala Project [http://opensvn.csie.org/traccgi/jala]
|
|
//
|
|
// Copyright 2004 ORF Online und Teletext GmbH
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the ``License'');
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an ``AS IS'' BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
// $Revision$
|
|
// $LastChangedBy$
|
|
// $LastChangedDate$
|
|
// $HeadURL$
|
|
//
|
|
|
|
|
|
/**
|
|
* @fileoverview Fields and methods of the jala.BitTorrent class.
|
|
*/
|
|
|
|
|
|
// Define the global namespace for Jala modules
|
|
if (!global.jala) {
|
|
global.jala = {};
|
|
}
|
|
|
|
|
|
/**
|
|
* Module dependencies
|
|
*/
|
|
app.addRepository("modules/core/String.js");
|
|
app.addRepository("modules/helma/File.js");
|
|
|
|
|
|
/**
|
|
* Constructs a new BitTorrent file.
|
|
* @class This class provides methods to create a BitTorrent
|
|
* metadata file from any desired file.
|
|
* @param {String} trackerUrl The URL string of the tracker.
|
|
* @param {String} filePath The path to the original file.
|
|
* @returns A new BitTorrent file.
|
|
* @constructor
|
|
*/
|
|
jala.BitTorrent = function(filePath, trackerUrl) {
|
|
var self = this;
|
|
self.arguments = arguments;
|
|
|
|
// FIXME: Add support for multitracker mode as specified in
|
|
// http://www.bittornado.com/docs/multitracker-spec.txt
|
|
|
|
var torrent, sourceFile, torrentFile;
|
|
var pieceLength = 256;
|
|
|
|
var updateTorrent = function() {
|
|
if (torrent.info) {
|
|
return torrent;
|
|
}
|
|
|
|
var file = new java.io.File(filePath);
|
|
if (!file.exists()) {
|
|
throw Error("File " + file + " does not exist!");
|
|
}
|
|
|
|
var md5 = java.security.MessageDigest.getInstance("MD5");
|
|
var sha1 = java.security.MessageDigest.getInstance("SHA-1");
|
|
|
|
var fis = new java.io.FileInputStream(file);
|
|
var bis = new java.io.BufferedInputStream(fis);
|
|
var cache = new java.io.ByteArrayOutputStream();
|
|
|
|
var pieces = [];
|
|
var length = pieceLength * 1024;
|
|
var buffer = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, length);
|
|
|
|
while (bis.read(buffer, 0, buffer.length) > -1) {
|
|
app.debug("Updating SHA-1 hash with " + buffer.length + " bytes");
|
|
sha1.reset();
|
|
sha1["update(byte[])"](buffer);
|
|
cache["write(byte[])"](buffer);
|
|
pieces.push(new java.lang.String(sha1.digest()));
|
|
}
|
|
|
|
bis.close();
|
|
fis.close();
|
|
|
|
torrent.info = {
|
|
//md5sum: new java.lang.String(md5.digest(cache.toByteArray())),
|
|
length: cache.size(),
|
|
name: file.getName(),
|
|
"piece length": length,
|
|
pieces: pieces.join("")
|
|
};
|
|
|
|
return torrent;
|
|
};
|
|
|
|
/**
|
|
* Get all available property names.
|
|
* @returns The list of property names.
|
|
* @type Array
|
|
*/
|
|
this.keys = function() {
|
|
var keys = [];
|
|
for (var i in torrent) {
|
|
keys.push(i);
|
|
}
|
|
keys.sort();
|
|
return keys;
|
|
};
|
|
|
|
/**
|
|
* Get a torrent property.
|
|
* @param {String} name The name of the property.
|
|
* @returns The value of the property.
|
|
*/
|
|
this.get = function(name) {
|
|
return torrent[name];
|
|
};
|
|
|
|
/**
|
|
* Set a torrent property.
|
|
* @param {String} name The name of the property.
|
|
* @param {Object} value The property's value.
|
|
*/
|
|
this.set = function(name, value) {
|
|
if (typeof torrent[name] == "undefined") {
|
|
throw Error("Cannot set torrent property " + name);
|
|
}
|
|
torrent[name] = value;
|
|
delete torrent.info;
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* Get the creation date of the torrent.
|
|
* @returns The torrent's creation date.
|
|
* @type Date
|
|
*/
|
|
this.getCreationDate = function() {
|
|
return new Date(torrent["creation date"] * 1000);
|
|
};
|
|
|
|
/**
|
|
* Set the creation date of the torrent.
|
|
* @param {Date} date The desired creation date.
|
|
*/
|
|
this.setCreationDate = function(date) {
|
|
this.set("creation date", Math.round((date || new Date()).getTime() / 1000));
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* Get the piece length of the torrent.
|
|
* @returns The torrent's piece length.
|
|
* @type Number
|
|
*/
|
|
this.getPieceLength = function() {
|
|
return pieceLength;
|
|
};
|
|
|
|
/**
|
|
* Set the piece length of the torrent.
|
|
* @param {Number} length The desired piece length.
|
|
*/
|
|
this.setPieceLength = function(length) {
|
|
pieceLength = length;
|
|
delete torrent.info;
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* Returns the underlying torrent file.
|
|
* @returns The torrent file.
|
|
* @type helma.File
|
|
*/
|
|
this.getTorrentFile = function() {
|
|
return torrentFile;
|
|
};
|
|
|
|
/**
|
|
* Returns the underlying source file.
|
|
* @returns The source file.
|
|
* @type helma.File
|
|
*/
|
|
this.getSourceFile = function() {
|
|
return sourceFile;
|
|
};
|
|
|
|
/**
|
|
* Saves the torrent as file.
|
|
* @param {String} filename An optional name for the torrent file.
|
|
* If no name is given it will be composed from name of source
|
|
* file as defined in the torrent plus the ending ".torrent".
|
|
*/
|
|
this.save = function(filename) {
|
|
updateTorrent();
|
|
if (!filename) {
|
|
filename = torrent.info.name + ".torrent";
|
|
}
|
|
torrentFile = new helma.File(sourceFile.getParent(), filename);
|
|
torrentFile.remove();
|
|
torrentFile.open();
|
|
torrentFile.write(jala.BitTorrent.bencode(torrent));
|
|
torrentFile.close();
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* Get a string representation of the torrent.
|
|
* @returns The torrent as string.
|
|
* @type String
|
|
*/
|
|
this.toString = function() {
|
|
return "[jala.BitTorrent " + filePath + "]";
|
|
};
|
|
|
|
if (String(filePath).endsWith(".torrent")) {
|
|
torrentFile = new helma.File(filePath);
|
|
torrent = jala.BitTorrent.bdecode(torrentFile.readAll());
|
|
sourceFile = new helma.File(torrent.info.name);
|
|
} else {
|
|
torrent = {
|
|
announce: trackerUrl || null,
|
|
"announce-list": null,
|
|
"creation date": null,
|
|
comment: null,
|
|
"created by": null,
|
|
};
|
|
this.setCreationDate();
|
|
sourceFile = new helma.File(filePath);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* The bencode method. Turns an arbitrary JavaScript
|
|
* object structure into a corresponding encoded
|
|
* string.
|
|
* @param {Object} obj The target JavaScript object.
|
|
* @returns The encoded string.
|
|
* @type String
|
|
*/
|
|
jala.BitTorrent.bencode = function(obj) {
|
|
var bencode = arguments.callee;
|
|
var str = obj.toString();
|
|
res.push();
|
|
switch (obj.constructor) {
|
|
case Array:
|
|
res.write("l");
|
|
for (var i in obj) {
|
|
if (obj[i])
|
|
res.write(bencode(obj[i]));
|
|
}
|
|
res.write("e");
|
|
break;
|
|
|
|
case Number:
|
|
res.write("i" + str + "e");
|
|
break;
|
|
|
|
case Object:
|
|
res.write("d");
|
|
var keys = [];
|
|
for (var i in obj) {
|
|
keys.push(i);
|
|
}
|
|
keys.sort();
|
|
var key;
|
|
for (var i in keys) {
|
|
key = keys[i];
|
|
if (obj[key]) {
|
|
res.write(bencode(key));
|
|
res.write(bencode(obj[key]));
|
|
}
|
|
}
|
|
res.write("e");
|
|
break;
|
|
|
|
default:
|
|
res.write(str.length + ":" + str);
|
|
}
|
|
return res.pop();
|
|
};
|
|
|
|
|
|
/**
|
|
* The bdecode method. Turns an encoded string into
|
|
* a corresponding JavaScript object structure.
|
|
* FIXME: Handle with caution...
|
|
* @param {String} code The encoded string.
|
|
* @returns The decoded JavaScript structure.
|
|
* @type Object
|
|
*/
|
|
jala.BitTorrent.bdecode = function(code) {
|
|
var DICTIONARY = "d";
|
|
var LIST = "l";
|
|
var INTEGER = "i";
|
|
var STRING = "s";
|
|
var END = "e";
|
|
|
|
var stack = [];
|
|
var overflowCounter = 0;
|
|
|
|
var position = -1, current;
|
|
|
|
function getResult() {
|
|
update();
|
|
var result;
|
|
switch (current) {
|
|
case DICTIONARY:
|
|
result = bdecodeDictionary();
|
|
break;
|
|
case LIST:
|
|
result = bdecodeList();
|
|
break;
|
|
case INTEGER:
|
|
result = bdecodeInteger();
|
|
break;
|
|
case END:
|
|
case null:
|
|
//res.debug("*** end detected in getResult()");
|
|
result = null;
|
|
break;
|
|
default:
|
|
result = bdecodeString();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function update() {
|
|
position += 1;
|
|
current = code.charAt(position);
|
|
/* res.debug("stack: " + stack);
|
|
res.debug("position: " + position);
|
|
res.debug("current: " + current);
|
|
res.debug("remains: " + code.substr(position)); */
|
|
return;
|
|
}
|
|
|
|
function overflow() {
|
|
if (overflowCounter++ > 100)
|
|
throw Error("Error parsing bdecoded string");
|
|
return false;
|
|
}
|
|
|
|
function bdecodeDictionary() {
|
|
stack.push(DICTIONARY);
|
|
var dictionary = {}, key, value;
|
|
while (current && !overflow()) {
|
|
key = getResult();
|
|
if (key === null)
|
|
break;
|
|
value = getResult();
|
|
if (key && value)
|
|
dictionary[key] = value;
|
|
else
|
|
break;
|
|
}
|
|
stack.pop();
|
|
return dictionary;
|
|
}
|
|
|
|
function bdecodeList() {
|
|
stack.push(LIST);
|
|
var list = [], value;
|
|
while (current && !overflow()) {
|
|
var value = getResult();
|
|
if (value)
|
|
list.push(value);
|
|
else
|
|
break;
|
|
}
|
|
stack.pop();
|
|
return list;
|
|
}
|
|
|
|
function bdecodeInteger() {
|
|
var integer = "";
|
|
stack.push(integer);
|
|
while (current && !overflow()) {
|
|
update();
|
|
if (current == "e")
|
|
break;
|
|
integer += current;
|
|
}
|
|
if (isNaN(integer))
|
|
throw("Error in bdecoded integer: " + integer + " is not a number");
|
|
//res.debug("integer = " + integer);
|
|
stack.pop();
|
|
return parseInt(integer);
|
|
}
|
|
|
|
function bdecodeString() {
|
|
var length = current, string = "";
|
|
stack.push(string);
|
|
update();
|
|
while (current && current != ":" && !overflow()) {
|
|
length += current;
|
|
update();
|
|
}
|
|
if (isNaN(length))
|
|
throw("Error in bdecoded string: invalid length " + length);
|
|
//res.debug("length = " + length);
|
|
length = parseInt(length);
|
|
if (length > code.length - position)
|
|
throw Error("Error parsing bdecoded string");
|
|
for (var i=0; i<length; i+=1) {
|
|
update();
|
|
string += current;
|
|
}
|
|
//res.debug("string = " + string);
|
|
if (string == "creation date")
|
|
void(null);
|
|
stack.pop();
|
|
return string;
|
|
}
|
|
|
|
return getResult();
|
|
};
|