package org.torrent.internal.data; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import org.torrent.internal.bencoding.BEncoder; import org.torrent.internal.bencoding.BList; import org.torrent.internal.bencoding.BMap; import org.torrent.internal.bencoding.BTypeException; import org.torrent.internal.util.Validator; import org.torrent.data.FileInfo; public class InfoBlock { private static final String PIECE_LENGTH = "piece length"; private static final String PIECES = "pieces"; private static final String NAME = "name"; private static final String LENGTH = "length"; private static final String FILES = "files"; private static final String PATH = "path"; private static final String PRIVATE = "private"; private final int pieceLength; private final List<Hash> hashList; // private final List<FileInfo> files; private final ArrayList<FileInfo> files; private Hash infoHash; private final Boolean privTracker; private final boolean singleFileMode; private final String baseDir; private long dataSize; public static InfoBlock singleFile(FileInfo fileInfo, List<Hash> hashes, int pieceLength, Boolean priv) { Validator.nonNull(fileInfo, hashes); Validator.isTrue(pieceLength > 0, "Illegal piece length: " + pieceLength); InfoBlock data = new InfoBlock(Arrays .asList(new FileInfo[] { fileInfo }), hashes, pieceLength, priv, null, true); Map<String, Object> info = data.createBasicInfo(); info.put(NAME, fileInfo.getFileName()); info.put(LENGTH, fileInfo.getLength()); data.infoHash = data.calculateInfoHash(info); return data; } public static InfoBlock multiFile(String baseDir, List<FileInfo> files, List<Hash> hashes, int pieceLength, Boolean priv) { Validator.nonNull(baseDir, files, hashes); Validator.isTrue(pieceLength > 0, "Illegal piece length: " + pieceLength); InfoBlock data = new InfoBlock(files, hashes, pieceLength, priv, baseDir, false); Map<String, Object> info = data.createBasicInfo(); info.put(NAME, baseDir); List<Map<String, Object>> flst = new ArrayList<Map<String, Object>>( files.size()); for (FileInfo file : files) { Map<String, Object> fmap = new HashMap<String, Object>(); flst.add(fmap); fmap.put(LENGTH, file.getLength()); List<String> path = fileToPath(file); fmap.put(PATH, path); } data.infoHash = data.calculateInfoHash(info); return data; } /** * @param file * @return */ private static List<String> fileToPath(FileInfo file) { List<String> path = new LinkedList<String>(); StringTokenizer tok = new StringTokenizer(file.getFileName(), File.separator); while (tok.hasMoreTokens()) { path.add(tok.nextToken()); } return path; } private Map<String, Object> createBasicInfo() { Map<String, Object> info = new HashMap<String, Object>(); info.put(PIECE_LENGTH, pieceLength); ByteArrayOutputStream bout = new ByteArrayOutputStream( hashList.size() * 20); for (Hash h : hashList) { try { bout.write(h.toByteArray()); } catch (IOException e) { throw new RuntimeException(e); } } info.put(PIECES, bout.toByteArray()); if (privTracker != null) { info.put(PRIVATE, privTracker ? 1 : 0); } return info; } private InfoBlock(List<FileInfo> files, List<Hash> hashes, int pieceLength, Boolean priv, String baseDir, boolean singleFileMode) { assert files.size() == 1 || !singleFileMode; assert baseDir != null || singleFileMode; this.pieceLength = pieceLength; privTracker = priv; this.singleFileMode = singleFileMode; this.files = new ArrayList<FileInfo>(files); hashList = new ArrayList<Hash>(hashes); this.baseDir = baseDir; calculateSize(); int reqHashes = (int) ((dataSize + pieceLength - 1) / pieceLength); Validator.isTrue(reqHashes == hashList.size(), "No. of hashes required: " + reqHashes + " but got " + hashList.size()); } private void calculateSize() { dataSize = 0; for (FileInfo fi : files) { dataSize += fi.getLength(); } } public InfoBlock(BMap info) throws BTypeException { Validator.notNull(info, "Info dictionary is null!"); pieceLength = ((BigInteger) info.get(PIECE_LENGTH)).intValue(); byte[] hashes = (byte[]) info.get(PIECES); Validator.notNull(hashes, "Pieces entry in info dictionary is null!"); Validator.isTrue(hashes.length % 20 == 0, "Pieces entry in info dictionary is invalid (length " + hashes.length + " not multiple of 20)!"); hashList = new ArrayList<Hash>(hashes.length / 20); int off = 0; while (off < hashes.length) { hashList.add(new Hash(Arrays.copyOfRange(hashes, off, off + 20), Hash.Type.SHA1)); off += 20; } files = new ArrayList<FileInfo>(); String base = info.getString(NAME); Validator.notNull(base, "Missing non-optional entry " + NAME); if (info.containsKey(LENGTH)) { singleFileMode = true; baseDir = null; // Single file mode files.add(new FileInfo(base, ((BigInteger) info.get(LENGTH)) .longValue())); } else { singleFileMode = false; baseDir = base; BList fList = info.getList(FILES); for (int i = 0; i < fList.size(); i++) { BMap file = fList.getMap(i); StringBuilder path = new StringBuilder(); BList elements = file.getList(PATH); for (int j = 0; j < elements.size(); j++) { String element = elements.getString(j); if (path.length() > 0) { path.append(File.separatorChar); } path.append(element); } files.add(new FileInfo(path.toString(), ((BigInteger) file .get(LENGTH)).longValue())); } } infoHash = calculateInfoHash(info); privTracker = Integer.valueOf(1).equals(info.getInteger(PRIVATE)); calculateSize(); } /** * @param info * @return */ private Hash calculateInfoHash(Map<String, ?> info) { try { MessageDigest sha1 = MessageDigest.getInstance("sha1"); sha1.update(BEncoder.bencode(info)); return new Hash(sha1.digest(), Hash.Type.SHA1); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public int getPieceLength() { return pieceLength; } public List<Hash> getPieceHashes() { return Collections.unmodifiableList(hashList); } public int getPiecesCount() { return hashList.size(); } public ArrayList<FileInfo> getFiles() { // return Collections.unmodifiableList(files); return files; } public Hash getInfoHash() { return infoHash; } public boolean isPrivate() { return privTracker != null && privTracker; } /** * Determines if this DataInfo was created with a bencoded SingleFileMode * structure. * * @return */ public boolean isSingleFileMode() { return singleFileMode; } public String getBaseDir() { return baseDir; } public Map<String, ?> asDictionary() { Map<String, Object> infoMap = createBasicInfo(); if (singleFileMode) { infoMap.put(NAME, files.get(0).getFileName()); infoMap.put(LENGTH, files.get(0).getLength()); } else { infoMap.put(NAME, baseDir); List<Map<String, Object>> tfiles = new LinkedList<Map<String, Object>>(); for (FileInfo file : files) { Map<String, Object> entry = new HashMap<String, Object>(); entry.put(PATH, fileToPath(file)); entry.put(LENGTH, file.getLength()); tfiles.add(entry); } infoMap.put(FILES, tfiles); } return infoMap; } public long getDataSize() { return dataSize; } }