/* * 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. * * Revised by Stephen L. Reed, Dec 22, 2009. * Reformatted, fixed Checkstyle, Findbugs and PMD violations, and substituted Log4J logger * for consistency with the Texai project. */ package org.texai.torrent.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 inputStream; /** 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 lastIndicator = 0; /** the special map which is used for ugly hack to get SHA hash over the metainfo info map */ private String specialMap = "info"; /** the indicator whether we are using the special map */ private boolean isSpecialMap = false; /** the message digest */ private final MessageDigest shaDigest; /** Return the SHA has over bytes that make up the special map. * * @return the SHA has over bytes that make up the special map */ public byte[] get_special_map_digest() { return shaDigest.digest(); } /** Sets the special map name. Name defaults to "info". * * @param name the special map name */ public void set_special_map_name(final String name) { specialMap = name; } /** Initalizes a new BDecoder. Nothing is read from the given * <code>InputStream</code> yet. * * @param inputStream the input stream */ public BDecoder(final InputStream inputStream) { this.inputStream = inputStream; // XXX - Used for ugly hack. try { shaDigest = MessageDigest.getInstance("SHA"); } catch (NoSuchAlgorithmException nsa) { throw new InternalError(nsa.toString()); // NOPMD } } /** Creates a new BDecoder and immediately decodes the first value it sees. * * @param inputStream the input stream * @return the first BEValue on the stream or null when the stream has ended * * @exception IOException when somthing bad happens with the stream to read from. */ public static BEValue bdecode(final InputStream inputStream) throws IOException { return new BDecoder(inputStream).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. * * @return what the next bencoded object will be on the stream or -1 when * the end of stream has been reached * @throws IOException when an input/output error occurs */ public int getNextIndicator() throws IOException { if (lastIndicator == 0) { lastIndicator = inputStream.read(); // XXX - Used for ugly hack if (isSpecialMap) { shaDigest.update((byte) lastIndicator); } } return lastIndicator; } /** 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. * * @return the appropriate BEValue encoded object or null when the stream has ended * @throws IOException when an input/output error occurs */ public BEValue bdecode() throws IOException { lastIndicator = getNextIndicator(); if (lastIndicator == -1) { return null; } if (lastIndicator >= '0' && lastIndicator <= '9') { return bdecodeBytes(); } else if (lastIndicator == 'i') { return bdecodeNumber(); } else if (lastIndicator == 'l') { return bdecodeList(); } else if (lastIndicator == 'd') { return bdecodeMap(); } else { throw new InvalidBEncodingException("Unknown indicator '" + lastIndicator + "'"); } } /** 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. * * @return the next bencoded value on the stream * @throws IOException when the next bencoded value is not a byte array */ public BEValue bdecodeBytes() throws IOException { int indicator = getNextIndicator(); int num = indicator - '0'; if (num < 0 || num > 9) { throw new InvalidBEncodingException("Number expected, not '" + (char) indicator + "'"); } lastIndicator = 0; indicator = read(); int index = indicator - '0'; while (index >= 0 && index <= 9) { // XXX - This can overflow! num = num * 10 + index; indicator = read(); index = indicator - '0'; } if (indicator != ':') { throw new InvalidBEncodingException("Colon expected, not '" + (char) indicator + "'"); } 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. * * @return the next bencoded value on the stream * @throws IOException when the next bencoded value on the stream is not a number */ public BEValue bdecodeNumber() throws IOException { int indicator = getNextIndicator(); if (indicator != 'i') { throw new InvalidBEncodingException("Expected 'i', not '" + (char) indicator + "'"); } lastIndicator = 0; indicator = read(); if (indicator == '0') { indicator = read(); if (indicator == 'e') { return new BEValue(BigInteger.ZERO); } else { throw new InvalidBEncodingException("'e' expected after zero," + " not '" + (char) indicator + "'"); } } // XXX - We don't support more the 255 char big integers final char[] chars = new char[256]; int off = 0; if (indicator == '-') { indicator = read(); if (indicator == '0') { throw new InvalidBEncodingException("Negative zero not allowed"); } chars[off] = (char) indicator; off++; } if (indicator < '1' || indicator > '9') { throw new InvalidBEncodingException("Invalid Integer start '" + (char) indicator + "'"); } chars[off] = (char) indicator; off++; indicator = read(); int index = indicator - '0'; while (index >= 0 && index <= 9) { chars[off] = (char) indicator; off++; indicator = read(); index = indicator - '0'; } if (indicator != 'e') { throw new InvalidBEncodingException("Integer should end with 'e'"); } return new BEValue(new BigInteger(new String(chars, 0, off))); } /** 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. * * @return the next bencoded value on the stream * @throws IOException when the next bencoded value on the stream is not a list */ public BEValue bdecodeList() throws IOException { int indicator = getNextIndicator(); if (indicator != 'l') { throw new InvalidBEncodingException("Expected 'l', not '" + (char) indicator + "'"); } lastIndicator = 0; final List<BEValue> result = new ArrayList<>(); indicator = getNextIndicator(); while (indicator != 'e') { result.add(bdecode()); indicator = getNextIndicator(); } lastIndicator = 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. * * @return the next bencoded value on the stream * @throws IOException when the next bencoded value on the stream is not a map (String --> BEValue) */ public BEValue bdecodeMap() throws IOException { int indicator = getNextIndicator(); if (indicator != 'd') { throw new InvalidBEncodingException("Expected 'd', not '" + (char) indicator + "'"); } lastIndicator = 0; final Map<String, BEValue> result = new HashMap<>(); indicator = getNextIndicator(); while (indicator != 'e') { // Dictonary keys are always strings. final String key = bdecode().getString(); // XXX ugly hack final boolean special = specialMap.equals(key); if (special) { isSpecialMap = true; } final BEValue value = bdecode(); result.put(key, value); // XXX ugly hack continued if (special) { isSpecialMap = false; } indicator = getNextIndicator(); } lastIndicator = 0; return new BEValue(result); } /** Returns the next byte read from the InputStream (as int). Throws * EOFException if InputStream.read() returned -1. * * @return the next byte read * @throws IOException when an input/output error occurs */ private int read() throws IOException { final int byte1 = inputStream.read(); if (byte1 == -1) { throw new EOFException(); } if (isSpecialMap) { shaDigest.update((byte) byte1); } return byte1; } /** 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. * * @param length the length of the bytes to be read * @return a byte[] containing length valid bytes starting at offset zero * @throws IOException when an input/output error occurs */ private byte[] read(final int length) throws IOException { final byte[] result = new byte[length]; int read = 0; while (read < length) { final int index = inputStream.read(result, read, length - read); if (index == -1) { throw new EOFException(); } read += index; } if (isSpecialMap) { shaDigest.update(result, 0, length); } return result; } }