// // 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