Android BT种子文件解析

来源:互联网 发布:冲压模具报价软件 编辑:程序博客网 时间:2024/04/28 21:55

对种子文件进行解析需要先了解一下种子文件的格式, 然后根据格式进行对应的解析.

一. BT种子文件格式

这里只是简单的介绍一下, 具体信息可以自行google.
BT种子文件使用了一种叫bencoding的编码方法来保存数据。
bencoding现有四种类型的数据:srings(字符串),integers(整数),lists(列表),dictionaries(字典)

字符串:
字符串被如此编码:<字符串长度>:字符串正文.这种表示法没有任何的分界符.
例子:如”8:announce”指”announce”.

整数:
整数被如此编码: i整数值e. 可以为负数,如’i-3e’
例子:’i3e’ 指 3.

列表:
列表是如此被表示的:

<l>Bencode Value<e>

列表可以用来表示多个对象.
列表内容可以包括字符串,整数,字典,甚至列表本身.
例子:’l4:spam4:eggse’ 指 [ “spam”, eggs” ]

字典:
字典是一个一对一的映射.它表示了一个主键(必须为字符串)和一个数据项(可以为任何Bencode值)的关系.字典可以用来表示一个对象的多种属性.
字典是如此被编码:

 <d><bencoded string><bencoded element><e>

注意:字典必须根据主键预排序.

二. 对BT文件进行解析
1.首先创建一个对象BtValue来保存bt文件的四种类型的数据

