unit TorrentFile;

interface

uses
  SysUtils, Contnrs, Hashes, Classes, BDecode;

type
  TBitfield = array of boolean;

  TTorrentPiece = class(TObject)
  private
    _Hash: String;
    _HashBin: String;
    _Valid: Boolean;
  public
    property Hash: String read _Hash;
    property HashBin: String read _HashBin;
    property Valid: Boolean read _Valid write _Valid;
    constructor Create(Hash: String; HashBin:String; Valid: Boolean);
  end;

  TTorrentSubFile = class(TObject)
  private
    _Name: String;
    _Path: String;
    _Filename: String;
    _Length: Int64;
    _Offset: Int64;
    _Left: Int64;
  public
    property Name: String read _Name write _Name;
    property Path: String read _Path write _Path;
    property Length: Int64 read _Length;
    property Offset: Int64 read _Offset;
    property Left: Int64 read _Left write _Left;
    property Filename: String read _Filename write _Filename;
    constructor Create(Name: String; Path: String; Length: Int64; Offset: Int64);
  end;

  TTorrentFile = class(TObject)
  published
  private
    _Announce : String;
    _Name : String;
    _Comment : String;
    _Length : Int64;
    _CreationTime : Int32;
    _Count : Integer;
    _Err : TStringList;
    _Tree : TObjectHash;
    _SHA1Hash : String;
    _HashBin : String;
    _Multifile : Boolean;
    _Files : TObjectList;
  public
    Pieces : array of TTorrentPiece;
    PieceLength : Integer;
    BackupTrackers : TStringList;

    property Announce: String read _Announce write _Announce;
    property Name: String read _Name write _Name;
    property CreationTime: Int32 read _CreationTime write _CreationTime;
    property Length: Int64 read _Length;
    property Count: Integer read _Count;
    property Tree: TObjectHash read _Tree;
    property Errors: TStringList read _Err;
    property Hash: String read _SHA1Hash;
    property Comment: String read _Comment write _Comment;
    property HashBin: String read _HashBin;
    property Multifile: Boolean read _Multifile;
    property Files: TObjectList read _Files write _Files;
    procedure Clear();
    function Load(Stream: TStream): Boolean;
    procedure Save(Stream: TStream; Pieces : array of TTorrentPiece);
    procedure Init(Announce, Name, Comment, HashBin:String; Length:Int64; Multifile:Boolean);
    constructor Create();
    destructor Destroy(); override;
  end;

implementation

uses
  DCDateTimeUtils, SHA1;

{ TTorrentSubFile }

constructor TTorrentSubFile.Create(Name, Path: String; Length: Int64; Offset: Int64);
begin
  _Name := Name;
  _Path := Path;
  _Length := Length;
  _Offset := Offset;
  _Left := Length;
  
  inherited Create();
end;

procedure TTorrentFile.Clear();
var
  i : Integer;
begin
  _Announce := '';
  _Name := '';
  _SHA1Hash := '';
  _Length := 0;
  _Count := 0;
  _Files.Clear();
  _Tree.Clear();
  _Err.Clear();
  for i := Low(Pieces) to High(Pieces) do FreeAndNil(Pieces[i]);
  SetLength(Pieces,0);
  _Multifile := False;
end;

constructor TTorrentFile.Create();
begin
  _Files := TObjectList.Create();
  _Tree := TObjectHash.Create();
  _Err := TStringList.Create();
  BackupTrackers := TStringList.Create;
  inherited Create();
end;

destructor TTorrentFile.Destroy();
begin
  Clear();
  FreeAndNil(_Files);
  FreeAndNil(_Tree);
  FreeAndNil(_Err);
  FreeAndNil(BackupTrackers);
  inherited;
end;

procedure TTorrentFile.Init(Announce, Name, Comment, HashBin:String; Length:Int64; Multifile:Boolean);
begin
  _Announce := Announce;
  _Name := Name;
  _Comment := Comment;
  _HashBin := HashBin;
  _Length := Length;
  _Multifile := Multifile;
  _CreationTime := DateTimeToUnixFileTime(Now);
