/*
* File : TOTorrentImpl.java
* Created : 5 Oct. 2003
* By : Parg
*
* Azureus - a Java Bittorrent client
*
* 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 of the License.
*
* 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 ( see the LICENSE file ).
*
* 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 com.frostwire.torrent;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class is NOT thread safe.
*
*/
class TOTorrentImpl implements TOTorrent {
protected static final String TK_ANNOUNCE = "announce";
protected static final String TK_ANNOUNCE_LIST = "announce-list";
protected static final String TK_COMMENT = "comment";
protected static final String TK_CREATION_DATE = "creation date";
protected static final String TK_CREATED_BY = "created by";
protected static final String TK_INFO = "info";
protected static final String TK_NAME = "name";
protected static final String TK_LENGTH = "length";
protected static final String TK_PATH = "path";
protected static final String TK_FILES = "files";
protected static final String TK_PIECE_LENGTH = "piece length";
protected static final String TK_PIECES = "pieces";
protected static final String TK_PRIVATE = "private";
protected static final String TK_NAME_UTF8 = "name.utf-8";
protected static final String TK_PATH_UTF8 = "path.utf-8";
protected static final String TK_COMMENT_UTF8 = "comment.utf-8";
protected static final String TK_WEBSEED_BT = "httpseeds";
protected static final String TK_WEBSEED_GR = "url-list";
protected static final String TK_HASH_OVERRIDE = "hash-override";
protected static final List<String> TK_ADDITIONAL_OK_ATTRS = Arrays.asList(TK_COMMENT_UTF8, AZUREUS_PROPERTIES, TK_WEBSEED_BT, TK_WEBSEED_GR);
private byte[] torrent_name;
private byte[] torrent_name_utf8;
private byte[] comment;
private URI announce_url;
private TOTorrentAnnounceURLGroupImpl announce_group = new TOTorrentAnnounceURLGroupImpl(this);
private long piece_length;
private byte[][] pieces;
private int number_of_pieces;
private byte[] torrent_hash_override;
private byte[] torrent_hash;
private HashWrapper torrent_hash_wrapper;
private boolean simple_torrent;
private TOTorrentFileImpl[] files;
private long creation_date;
private byte[] created_by;
private Map<String, Object> additional_properties = new HashMap<String, Object>(4);
private Map<String, Object> additional_info_properties = new HashMap<String, Object>(4);
private List<TOTorrentListener> listeners;
/**
* Constructor for deserialisation
*/
protected TOTorrentImpl() {
}
/**
* Constructor for creation
*/
protected TOTorrentImpl(String _torrent_name, URI _announce_url, boolean _simple_torrent) throws TOTorrentException {
try {
torrent_name = _torrent_name.getBytes(Constants.DEFAULT_ENCODING);
torrent_name_utf8 = torrent_name;
setAnnounceURL(_announce_url);
simple_torrent = _simple_torrent;
} catch (UnsupportedEncodingException e) {
throw (new TOTorrentException("Unsupported encoding for '" + _torrent_name + "'", TOTorrentException.RT_UNSUPPORTED_ENCODING));
}
}
public void serialiseToBEncodedFile(final File output_file)
throws TOTorrentException {
byte[] res = serialiseToByteArray();
BufferedOutputStream bos = null;
try {
File parent = output_file.getParentFile();
if (parent == null) {
throw new TOTorrentException("Path '" + output_file + "' is invalid", TOTorrentException.RT_WRITE_FAILS);
}
// We would expect this to be normally true most of the time.
if (!parent.isDirectory()) {
// Try to create a directory.
boolean dir_created = mkdirs(parent);
// Something strange going on...
if (!dir_created) {
// Does it exist already?
if (parent.exists()) {
// And it really isn't a directory?
if (!parent.isDirectory()) {
// How strange.
throw new TOTorrentException("Path '" + output_file + "' is invalid", TOTorrentException.RT_WRITE_FAILS);
}
// It is a directory which does exist. But we tested for that earlier. Perhaps it has been created in the
// meantime.
else {
/* do nothing */
}
}
// It doesn't exist, and we couldn't create it.
else {
throw new TOTorrentException("Failed to create directory '" + parent + "'", TOTorrentException.RT_WRITE_FAILS);
}
} // end if (!dir_created)
} // end if (!parent.isDirectory)
File temp = new File(parent, output_file.getName() + ".saving");
if (temp.exists()) {
if (!temp.delete()) {
throw (new TOTorrentException("Insufficient permissions to delete '" + temp + "'", TOTorrentException.RT_WRITE_FAILS));
}
} else {
boolean ok = false;
try {
ok = temp.createNewFile();
} catch (Throwable e) {
}
if (!ok) {
throw (new TOTorrentException("Insufficient permissions to write '" + temp + "'", TOTorrentException.RT_WRITE_FAILS));
}
}
FileOutputStream fos = new FileOutputStream(temp, false);
bos = new BufferedOutputStream(fos, 8192);
bos.write(res);
bos.flush();
// thinking about removing this - just do so for CVS for the moment
//if (!Constants.isCVSVersion()) {
fos.getFD().sync();
//}
bos.close();
bos = null;
//only use newly saved file if it got this far, i.e. it was written successfully
if (temp.length() > 1L) {
output_file.delete(); // Will fail silently if it doesn't exist.
temp.renameTo(output_file);
}
} catch (TOTorrentException e) {
throw (e);
} catch (Throwable e) {
throw (new TOTorrentException("Failed to serialise torrent: " + Debug.getNestedExceptionMessage(e), TOTorrentException.RT_WRITE_FAILS));
} finally {
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
Debug.printStackTrace(e);
}
}
}
}
protected byte[] serialiseToByteArray() throws TOTorrentException {
Map<String, Object> root = serialiseToMap();
try {
return (BEncoder.encode(root));
} catch (IOException e) {
throw (new TOTorrentException("Failed to serialise torrent: " + Debug.getNestedExceptionMessage(e), TOTorrentException.RT_WRITE_FAILS));
}
}
public Map<String, Object> serialiseToMap() throws TOTorrentException {
// protect against recursion when getting the hash
Map<String, Object> root = new HashMap<String, Object>();
// seen a NPE here, not sure of cause so handling null announce_url in case
writeStringToMetaData(root, TK_ANNOUNCE, (announce_url == null ? TorrentUtils.getDecentralisedEmptyURL() : announce_url).toString());
TOTorrentAnnounceURLSet[] sets = announce_group.getAnnounceURLSets();
if (sets.length > 0) {
List<List<byte[]>> announce_list = new ArrayList<List<byte[]>>();
for (int i = 0; i < sets.length; i++) {
TOTorrentAnnounceURLSet set = sets[i];
URI[] urls = set.getAnnounceURLs();
if (urls.length == 0) {
continue;
}
List<byte[]> sub_list = new ArrayList<byte[]>();
announce_list.add(sub_list);
for (int j = 0; j < urls.length; j++) {
sub_list.add(writeStringToMetaData(urls[j].toString()));
}
}
if (announce_list.size() > 0) {
root.put(TK_ANNOUNCE_LIST, announce_list);
}
}
if (comment != null) {
root.put(TK_COMMENT, comment);
}
if (creation_date != 0) {
root.put(TK_CREATION_DATE, Long.valueOf(creation_date));
}
if (created_by != null) {
root.put(TK_CREATED_BY, created_by);
}
Map<String, Object> info = new HashMap<String, Object>();
root.put(TK_INFO, info);
info.put(TK_PIECE_LENGTH, Long.valueOf(piece_length));
if (pieces == null) {
throw (new TOTorrentException("Pieces is null", TOTorrentException.RT_WRITE_FAILS));
}
byte[] flat_pieces = new byte[pieces.length * 20];
for (int i = 0; i < pieces.length; i++) {
System.arraycopy(pieces[i], 0, flat_pieces, i * 20, 20);
}
info.put(TK_PIECES, flat_pieces);
info.put(TK_NAME, torrent_name);
if (torrent_name_utf8 != null) {
info.put(TK_NAME_UTF8, torrent_name_utf8);
}
if (torrent_hash_override != null) {
info.put(TK_HASH_OVERRIDE, torrent_hash_override);
}
if (simple_torrent) {
TOTorrentFile file = files[0];
info.put(TK_LENGTH, Long.valueOf(file.getLength()));
} else {
List<Map<String, Object>> meta_files = new ArrayList<Map<String, Object>>();
info.put(TK_FILES, meta_files);
for (int i = 0; i < files.length; i++) {
TOTorrentFileImpl file = files[i];
Map<String, Object> file_map = file.serializeToMap();
meta_files.add(file_map);
}
}
Iterator<String> info_it = additional_info_properties.keySet().iterator();
while (info_it.hasNext()) {
String key = (String) info_it.next();
info.put(key, additional_info_properties.get(key));
}
Iterator<String> it = additional_properties.keySet().iterator();
while (it.hasNext()) {
String key = (String) it.next();
Object value = additional_properties.get(key);
if (value != null) {
root.put(key, value);
}
}
return (root);
}
public byte[] getName() {
return torrent_name;
}
protected void setName(byte[] name) {
torrent_name = name;
}
public String getUTF8Name() {
try {
return torrent_name_utf8 == null ? null : new String(torrent_name_utf8, "utf8");
} catch (UnsupportedEncodingException e) {
return null;
}
}
protected void setNameUTF8(byte[] _name) {
torrent_name_utf8 = _name;
}
public boolean isSimpleTorrent() {
return (simple_torrent);
}
public byte[] getComment() {
return (comment);
}
protected void setComment(byte[] _comment) {
comment = _comment;
}
public void setComment(String _comment) {
try {
byte[] utf8_comment = _comment.getBytes(Constants.DEFAULT_ENCODING);
setComment(utf8_comment);
setAdditionalByteArrayProperty(TK_COMMENT_UTF8, utf8_comment);
} catch (UnsupportedEncodingException e) {
Debug.printStackTrace(e);
comment = null;
}
}
public URI getAnnounceURL() {
return (announce_url);
}
public boolean setAnnounceURL(URI url) {
URI newURL = anonymityTransform(url);
String s0 = (newURL == null) ? "" : newURL.toString();
String s1 = (announce_url == null) ? "" : announce_url.toString();
if (s0.equals(s1))
return false;
if (newURL == null) {
// anything's better than null...
newURL = TorrentUtils.getDecentralisedEmptyURL();
}
//announce_url = StringInterner.internURL(newURL);
announce_url = newURL;
fireChanged(TOTorrentListener.CT_ANNOUNCE_URLS);
return true;
}
public boolean isDecentralised() {
return (TorrentUtils.isDecentralised(getAnnounceURL()));
}
public long getCreationDate() {
return (creation_date);
}
public void setCreationDate(long _creation_date) {
creation_date = _creation_date;
}
public void setCreatedBy(byte[] _created_by) {
created_by = _created_by;
}
protected void setCreatedBy(String _created_by) {
try {
setCreatedBy(_created_by.getBytes(Constants.DEFAULT_ENCODING));
} catch (UnsupportedEncodingException e) {
Debug.printStackTrace(e);
created_by = null;
}
}
public byte[] getCreatedBy() {
return (created_by);
}
public byte[] getHash() throws TOTorrentException {
if (torrent_hash == null) {
Map<String, Object> root = serialiseToMap();
@SuppressWarnings("unchecked")
Map<String, Object> info = (Map<String, Object>) root.get(TK_INFO);
setHashFromInfo(info);
}
return (torrent_hash);
}
public HashWrapper getHashWrapper() throws TOTorrentException {
if (torrent_hash_wrapper == null) {
getHash();
}
return (torrent_hash_wrapper);
}
public boolean hasSameHashAs(TOTorrent other) {
try {
byte[] other_hash = other.getHash();
return (Arrays.equals(getHash(), other_hash));
} catch (TOTorrentException e) {
Debug.printStackTrace(e);
return (false);
}
}
protected void setHashFromInfo(Map<String, Object> info) throws TOTorrentException {
try {
if (torrent_hash_override == null) {
SHA1Hasher s = new SHA1Hasher();
torrent_hash = s.calculateHash(BEncoder.encode(info));
} else {
torrent_hash = torrent_hash_override;
}
torrent_hash_wrapper = new HashWrapper(torrent_hash);
} catch (Throwable e) {
throw (new TOTorrentException("Failed to calculate hash: " + Debug.getNestedExceptionMessage(e), TOTorrentException.RT_HASH_FAILS));
}
}
public void setHashOverride(byte[] hash)
throws TOTorrentException {
if (torrent_hash_override != null) {
if (Arrays.equals(hash, torrent_hash_override)) {
return;
} else {
throw (new TOTorrentException("Hash override can only be set once", TOTorrentException.RT_HASH_FAILS));
}
}
/* support this for fixing borked torrents
if ( !TorrentUtils.isDecentralised( announce_url )){
throw( new TOTorrentException(
"Hash override can only be set on decentralised torrents",
TOTorrentException.RT_HASH_FAILS ));
}
*/
torrent_hash_override = hash;
torrent_hash = null;
getHash();
}
protected byte[] getHashOverride() {
return torrent_hash_override;
}
public void setPrivate(boolean _private_torrent) throws TOTorrentException {
additional_info_properties.put(TK_PRIVATE, Long.valueOf(_private_torrent ? 1 : 0));
// update torrent hash
torrent_hash = null;
getHash();
}
public boolean getPrivate() {
Object o = additional_info_properties.get(TK_PRIVATE);
if (o instanceof Long) {
return (((Long) o).intValue() != 0);
}
return (false);
}
public TOTorrentAnnounceURLGroup getAnnounceURLGroup() {
return (announce_group);
}
protected void addTorrentAnnounceURLSet(URI[] urls) {
announce_group.addSet(new TOTorrentAnnounceURLSetImpl(this, urls));
}
public long getSize() {
long res = 0;
for (int i = 0; i < files.length; i++) {
res += files[i].getLength();
}
return (res);
}
public long getPieceLength() {
return (piece_length);
}
protected void setPieceLength(long _length) {
piece_length = _length;
}
public int getNumberOfPieces() {
// to support buggy torrents with extraneous pieces (they seem to exist) we calculate
// the required number of pieces rather than the using the actual. Note that we
// can't adjust the pieces array itself as this results in incorrect torrent hashes
// being derived later after a save + restore
if (number_of_pieces == 0) {
number_of_pieces = (int) ((getSize() + (piece_length - 1)) / piece_length);
}
return (number_of_pieces);
}
public byte[][] getPieces() {
return (pieces);
}
public void setPieces(byte[][] _pieces) {
pieces = _pieces;
}
public int getFileCount() {
return (files.length);
}
public TOTorrentFile[] getFiles() {
return (files);
}
protected void setFiles(TOTorrentFileImpl[] _files) {
files = _files;
}
protected boolean getSimpleTorrent() {
return (simple_torrent);
}
protected void setSimpleTorrent(boolean _simple_torrent) {
simple_torrent = _simple_torrent;
}
protected Map<String, Object> getAdditionalProperties() {
return (additional_properties);
}
public void setAdditionalStringProperty(String name, String value) {
try {
setAdditionalByteArrayProperty(name, writeStringToMetaData(value));
} catch (TOTorrentException e) {
// hide encoding exceptions as default encoding must be available
Debug.printStackTrace(e);
}
}
public String getAdditionalStringProperty(String name) {
try {
return (readStringFromMetaData(getAdditionalByteArrayProperty(name)));
} catch (TOTorrentException e) {
// hide encoding exceptions as default encoding must be available
Debug.printStackTrace(e);
return (null);
}
}
public void setAdditionalByteArrayProperty(String name, byte[] value) {
additional_properties.put(name, value);
}
public void setAdditionalProperty(String name, Object value) {
if (value instanceof String) {
setAdditionalStringProperty(name, (String) value);
} else {
additional_properties.put(name, value);
}
}
public byte[] getAdditionalByteArrayProperty(String name) {
Object obj = additional_properties.get(name);
if (obj instanceof byte[]) {
return ((byte[]) obj);
}
return (null);
}
public void setAdditionalLongProperty(String name, Long value) {
additional_properties.put(name, value);
}
public Long getAdditionalLongProperty(String name) {
Object obj = additional_properties.get(name);
if (obj instanceof Long) {
return ((Long) obj);
}
return (null);
}
public void setAdditionalListProperty(String name, List<Object> value) {
additional_properties.put(name, value);
}
@SuppressWarnings("unchecked")
public List<Object> getAdditionalListProperty(String name) {
Object obj = additional_properties.get(name);
if (obj instanceof List) {
return ((List<Object>) obj);
}
return (null);
}
public void setAdditionalMapProperty(String name, Map<String, Object> value) {
additional_properties.put(name, value);
}
@SuppressWarnings("unchecked")
public Map<String, Object> getAdditionalMapProperty(String name) {
Object obj = additional_properties.get(name);
if (obj instanceof Map) {
return ((Map<String, Object>) obj);
}
return (null);
}
public Object getAdditionalProperty(String name) {
return (additional_properties.get(name));
}
public void removeAdditionalProperty(String name) {
additional_properties.remove(name);
}
public void removeAdditionalProperties() {
Map<String, Object> new_props = new HashMap<String, Object>();
Iterator<String> it = additional_properties.keySet().iterator();
while (it.hasNext()) {
String key = (String) it.next();
if (TK_ADDITIONAL_OK_ATTRS.contains(key)) {
new_props.put(key, additional_properties.get(key));
}
}
additional_properties = new_props;
}
protected void addAdditionalProperty(String name, Object value) {
additional_properties.put(name, value);
}
protected void addAdditionalInfoProperty(String name, Object value) {
additional_info_properties.put(name, value);
}
protected Map<String, Object> getAdditionalInfoProperties() {
return (additional_info_properties);
}
protected String readStringFromMetaData(Map<String, Object> meta_data, String name) throws TOTorrentException {
Object obj = meta_data.get(name);
if (obj instanceof byte[]) {
return (readStringFromMetaData((byte[]) obj));
}
return (null);
}
protected String readStringFromMetaData(byte[] value) throws TOTorrentException {
try {
if (value == null) {
return null;
}
return new String(value, Constants.DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
throw new TOTorrentException("Unsupported encoding for '" + value + "'", TOTorrentException.RT_UNSUPPORTED_ENCODING);
}
}
protected void writeStringToMetaData(Map<String, Object> meta_data, String name, String value) throws TOTorrentException {
meta_data.put(name, writeStringToMetaData(value));
}
protected byte[] writeStringToMetaData(String value) throws TOTorrentException {
try {
return (value.getBytes(Constants.DEFAULT_ENCODING));
} catch (UnsupportedEncodingException e) {
throw (new TOTorrentException("Unsupported encoding for '" + value + "'", TOTorrentException.RT_UNSUPPORTED_ENCODING));
}
}
protected URI anonymityTransform(URI url) {
/*
* hmm, doing this is harder than it looks as we have issues hosting
* (both starting tracker instances and also short-cut loopback for seeding
* leave as is for the moment
if ( HostNameToIPResolver.isNonDNSName( url.getHost())){
// remove the port as it is uninteresting and could leak information about the
// tracker
String url_string = url.toString();
String port_string = ":" + (url.getPort()==-1?url.getDefaultPort():url.getPort());
int port_pos = url_string.indexOf( ":" + url.getPort());
if ( port_pos != -1 ){
try{
return( new URL( url_string.substring(0,port_pos) + url_string.substring(port_pos+port_string.length())));
}catch( MalformedURLException e){
Debug.printStackTrace(e);
}
}
}
*/
return (url);
}
public void print() {
try {
byte[] hash = getHash();
System.out.println("name = " + torrent_name);
System.out.println("announce url = " + announce_url);
System.out.println("announce group = " + announce_group.getAnnounceURLSets().length);
System.out.println("creation date = " + creation_date);
try {
System.out.println("creation by = " + (created_by != null ? new String(created_by, Constants.DEFAULT_ENCODING) : ""));
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
}
System.out.println("comment = " + comment);
System.out.println("hash = " + ByteFormatter.nicePrint(hash));
System.out.println("piece length = " + getPieceLength());
System.out.println("pieces = " + getNumberOfPieces());
Iterator<String> info_it = additional_info_properties.keySet().iterator();
while (info_it.hasNext()) {
String key = (String) info_it.next();
Object value = additional_info_properties.get(key);
try {
System.out.println("info prop '" + key + "' = '" + (value instanceof byte[] ? new String((byte[]) value, Constants.DEFAULT_ENCODING) : value.toString()) + "'");
} catch (UnsupportedEncodingException e) {
System.out.println("info prop '" + key + "' = unsupported encoding!!!!");
}
}
Iterator<String> it = additional_properties.keySet().iterator();
while (it.hasNext()) {
String key = (String) it.next();
Object value = additional_properties.get(key);
try {
System.out.println("prop '" + key + "' = '" + (value instanceof byte[] ? new String((byte[]) value, Constants.DEFAULT_ENCODING) : value.toString()) + "'");
} catch (UnsupportedEncodingException e) {
System.out.println("prop '" + key + "' = unsupported encoding!!!!");
}
}
if (pieces == null) {
System.out.println("\tpieces = null");
} else {
for (int i = 0; i < pieces.length; i++) {
System.out.println("\t" + ByteFormatter.nicePrint(pieces[i]));
}
}
for (int i = 0; i < files.length; i++) {
byte[][] path_comps = files[i].getPathComponents();
String path_str = "";
for (int j = 0; j < path_comps.length; j++) {
try {
path_str += (j == 0 ? "" : File.separator) + new String(path_comps[j], Constants.DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
System.out.println("file - unsupported encoding!!!!");
}
}
System.out.println("\t" + path_str + " (" + files[i].getLength() + ")");
}
} catch (TOTorrentException e) {
Debug.printStackTrace(e);
}
}
protected void fireChanged(int type) {
List<TOTorrentListener> to_fire = null;
try {
//this_mon.enter();
if (listeners != null) {
to_fire = new ArrayList<TOTorrentListener>(listeners);
}
} finally {
//this_mon.exit();
}
if (to_fire != null) {
for (TOTorrentListener l : to_fire) {
try {
l.torrentChanged(this, type);
} catch (Throwable e) {
Debug.out(e);
}
}
}
}
public void addListener(TOTorrentListener l) {
try {
//this_mon.enter();
if (listeners == null) {
listeners = new ArrayList<TOTorrentListener>();
}
listeners.add(l);
} finally {
//this_mon.exit();
}
}
public void removeListener(TOTorrentListener l) {
try {
//this_mon.enter();
if (listeners != null) {
listeners.remove(l);
if (listeners.size() == 0) {
listeners = null;
}
}
} finally {
//this_mon.exit();
}
}
/**
* Makes Directories as long as the directory isn't directly in Volumes (OSX)
* @param f
* @return
*/
private static boolean mkdirs(File f) {
if (Constants.isOSX) {
Pattern pat = Pattern.compile("^(/Volumes/[^/]+)");
Matcher matcher = pat.matcher(f.getParent());
if (matcher.find()) {
String sVolume = matcher.group();
File fVolume = new File(sVolume);
if (!fVolume.isDirectory()) {
System.out.println(sVolume + " is not mounted or not available.");
return false;
}
}
}
return f.mkdirs();
}
}