/* * BDecoder - Converts an InputStream to BEValues. Copyright (C) 2003 Mark J. * Wielaard * * This file is part of Snark. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 59 Temple * Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.klomp.snark.bencode; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Decodes a bencoded stream to <code>BEValue</code>s. * * A bencoded byte stream can represent byte arrays, numbers, lists and maps * (dictionaries). * * It currently contains a hack to indicate a name of a dictionary of which a * SHA-1 digest hash should be calculated (the hash over the original bencoded * bytes). * * @author Mark Wielaard (mark@klomp.org). */ public class BDecoder { // The InputStream to BDecode. private final InputStream in; // The last indicator read. // Zero if unknown. // '0'..'9' indicates a byte[]. // 'i' indicates an Number. // 'l' indicates a List. // 'd' indicates a Map. // 'e' indicates end of Number, List or Map (only used internally). // -1 indicates end of stream. // Call getNextIndicator to get the current value (will never return zero). private int indicator = 0; // Used for ugly hack to get SHA hash over the metainfo info map private String special_map = "info"; private boolean in_special_map = false; private final MessageDigest sha_digest; // Ugly hack. Return the SHA has over bytes that make up the special map. public byte[] get_special_map_digest () { byte[] result = sha_digest.digest(); return result; } // Ugly hack. Name defaults to "info". public void set_special_map_name (String name) { special_map = name; } /** * Initalizes a new BDecoder. Nothing is read from the given * <code>InputStream</code> yet. */ public BDecoder (InputStream in) { this.in = in; // XXX - Used for ugly hack. try { sha_digest = MessageDigest.getInstance("SHA"); } catch (NoSuchAlgorithmException nsa) { throw new InternalError(nsa.toString()); } } /** * Creates a new BDecoder and immediatly decodes the first value it sees. * * @return The first BEValue on the stream or null when the stream has * ended. * * @exception InvalidBEncoding * when the stream doesn't start with a bencoded value or the * stream isn't a bencoded stream at all. * @exception IOException * when somthing bad happens with the stream to read from. */ public static BEValue bdecode (InputStream in) throws IOException { return new BDecoder(in).bdecode(); } /** * Returns what the next bencoded object will be on the stream or -1 when * the end of stream has been reached. Can return something unexpected (not * '0' .. '9', 'i', 'l' or 'd') when the stream isn't bencoded. * * This might or might not read one extra byte from the stream. */ public int getNextIndicator () throws IOException { if (indicator == 0) { indicator = in.read(); // XXX - Used for ugly hack if (in_special_map) { sha_digest.update((byte)indicator); } } return indicator; } /** * Gets the next indicator and returns either null when the stream has ended * or bdecodes the rest of the stream and returns the appropriate BEValue * encoded object. */ public BEValue bdecode () throws IOException { indicator = getNextIndicator(); if (indicator == -1) { return null; } if (indicator >= '0' && indicator <= '9') { return bdecodeBytes(); } else if (indicator == 'i') { return bdecodeNumber(); } else if (indicator == 'l') { return bdecodeList(); } else if (indicator == 'd') { return bdecodeMap(); } else { throw new InvalidBEncodingException("Unknown indicator '" + indicator + "'"); } } /** * Returns the next bencoded value on the stream and makes sure it is a byte * array. If it is not a bencoded byte array it will throw * InvalidBEncodingException. */ public BEValue bdecodeBytes () throws IOException { int c = getNextIndicator(); int num = c - '0'; if (num < 0 || num > 9) { throw new InvalidBEncodingException("Number expected, not '" + (char)c + "'"); } indicator = 0; c = read(); int i = c - '0'; while (i >= 0 && i <= 9) { // XXX - This can overflow! num = num * 10 + i; c = read(); i = c - '0'; } if (c != ':') { throw new InvalidBEncodingException("Colon expected, not '" + (char)c + "'"); } return new BEValue(read(num)); } /** * Returns the next bencoded value on the stream and makes sure it is a * number. If it is not a number it will throw InvalidBEncodingException. */ public BEValue bdecodeNumber () throws IOException { int c = getNextIndicator(); if (c != 'i') { throw new InvalidBEncodingException("Expected 'i', not '" + (char)c + "'"); } indicator = 0; c = read(); if (c == '0') { c = read(); if (c == 'e') { return new BEValue(BigInteger.ZERO); } else { throw new InvalidBEncodingException("'e' expected after zero," + " not '" + (char)c + "'"); } } // XXX - We don't support more the 255 char big integers char[] chars = new char[256]; int off = 0; if (c == '-') { c = read(); if (c == '0') { throw new InvalidBEncodingException("Negative zero not allowed"); } chars[off] = (char)c; off++; } if (c < '1' || c > '9') { throw new InvalidBEncodingException("Invalid Integer start '" + (char)c + "'"); } chars[off] = (char)c; off++; c = read(); int i = c - '0'; while (i >= 0 && i <= 9) { chars[off] = (char)c; off++; c = read(); i = c - '0'; } if (c != 'e') { throw new InvalidBEncodingException("Integer should end with 'e'"); } String s = new String(chars, 0, off); return new BEValue(new BigInteger(s)); } /** * Returns the next bencoded value on the stream and makes sure it is a * list. If it is not a list it will throw InvalidBEncodingException. */ public BEValue bdecodeList () throws IOException { int c = getNextIndicator(); if (c != 'l') { throw new InvalidBEncodingException("Expected 'l', not '" + (char)c + "'"); } indicator = 0; List<BEValue> result = new ArrayList<BEValue>(); c = getNextIndicator(); while (c != 'e') { result.add(bdecode()); c = getNextIndicator(); } indicator = 0; return new BEValue(result); } /** * Returns the next bencoded value on the stream and makes sure it is a map * (dictonary). If it is not a map it will throw InvalidBEncodingException. */ public BEValue bdecodeMap () throws IOException { int c = getNextIndicator(); if (c != 'd') { throw new InvalidBEncodingException("Expected 'd', not '" + (char)c + "'"); } indicator = 0; Map<String, BEValue> result = new HashMap<String, BEValue>(); c = getNextIndicator(); while (c != 'e') { // Dictonary keys are always strings. String key = bdecode().getString(); // XXX ugly hack boolean special = special_map.equals(key); if (special) { in_special_map = true; } BEValue value = bdecode(); result.put(key, value); // XXX ugly hack continued if (special) { in_special_map = false; } c = getNextIndicator(); } indicator = 0; return new BEValue(result); } /** * Returns the next byte read from the InputStream (as int). Throws * EOFException if InputStream.read() returned -1. */ private int read () throws IOException { int c = in.read(); if (c == -1) { throw new EOFException(); } if (in_special_map) { sha_digest.update((byte)c); } return c; } /** * Returns a byte[] containing length valid bytes starting at offset zero. * Throws EOFException if InputStream.read() returned -1 before all * requested bytes could be read. Note that the byte[] returned might be * bigger then requested but will only contain length valid bytes. The * returned byte[] will be reused when this method is called again. */ private byte[] read (int length) throws IOException { byte[] result = new byte[length]; int read = 0; while (read < length) { int i = in.read(result, read, length - read); if (i == -1) { throw new EOFException(); } read += i; } if (in_special_map) { sha_digest.update(result, 0, length); } return result; } }