public class BtValue {    //mValue可以是String, int, list或Map    private final Object mValue;    public BtValue(byte[] mValue) {        this.mValue = mValue;    }    public BtValue(String mValue) throws UnsupportedEncodingException {        this.mValue = mValue.getBytes("UTF-8");    }    public BtValue(String mValue, String enc) throws UnsupportedEncodingException {        this.mValue = mValue.getBytes(enc);    }    public BtValue(int mValue) {        this.mValue = mValue;    }    public BtValue(long mValue) {        this.mValue = mValue;    }    public BtValue(Number mValue) {        this.mValue = mValue;    }    public BtValue(List<BtValue> mValue) {        this.mValue = mValue;    }    public BtValue(Map<String, BtValue> mValue) {        this.mValue = mValue;    }    public Object getValue() {        return this.mValue;    }    /**     * 将BtValue作为String返回, 使用UTF-8进行编码     */    public String getString() throws InvalidBtEncodingException {        return getString("UTF-8");    }    public String getString(String encoding) throws InvalidBtEncodingException {        try {            return new String(getBytes(), encoding);        } catch (ClassCastException e) {            throw new InvalidBtEncodingException(e.toString());        } catch (UnsupportedEncodingException e) {            throw new InternalError(e.toString());        }    }    /**     * 将Btvalue对象作为byte[]数组返回     */    public byte[] getBytes() throws InvalidBtEncodingException {        try {            return (byte[])mValue;        } catch (ClassCastException e) {            throw new InvalidBtEncodingException(e.toString());        }    }    /**     * 将BtValue对象作为数字返回     */    public Number getNumber() throws InvalidBtEncodingException {        try {            return (Number)mValue;        } catch (ClassCastException e) {            throw new InvalidBtEncodingException(e.toString());        }    }    /**     * 将BtValue对象作为short返回     */    public short getShort() throws InvalidBtEncodingException {        return getNumber().shortValue();    }    /**     * 将BtValue对象作为int返回     */    public int getInt() throws InvalidBtEncodingException {        return getNumber().intValue();    }    /**     * 将BtValue对象作为long返回     */    public long getLong() throws InvalidBtEncodingException {        return getNumber().longValue();    }    /**     * 将BtValue对象作为List返回     */    @SuppressWarnings("unchecked")    public List<BtValue> getList() throws InvalidBtEncodingException {        if (mValue instanceof ArrayList) {            return (ArrayList<BtValue>)mValue;        } else {            throw new InvalidBtEncodingException("Excepted List<BtValue> !");        }    }    /**     * 将BtValue对象作为Map返回     */    @SuppressWarnings("unchecked")    public Map<String, BtValue> getMap() throws InvalidBtEncodingException {        if (mValue instanceof HashMap) {            return (Map<String, BtValue>)mValue;        } else {            throw new InvalidBtEncodingException("Expected Map<String, BtValue> !");        }    }

为了更好的管理异常, 在这里自定义 异常InvalidBtEncodingException来统一管理

public class InvalidBtEncodingException extends IOException {    public static final long serialVersionUID = -1;    public InvalidBtEncodingException(String message) {        super(message);    }}

2.编写解析类BtParser, 这里采用的方法为递归回溯, 根据bt文件的特有格式分四种类型进行解析, 将解析内容保存为BtValue对象.

public class BtParser {    private final InputStream mInput;    // Zero 未知类型    // '0'..'9' 表示是byte[]数组也就是字符串类型.    // 'i' 表示是数字数字.    // 'l' 表示是列表类型.    // 'd' 表示是字典类型    // 'e' 表示是数字,列表或字典的结束字符    // -1 表示读取到流的结尾    // 调用getNextIndicator接口获取当前的值    private int mIndicator = 0;    private BtParser(InputStream in) {        mInput = in;    }    public static BtValue btDecode(InputStream in) throws IOException {        return new BtParser(in).btParse();    }    private BtValue btParse() throws IOException{        if (getNextIndicator() == -1)            return null;        if (mIndicator >= '0' && mIndicator <= '9')            return btParseBytes();     //read string        else if (mIndicator == 'i')            return btParseNumber();    // read integer        else if (mIndicator == 'l')            return btParseList();      // read list        else if (mIndicator == 'd')            return btParseMap();       // read Map        else            throw new InvalidBtEncodingException                    ("Unknown indicator '" + mIndicator + "'");    }    /**    * 对应解析bt文件的字符串类型    * 1. 解析字符串的长度    * 2. 根据解析的长度从输入流中读取指定长度的字符    * 3. 根据读取到的字符数组构建BtValue对象    * 对应bt文件的 4:ptgh 字符串格式    */    private BtValue btParseBytes() throws IOException{        int b = getNextIndicator();        int num = b - '0';        if (num < 0 || num > 9) {            throw new InvalidBtEncodingException("parse bytes(String) error: not '"            + (char)b +"'");        }        mIndicator = 0;        b = read();        int i = b - '0';        while (i >= 0 && i <= 9) {            num = num * 10 + i;            b = read();            i = b - '0';        }        if (b != ':') {            throw new InvalidBtEncodingException("Colon error: not '" +                    (char)b + "'");        }        return new BtValue(read(num));    }    /**    * 对应解析bt文件中的数字类型    * 1. 判断是否是以 i 字符开头    * 2. 判断要解析的数字是否为负数    * 3. 读取数字到chars数组中直到遇见字符e    * 4. 有chars数组生成数字, 并生成BtValue对象    * 对应bt文件的 i5242e 数字格式    */    private BtValue btParseNumber() throws IOException{        int b = getNextIndicator();        if (b != 'i') {            throw new InvalidBtEncodingException("parse number error: not '" +                    (char)b + "'");        }        mIndicator = 0;        b = read();        if (b == '0') {            b = read();            if (b == 'e') {                return new BtValue(BigInteger.ZERO);            } else {                throw new InvalidBtEncodingException("'e' expected after zero," +                        " not '" + (char)b + "'");            }        }        // don't support more than 255 char big integers        char[] chars = new char[255];        int offset = 0;        // to determine whether the number is negative        if (b == '-') {            b = read();            if (b == '0') {                throw new InvalidBtEncodingException("Negative zero not allowed");            }            chars[offset] = '-';            offset++;        }        if (b < '1' || b > '9') {            throw new InvalidBtEncodingException("Invalid Integer start '"                    + (char)b + "'");        }        chars[offset] = (char)b;        offset++;        // start read the number, save in chars        b = read();        int i = b - '0';        while (i >= 0 && i <= 9) {            chars[offset] = (char)b;            offset++;            b = read();            i = b - '0';        }        if (b != 'e') {            throw new InvalidBtEncodingException("Integer should end with 'e'");        }        String s = new String(chars, 0, offset);        return new BtValue(new BigInteger(s));    }    /**    * 对应解析bt文件中的列表类型    * 1. 判断是否是以'l'字符开头    * 2. 调用btParse解析出BtValue对象, 添加到list中, 直到遇见'e'字符    * 3. 使用获得的list对象构造BtValue对象(这时代表了list)    * 对应bt文件的 l4:spam4:tease 格式    * 如果是 l4:spam4:tease 那么 list对象包含两个BtValue对象, 分别为 spam 和 tease 字符串    */    private BtValue btParseList() throws IOException{        int b = getNextIndicator();        if (b != 'l') {            throw new InvalidBtEncodingException("Expected 'l', not '" +                    (char)b + "'");        }        mIndicator = 0;        List<BtValue> result = new ArrayList<>();        b = getNextIndicator();        while (b != 'e') {            result.add(btParse());            b = getNextIndicator();        }        mIndicator = 0;        return new BtValue(result);    }    /**    * 对应解析bt文件中的字典类型    * 1. 判断是否是以'd'字符开头    * 2. 调用btParse解析获得key与value, 添加到Map中, 直到遇见'e'字符    * 3. 使用获得的Map对象构造BtValue对象(这时代表了Map)    * 对应bt文件的 <d> <key String> <value content> <e>格式    */    private BtValue btParseMap() throws IOException{        int b = getNextIndicator();        if (b != 'd') {            throw new InvalidBtEncodingException("Expected 'd', not '" +                    (char)b + "'");        }        mIndicator = 0;        Map<String, BtValue> result = new HashMap<>();        b = getNextIndicator();        while (b != 'e') {            // Dictionary keys are always strings            String key = btParse().getString();            BtValue value = btParse();            result.put(key, value);            b = getNextIndicator();        }        mIndicator = 0;        return new BtValue(result);    }    private int getNextIndicator() throws IOException{        if (mIndicator == 0) {            mIndicator = mInput.read();        }        return mIndicator;    }    /**    * 从输入流读取一个数据    */    private int read() throws IOException {        int b = mInput.read();        if (b == -1)            throw new EOFException();        return b;    }    /**     * 根据指定长度, 从输入流读取字符数组     */    private byte[] read(int length) throws IOException {        byte[] result = new byte[length];        int read = 0;        while (read < length)        {            int i = mInput.read(result, read, length - read);            if (i == -1)                throw new EOFException();            read += i;        }        return result;    }}

其实这个解析类并不难, 逻辑的编写主要根据bt文件格式来进行解析.

3.最后创建Torrent对象来对解析的文件信息进行分类整理

public class Torrent {    private final static String TAG = "Torrent";    private final Map<String, BtValue> mDecoded;    private final Map<String, BtValue> mDecoded_info;    private final HashSet<URI> mAllTrackers;    private final ArrayList<List<URI>> mTrackers;    private final Date mCreateDate;       private final String mComment;        private final String mCreatedBy;    private final String mName;    private final int mPieceLength;    private final LinkedList<TorrentFile> mFiles;    private final long mSize;    // 对应bt文件中包含多个文件, 定义TorrentFile类来表示每个文件,方便管理    public static class TorrentFile {        public final File file;        public final long size;        public TorrentFile(File file, long size) {            this.file = file;            this.size = size;        }    }    public Torrent(byte[] torrent) throws IOException {        mDecoded = BtParser.btDecode(                new ByteArrayInputStream(mEncoded)).getMap();        mDecoded_info = mDecoded.get("info").getMap();        try {            mAllTrackers = new HashSet<>();            mTrackers = new ArrayList<>();            // 解析获得announce-list, 获取tracker地址            if (mDecoded.containsKey("announce-list")) {                List<BtValue> tiers = mDecoded.get("announce-list").getList();                for (BtValue bv : tiers) {                    List<BtValue> trackers = bv.getList();                    if (trackers.isEmpty()) {                        continue;                    }                    List<URI> tier = new ArrayList<>();                    for (BtValue tracker : trackers) {                        URI uri = new URI(tracker.getString());                        if (!mAllTrackers.contains(uri)) {                            tier.add(uri);                            mAllTrackers.add(uri);                        }                    }                    if (!tier.isEmpty()) {                        mTrackers.add(tier);                    }                }            } else if (mDecoded.containsKey("announce")) { // 对应单个tracker地址                URI tracker = new URI(mDecoded.get("announce").getString());                mAllTrackers.add(tracker);                List<URI> tier = new ArrayList<>();                tier.add(tracker);                mTrackers.add(tier);            }        } catch (URISyntaxException e) {            throw new IOException(e);        }        // 获取文件创建日期        mCreateDate = mDecoded.containsKey("creation date") ?                new Date(mDecoded.get("creation date").getLong() * 1000)                : null;        // 获取文件的comment        mComment = mDecoded.containsKey("comment")                ? mDecoded.get("comment").getString()                : null;        // 获取谁创建的文件        mCreatedBy = mDecoded.containsKey("created by")                ? mDecoded.get("created by").getString()                : null;        // 获取文件名字        mName = mDecoded_info.get("name").getString();        mPieceLength = mDecoded_info.get("piece length").getInt();        mFiles = new LinkedList<>();        // 解析多文件的信息结构        if (mDecoded_info.containsKey("files")) {            for (BtValue file : mDecoded_info.get("files").getList()) {                Map<String, BtValue> fileInfo = file.getMap();                StringBuilder path = new StringBuilder();                for (BtValue pathElement : fileInfo.get("path").getList()) {                    path.append(File.separator)                            .append(pathElement.getString());                }                mFiles.add(new TorrentFile(                        new File(mName, path.toString()),                        fileInfo.get("length").getLong()));            }        } else {            // 对于单文件的bt种子, bt文件的名字就是单文件的名字            mFiles.add(new TorrentFile(                    new File(mName),                    mDecoded_info.get("length").getLong()));        }        // 计算bt种子中所有文件的大小        long size = 0;        for (TorrentFile file : mFiles) {            size += file.size;        }        mSize = size;       // 下面就是单纯的将bt种子文件解析的内容打印出来        String infoType = isMultiFile() ? "Multi" : "Single";        Log.i(TAG, "Torrent: file information: " + infoType);        Log.i(TAG, "Torrent: file name: " + mName);        Log.i(TAG, "Torrent: Announced at: " + (mTrackers.size() == 0 ? " Seems to be trackerless" : ""));        for (int i = 0; i < mTrackers.size(); ++i) {            List<URI> tier = mTrackers.get(i);            for (int j = 0; j < tier.size(); ++j) {                Log.i(TAG, "Torrent: {} " + (j == 0 ? String.format("%2d. ", i + 1) : "    ")                + tier.get(j));            }        }        if (mCreateDate != null) {            Log.i(TAG, "Torrent: createDate: " + mCreateDate);        }        if (mComment != null) {            Log.i(TAG, "Torrent: Comment: " + mComment);        }        if (mCreatedBy != null) {            Log.i(TAG, "Torrent: created by: " + mCreatedBy);        }        if (isMultiFile()) {            Log.i(TAG, "Found {} file(s) in multi-file torrent structure." + mFiles.size());            int i = 0;            for (TorrentFile file : mFiles) {                Log.i(TAG, "Torrent: file is " +                        (String.format("%2d. path: %s size: %s", ++i, file.file.getPath(), file.size)));            }        }        long pieces = (mSize / mDecoded_info.get("piece length").getInt()) + 1;        Log.i(TAG, "Torrent: Pieces....: (byte(s)/piece" +                pieces + " " + mSize / mDecoded_info.get("piece length").getInt());        Log.i(TAG, "Torrent: Total size...: " + mSize);    }    /**    * 加载指定的种子文件, 将种子文件转化为Torrent对象    */    public static Torrent load(File torrent) throws IOException {        byte[] data = readFileToByteArray(torrent);        return new Torrent(data);    }    public boolean isMultiFile() {        return mFiles.size() > 1;    }    /**    * 由file对象获得byte[]对象    */    private static byte[] readFileToByteArray(File file) {        byte[] buffer = null;        try {            FileInputStream fis = new FileInputStream(file);            ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);            byte[] b = new byte[1024];            int n;            while ((n = fis.read(b)) != -1) {                bos.write(b, 0, n);            }            fis.close();            bos.close();            buffer = bos.toByteArray();        } catch (IOException e) {            e.printStackTrace();        }        return buffer;    }}

4.使用方式

File fileTorrent = new File("file path");try {  Torrent.load(fileTorrent);} catch (IOException e) {  e.printStackTrace();}

至此, 对BT种子文件的解析完成. 其实对种子文件的解析就是根据种子文件的编码格式进行对应的解码过程.

参考: mpetazzoni/ttorrent

0 0
原创粉丝点击