Can't get the proper hash of TorrentFile info

I’m stuck on Stage #RB2

I can’t get the expected hash value when hashing encoded info. Could anyone help me with this?

This is the result I get

Tracker URL: http://bittorrent-test-tracker.codecrafters.io/announce
Length: 92063
Info Hash: 7a4224da30feac31c30284c8d5fa8688c55c3dec

Here are my files

Program.cs

using System.Text.Json;
using BitTorrent.Helpers;
using BitTorrent.Models;

var (command, param) = args.Length switch
{
    0 => throw new InvalidOperationException("Usage: your_bittorrent.sh <command> <param>"),
    1 => throw new InvalidOperationException("Usage: your_bittorrent.sh <command> <param>"),
    _ => (args[0], args[1])
};

// Parse command and act accordingly
if (command == "decode")
{
    var bencodeDecoder = new BencodeDecoder(param);
    Console.WriteLine(bencodeDecoder.Parse());
}
else if (command == "info")
{
    using var stream = File.Open(param, FileMode.Open);

    var bencodeDecoder = new BencodeDecoder(stream);
    var parsedString = bencodeDecoder.Parse();

    Torrent torrentFile = JsonSerializer.Deserialize<Torrent>(parsedString)!;

    var encodedInfo = BencodeEncoder.EncodeDictionary(torrentFile.Info);

    var hash = Sha1Hash.ComputeHash(encodedInfo);

    Console.WriteLine("Tracker URL: {0}", torrentFile.Announce);
    Console.WriteLine("Length: {0}", torrentFile.Info.Length);
    Console.WriteLine("Info Hash: {0}", hash);
}
else
{
    throw new InvalidOperationException($"Invalid command: {command}");
}

BencodeDecoder.cs

using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;

namespace BitTorrent.Helpers
{
    public class BencodeDecoder
    {
        private readonly BinaryReader _reader;

        public BencodeDecoder(Stream stream)
        {
            _reader = new BinaryReader(stream, Encoding.UTF8);
        }

        public BencodeDecoder(string data)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(data);
            _reader = new BinaryReader(new MemoryStream(bytes), Encoding.UTF8);
        }

        public string Parse()
        {
            return JsonSerializer.Serialize(ParseValue());
        }

        private object ParseValue() 
        {
            char type = (char)_reader.PeekChar();
            if (type == 'i')
            {
                return ParseInt();
            }
            else if (type == 'l')
            {
                return ParseList();
            }
            else if (type == 'd')
            {
                return ParseDictionary();
            }
            else if (char.IsDigit(type))
            {
                return ParseString();
            }
            else
            {
                throw new InvalidOperationException("Unhandled encoded value");
            }
        }

        private long ParseInt() 
        {
            _reader.ReadChar(); // 'i'

            StringBuilder parsedInt = new();
            while (_reader.PeekChar() != 'e')
            {
                parsedInt.Append(_reader.ReadChar());
            }

            _reader.ReadChar(); // 'e'

            return long.Parse(parsedInt.ToString());
        }

        private List<object> ParseList()
        {
            _reader.ReadChar(); // 'l'

            List<object> parsedList = [];
            while (_reader.PeekChar() != 'e')
            {
                parsedList.Add(ParseValue());
            }

            _reader.ReadChar(); // 'e';

            return parsedList;
        }

        private string ParseString()
        {
            StringBuilder stringLength = new();
            while (_reader.PeekChar() != ':')
            {
                stringLength.Append(_reader.ReadChar());
            }
            int length = int.Parse(stringLength.ToString());

            _reader.ReadChar(); // ':'

            return new string(_reader.ReadChars(length));
        }

        private Dictionary<object, object> ParseDictionary() 
        {
            Dictionary<object, object> parsedDictionary = [];

            _reader.ReadChar(); // 'd';

            while (_reader.PeekChar() != 'e')
            {
                object key = ParseValue();
                object value = ParseValue();
                parsedDictionary.Add(key, value);
            }

            _reader.ReadChar(); // 'e';

            return parsedDictionary;
        }
    }
}

BencodeEncoder.cs

using System.Reflection;
using System.Text;
using BitTorrent.Extensions;

namespace BitTorrent.Helpers
{
    public static class BencodeEncoder
    {
        public static string EncodeDictionary<T>(T obj)
        {
            StringBuilder encodedDictionary = new();
            foreach (PropertyInfo prop in obj!.GetType().GetProperties())
            {
                var type = prop.PropertyType;
                
                encodedDictionary.Append(EncodeString(prop.Name.ToLowerCaseWithSpaces()));

                switch (Type.GetTypeCode(type))
                {
                    case TypeCode.Int16:
                    case TypeCode.Int32:
                    case TypeCode.Int64:
                        var intValue = Convert.ToInt64(prop.GetValue(obj, null));
                        encodedDictionary.Append(EncodeInteger(intValue));
                        break;

                    case TypeCode.String:
                        var stringValue = Convert.ToString(prop.GetValue(obj, null));
                        encodedDictionary.Append(EncodeString(stringValue!));
                        break;

                    default:
                        throw new InvalidOperationException(
                            string.Format("Type: {0} is not supported for bencode encoding", type.Name));

                }
            }

            return string.Format("d{0}e", encodedDictionary.ToString());
        }

        public static string EncodeInteger(long value)
        {
            return string.Format("i{0}e", value);
        }

        public static string EncodeString(string value)
        {
            return string.Format("{0}:{1}", value.Length, value);
        }
    }
}
1 Like

Hey @ArsenAghajanyan ,

Note that when encoding dictionaries, the keys need to be sorted first:

https://wiki.theory.org/BitTorrentSpecification

Does that help?

Hey @rohitpaulk

Thanks for reaching out!

I don’t think that’s the issue, because I’m using GetProperties() to encode the object, which returns properties in the order of declaration which was in correct order, that is length → name → piece length → pieces.

I do think something is wrong in my solution with UTF-8 parsing, that is I’m loosing data somewhere as I’m keeping everything in strings, but I can’t figure out where exactly :slight_smile:

2 Likes

Having the exact same problem… :melting_face:

This is unlikely to be the problem since you’re using C#, but in case it helps here’s something from a C user who ran into similar problems: Odd testing results for 6th stage - #10 by hipsteromer.

@ArsenAghajanyan if you manage to figure this out, please do leave a comment here to let us know!

Thanks @rohitpaulk !
I think it’s related.
What I did is instead of keeping torrent info as a string, I kept it as a byte and that solved the issue. So I would assume that it’s similar string manipulations that are causing issues when facing specific characters such as \x00

1 Like