end;

function TTorrentFile.Load(Stream: TStream): Boolean;
var
  info, thisfile: TObjectHash;
  files, path, backup, backup2: TObjectList;
  fp, fn: String;
  i, j, pcount: Integer;
  sz, fs, fo: Int64;
  digest: TSHA1Digest;
  r: Boolean;
  o: TObject;
  s:string;
begin
  Clear();
  r := False;
  sz := 0;
  try
    o := bdecodeStream(Stream);
    if(Assigned(o)) then begin
      _Tree := o as TObjectHash;
      if(_Tree.Exists('announce')) then begin
        _Announce := (_Tree['announce'] as TIntString).StringPart;
      end else begin
        _Err.Add('Corrupt File: Missing "announce" segment');
      end;
      if(_Tree.Exists('announce-list')) then begin
         backup := _Tree['announce-list'] as TObjectList;
         for i := 0 to backup.Count - 1 do begin
              backup2 := (backup[i] as TObjectList);
              for j:=0 to backup2.Count -1 do BackupTrackers.Add((backup2[j] as TIntString).StringPart);
         end;
      end;
      if(_Tree.Exists('comment')) then begin
        _Comment := (_Tree['comment'] as TIntString).StringPart;
      end;
      if(_Tree.Exists('creation date')) then begin
        _CreationTime := (_Tree['creation date'] as TIntString).IntPart;
      end;
      if(_Tree.Exists('info')) then begin
        info := _Tree['info'] as TObjectHash;
        if(info.Exists('name')) then begin
          _Name := (info['name'] as TIntString).StringPart;
          if copy(_Name,system.length(_Name)-7,8)='.torrent' then
            _Name:=copy(_Name,0,system.length(_Name)-8);
        end else begin
          _Err.Add('Corrupt File: Missing "info.name" segment');
        end;
        if(info.Exists('piece length')) then begin
          PieceLength := (info['piece length'] as TIntString).IntPart;
        end else begin
          _Err.Add('Corrupt File: Missing "info.piece length" segment');
        end;
        {
        if(info.Exists('pieces')) then begin
          fp := (info['pieces'] as TIntString).StringPart;
          pcount := System.Length(fp) div 20;
          SetLength(Pieces,pcount);
          for i := 0 to pcount - 1 do begin
            s:=copy(fp,(i * 20) + 1,20);
            Pieces[i] := TTorrentPiece.Create(bin2hex(s), s, False);
          end;
        end else begin
          _Err.Add('Corrupt File: Missing "info.pieces" segment');
        end;
        }
        if(info.Exists('length')) then begin // single-file archive
          sz := (info['length'] as TIntString).IntPart;
          _Count := 1;
          _Files.Add(TTorrentSubFile.Create(_Name,'',sz,Int64(0)));
        end else begin
          if(info.Exists('files')) then begin
            _Multifile := True;
            files := info['files'] as TObjectList;
            for i := 0 to files.Count - 1 do begin
              thisfile := files[i] as TObjectHash;
              if(thisfile.Exists('length')) then begin
                fs := (thisfile['length'] as TIntString).IntPart;
              end else begin
                fs := Int64(0);
                _Err.Add('Corrupt File: files[' + IntToStr(i) + '] is missing a "length" segment');
              end;
              fp := '';
              fn := '';
              if(thisfile.Exists('path')) then begin
                path := thisfile['path'] as TObjectList;
                for j := 0 to path.Count - 2 do
                  fp := fp + (path[j] as TIntString).StringPart + PathDelim;
                if(path.Count > 0) then fn := (path[path.Count - 1] as TIntString).StringPart;
              end else begin
                _Err.Add('Corrupt File: files[' + IntToStr(i) + '] is missing a "path" segment');
              end;
              _Files.Add(TTorrentSubFile.Create(fn,fp,fs,sz));
              sz := sz + fs;
            end;
            _Count := _Files.Count;
          end else begin
            _Err.Add('Corrupt File: Missing both "info.length" and "info.files" segments (should have one or the other)');
          end;
        end;
        if(_Tree.Exists('_info_start') and _Tree.Exists('_info_length')) then begin
          fo := Stream.Position;
          Stream.Seek((_Tree['_info_start'] as TIntString).IntPart,soFromBeginning);
          fs := (_Tree['_info_length'] as TIntString).IntPart;
          SetLength(fp,fs);
          Stream.Read(PChar(fp)^,fs);
          digest := SHA1String(fp);
          _SHA1Hash := SHA1Print(digest);
          SetLength(_HashBin, 20);
          Move(digest[0], _HashBin[1], 20);
          Stream.Seek(fo,soFromBeginning);
        end;
      end else begin
        _Err.Add('Corrupt File: Missing "info" segment');
      end;
      _Length := sz;
      r := True;
    end else begin
      _Err.Add('Error parsing file; does not appear to be valid bencoded metainfo');
    end;
  except
    _Err.Add('Something bad happened while trying to load the file, probably corrupt metainfo');
  end;
  Result := r;
