# Source: jala/code/BitTorrent.js

    //
    // 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();
    };