end;

procedure TTorrentFile.Save(Stream: TStream; Pieces : array of TTorrentPiece);
var i:integer;
    s,s2:string;

procedure WStrm(s:string);
begin
  Stream.WriteBuffer(s[1],system.length(s));
end;

procedure WStrg(s:string);
var t:String;
begin
  t:=inttostr(system.length(s))+':'+s;
  WStrm(t);
end;

procedure WInt(i:int64);
begin
  WStrm('i'); WStrm(IntToStr(i)); WStrm('e');
end;

begin
  WStrm('d');
  WStrg('announce'); WStrg(Announce);
  if BackupTrackers.Count > 0 then
  begin
    WStrg('announce-list');
    WStrm('l');
     // Primary Tracker
     WStrm('l');
      WStrg(Announce);
     WStrm('e');
     // Backup Tracker
     for i:=0 to BackupTrackers.Count-1 do
      if BackupTrackers[i] <> Announce then
      begin
      WStrm('l');
      WStrg(BackupTrackers[i]);
      WStrm('e');
      end;
    WStrm('e');
  end;
  if Comment <> '' then
  begin
    WStrg('comment');
    WStrg(comment);
  end;
  if Date <> 0 then
  begin
    WStrg('creation date');
    WInt(CreationTime);
  end;
  WStrg('info'); WStrm('d');

  if Multifile then
  begin
  WStrg('files'); WStrm('l');

  for i:=0 to Files.Count-1 do
   with (Files[i] as TTorrentSubFile) do
    begin
       WStrm('d');
         WStrg('length');
         WInt(Length);
         WStrg('path');
         WStrm('l');

         if Path <> '' then
         begin
           s:=path;
           repeat
            if pos('\',s) <> 0 then
            begin
              s2:=copy(s,1,pos('\',s)-1); WStrg(s2);
              Delete(s,1,pos('\',s));
            end;

            if (pos('\',s)=0) and (s <>'') then WStrg(s);
           until pos('\',s)=0;
         end;
         WStrg(Name);

         WStrm('e');
       WStrm('e');
    end;

  WStrm('e');
  end
  else
  begin
     WStrg('length');
     WInt(Length);
  end;

  WStrg('name');
  WStrg(Name);
  WStrg('piece length');
  WInt(PieceLength);

  WStrg('pieces');
  WStrm(IntToStr((high(pieces)+1)*20));
  WStrm(':');
  for i:=0 to high(pieces) do WStrm(pieces[i].HashBin);

  WStrm('e');
  WStrm('e');
end;

constructor TTorrentPiece.Create(Hash, HashBin: String; Valid: Boolean);
begin
  _Hash := Hash;
  _HashBin := HashBin;
  _Valid := Valid;
  inherited Create();
end;

end.
