diff options
author | TheArcaneBrony <myrainbowdash949@gmail.com> | 2023-06-05 03:25:53 +0200 |
---|---|---|
committer | TheArcaneBrony <myrainbowdash949@gmail.com> | 2023-06-05 03:25:53 +0200 |
commit | 51d820e22a4517dbb06d38a4f07f7c48522ef811 (patch) | |
tree | 4a7749cf77223dff2414fd4b73cb17df43d7449e /LibGit | |
download | GitTools-master.tar.xz |
Diffstat (limited to 'LibGit')
-rw-r--r-- | LibGit/BlobObject.cs | 71 | ||||
-rw-r--r-- | LibGit/CommitObject.cs | 171 | ||||
-rw-r--r-- | LibGit/Extensions/DictionaryExtensions.cs | 15 | ||||
-rw-r--r-- | LibGit/Extensions/HttpClientExtensions.cs | 19 | ||||
-rw-r--r-- | LibGit/Extensions/IEnumerableExtensions.cs | 45 | ||||
-rw-r--r-- | LibGit/Extensions/JsonElementExtensions.cs | 22 | ||||
-rw-r--r-- | LibGit/Extensions/ObjectExtensions.cs | 15 | ||||
-rw-r--r-- | LibGit/Extensions/QueueExtensions.cs | 26 | ||||
-rw-r--r-- | LibGit/Extensions/StreamExtensions.cs | 211 | ||||
-rw-r--r-- | LibGit/Extensions/StringExtensions.cs | 8 | ||||
-rw-r--r-- | LibGit/GitPack.cs | 135 | ||||
-rw-r--r-- | LibGit/GitRepo.cs | 141 | ||||
-rw-r--r-- | LibGit/Interfaces/IRepoSource.cs | 9 | ||||
-rw-r--r-- | LibGit/LibGit.csproj | 17 | ||||
-rw-r--r-- | LibGit/Static.cs | 6 | ||||
-rw-r--r-- | LibGit/TreeObject.cs | 101 |
16 files changed, 1012 insertions, 0 deletions
diff --git a/LibGit/BlobObject.cs b/LibGit/BlobObject.cs new file mode 100644 index 0000000..93fd5f3 --- /dev/null +++ b/LibGit/BlobObject.cs @@ -0,0 +1,71 @@ +using System.IO.Compression; +using System.Text.Json.Serialization; +using LibGit.Extensions; +using LibGit.Interfaces; + +namespace LibGit; + +public class BlobObject +{ + [JsonIgnore] + public IRepoSource RepoSource { get; } + public string ObjectId { get; } + + public BlobObject(IRepoSource repoSource, string objectId) + { + RepoSource = repoSource; + ObjectId = objectId; + } + + private const bool _debug = false; + + public string Length { get; set; } + public IEnumerable<byte> Data { get; set; } + + public BlobObject ReadFromZlibCompressedObjFile(Stream bytes) + { + if(_debug) Console.WriteLine($"Decompressing {this.GetType().Name}"); + using ZLibStream stream = new ZLibStream(bytes, CompressionMode.Decompress); + using var result = new MemoryStream(); + stream.CopyTo(result); + stream.Flush(); + stream.Close(); + return ReadFromDecompressedObjFile(result); + } + + public BlobObject ReadFromDecompressedObjFile(Stream data) + { + if(_debug) Console.WriteLine("Parsing commit object"); + // var data = new Queue<byte>(bytes.ToArray()); + int iters = 0; + data.Seek(0, SeekOrigin.Begin); + if(_debug)Console.WriteLine($"Iteration {iters}: starting pos: {data.Position}/+{data.Remaining()}/{data.Length}"); + while (data.Position < data.Length) + { + if(_debug) Console.WriteLine($"Iteration {iters} ({data.Position}/+{data.Remaining()}/{data.Length})"); + if (data.StartsWith("tree ")) + Length = data.ReadNullTerminatedField(asciiPrefix: "tree ").AsString(); + + Data = data.ReadBytes(data.Remaining()); + + if (data.Remaining() > 0) + { + Console.WriteLine($"--Unparsed data after {++iters} iteration(s) of parsing CommitObject--"); + Console.WriteLine(this.ToJson()); + Console.WriteLine("--HexDump of remaining data--"); + data.Peek(data.Remaining()).HexDump(); + //Console.WriteLine($"Unparsed data: {Encoding.UTF8.GetString(data.ToArray())}"); + } + if(iters > 100) throw new Exception("Too many iterations"); + } + + data.Close(); + return this; + } + + public class TreeObjectEntry + { + public string Mode { get; set; } + public string Hash { get; set; } + } +} \ No newline at end of file diff --git a/LibGit/CommitObject.cs b/LibGit/CommitObject.cs new file mode 100644 index 0000000..c2107df --- /dev/null +++ b/LibGit/CommitObject.cs @@ -0,0 +1,171 @@ +using System.IO.Compression; +using System.Text.Json.Serialization; +using LibGit.Extensions; +using LibGit.Interfaces; + +namespace LibGit; + +public class CommitObject +{ + [JsonIgnore] + public IRepoSource RepoSource { get; } + public string CommitId { get; } + + public CommitObject(IRepoSource repoSource, string commitId) + { + RepoSource = repoSource; + CommitId = commitId; + } + + private const bool _debug = false; + + public string Length { get; set; } + public string TreeId { get; set; } + public List<string> ParentIds { get; set; } = new(); + + public string Author { get; set; } + + //The Arcane Brony <myrainbowdash949@gmail.com> 1682901812 +0200 + public string Committer { get; set; } + public string GpgSignature { get; set; } + public string Message { get; set; } + public string Description { get; set; } + + public List<CommitObject> Children { get; set; } = new(); + + [JsonIgnore] + public DateTime CommitDate => new DateTime(long.Parse(Committer.Split(" ").TakeLast(2).First())*TimeSpan.TicksPerSecond) + .AddHours(long.Parse(Committer.Split(" ").TakeLast(1).First()[1..3])) + .AddMinutes(long.Parse(Committer.Split(" ").TakeLast(1).First()[3..])) + .AddYears(1970); + + [JsonIgnore] + public DateTime AuthorDate => new DateTime(long.Parse(Author.Split(" ").TakeLast(2).First())*TimeSpan.TicksPerSecond) + .AddHours(long.Parse(Committer.Split(" ").TakeLast(1).First()[1..3])) + .AddMinutes(long.Parse(Committer.Split(" ").TakeLast(1).First()[3..])) + .AddYears(1970); + + [JsonIgnore] + public string AuthorEmail => Author.Split(" <").Last().Split("> ").First(); + + [JsonIgnore] + public string AuthorName => Author.Split(" <").First(); + + [JsonIgnore] + public string CommitterEmail => Committer.Split(" <").Last().Split("> ").First(); + + [JsonIgnore] + public string CommitterName => Committer.Split(" <").First(); + + public async Task<TreeObject> GetTreeAsync() => new TreeObject(RepoSource, TreeId).ReadFromZlibCompressedObjFile(await RepoSource.GetObjectStreamById(TreeId)); + + public CommitObject ReadFromZlibCompressedObjFile(Stream bytes) + { + if (_debug) + { + Console.WriteLine($"Decompressing {this.GetType().Name}"); + Console.WriteLine($"File header:"); + // bytes.Peek(64).HexDump(16); + } + using ZLibStream stream = new ZLibStream(bytes, CompressionMode.Decompress); + using var result = new MemoryStream(); + stream.CopyTo(result); + stream.Flush(); + stream.Close(); + return ReadFromDecompressedObjFile(result); + } + + public CommitObject ReadFromDecompressedObjFile(Stream data) + { + if(_debug) Console.WriteLine("Parsing commit object"); + // var data = new Queue<byte>(bytes.ToArray()); + int iters = 0; + data.Seek(0, SeekOrigin.Begin); + if(_debug)Console.WriteLine($"Iteration {iters}: starting pos: {data.Position}/+{data.Remaining()}/{data.Length}"); + Length = data.ReadNullTerminatedField(asciiPrefix: "commit ").AsString(); + while (data.Position < data.Length) + { + if(_debug) Console.WriteLine($"Iteration {iters} ({data.Position}/+{data.Remaining()}/{data.Length})"); + if (data.StartsWith("tree ")) + TreeId = data.ReadTerminatedField((byte)'\n', asciiPrefix: "tree ").AsString(); + while (data.StartsWith("parent ")) + ParentIds.Add(data.ReadTerminatedField((byte)'\n', asciiPrefix: "parent ").AsString()); + if (data.StartsWith("author ")) + Author = data.ReadTerminatedField((byte)'\n', asciiPrefix: "author ").AsString(); + if (data.StartsWith("committer ")) + Committer = data.ReadTerminatedField((byte)'\n', asciiPrefix: "committer ").AsString();; + if (data.StartsWith("gpgsig ")) + GpgSignature = GetGpgSignature(data); + if(data.Remaining() >= 1 && (data.Peek() == (byte)'\n' || data.Peek() == (byte)'\r')) + data.Skip(1); + if(data.Peek() != (byte)'\n') + Message = GetMessage(data); + while(data.Remaining() >= 1 && (data.Peek() == (byte)'\n' || data.Peek() == (byte)'\r')) + data.Skip(1); + if(data.Remaining() >= 1 && data.Peek() != (byte)'\n') + Description = GetDescription(data); + + if (data.Remaining() > 0) + { + Console.WriteLine($"--Unparsed data after {++iters} iteration(s) of parsing CommitObject--"); + Console.WriteLine(this.ToJson()); + Console.WriteLine("--HexDump of remaining data--"); + data.Peek(data.Remaining()).HexDump(); + //Console.WriteLine($"Unparsed data: {Encoding.UTF8.GetString(data.ToArray())}"); + } + if(iters > 100) throw new Exception("Too many iterations"); + } + + data.Close(); + return this; + } + + //parsing + private string GetMessage(Stream data) + { + if(_debug) Console.WriteLine($"--commit.GetMessage--"); + var message = ""; + while (data.Remaining() > 0 && data.Peek() != (byte)'\n') + { + if(_debug) Console.WriteLine($"Commit.GetMessage -- pos: {data.Position}/+{data.Remaining()}/{data.Length} | next: {(char)data.Peek()} | Message: {message}"); + message += (char)data.ReadByte(); + + } + + // if(data.Count > 0 && data.Peek() == (byte)'\n') + // data.Dequeue(); + return message.Trim(); + } + + private string GetDescription(Stream data) + { + if(_debug) Console.WriteLine($"--commit.GetDescription--"); + var message = ""; + while (data.Remaining() > 0) + { + message += (char)data.ReadByte(); + } + + // if(data.Count > 0 && data.Peek() == (byte)'\n') + // data.Dequeue(); + return message.Trim(); + } + + private string GetGpgSignature(Stream data) + { + if(_debug) Console.WriteLine($"--commit.GetGpgSignature--"); + data.Seek(7, SeekOrigin.Current); + var signature = ""; + while ( + !signature.EndsWith("-----END PGP SIGNATURE-----\n\n") + && !signature.EndsWith("-----END PGP SIGNATURE-----\n \n") + && !signature.EndsWith("-----END SSH SIGNATURE-----\n\n") + ) + { + // Console.Write((char)data.Peek()); + signature += (char)data.ReadByte(); + } + + return signature; + } +} \ No newline at end of file diff --git a/LibGit/Extensions/DictionaryExtensions.cs b/LibGit/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000..0e6df06 --- /dev/null +++ b/LibGit/Extensions/DictionaryExtensions.cs @@ -0,0 +1,15 @@ +namespace LibGit.Extensions; + +public static class DictionaryExtensions +{ + public static bool ChangeKey<TKey, TValue>(this IDictionary<TKey, TValue> dict, + TKey oldKey, TKey newKey) + { + TValue value; + if (!dict.Remove(oldKey, out value)) + return false; + + dict[newKey] = value; // or dict.Add(newKey, value) depending on ur comfort + return true; + } +} \ No newline at end of file diff --git a/LibGit/Extensions/HttpClientExtensions.cs b/LibGit/Extensions/HttpClientExtensions.cs new file mode 100644 index 0000000..79e3e5f --- /dev/null +++ b/LibGit/Extensions/HttpClientExtensions.cs @@ -0,0 +1,19 @@ +namespace LibGit.Extensions; + +public static class HttpClientExtensions +{ + public static async Task<bool> CheckSuccessStatus(this HttpClient hc, string url) + { + //cors causes failure, try to catch + try + { + var resp = await hc.GetAsync(url); + return resp.IsSuccessStatusCode; + } + catch (Exception e) + { + Console.WriteLine($"Failed to check success status: {e.Message}"); + return false; + } + } +} \ No newline at end of file diff --git a/LibGit/Extensions/IEnumerableExtensions.cs b/LibGit/Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000..d8fc54d --- /dev/null +++ b/LibGit/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,45 @@ +using System.IO.Compression; +using System.Text; + +namespace LibGit.Extensions; + +public static class IEnumerableExtensions +{ + public static bool StartsWith<T>(this IEnumerable<T> list, IEnumerable<T> prefix) + { + return prefix.SequenceEqual(list.Take(prefix.Count())); + } + + public static void HexDump(this IEnumerable<byte> bytes, int width = 32) + { + var data = new Queue<(string hex, char utf8)>(bytes.ToArray().Select(x => ($"{x:X2}", (char)x)).ToArray()); + while (data.Count > 0) + { + var line = data.Dequeue(Math.Min(width, data.Count)).ToArray(); + Console.WriteLine( + string.Join(" ", line.Select(x => x.hex)).PadRight(width * 3) + + " | " + + string.Join("", line.Select(x => x.utf8)) + .Replace('\n', '.') + .Replace('\r', '.') + .Replace('\0', '.') + ); + } + } + + public static string AsHexString(this IEnumerable<byte> bytes) => string.Join(' ', bytes.Select(x => $"{x:X2}")); + public static string AsString(this IEnumerable<byte> bytes) => Encoding.UTF8.GetString(bytes.ToArray()); + + + //zlib decompress + public static byte[] ZlibDecompress(this IEnumerable<byte> bytes) + { + var inStream = new MemoryStream(bytes.ToArray()); + using ZLibStream stream = new ZLibStream(inStream, CompressionMode.Decompress); + using var result = new MemoryStream(); + stream.CopyTo(result); + stream.Flush(); + stream.Close(); + return result.ToArray(); + } +} \ No newline at end of file diff --git a/LibGit/Extensions/JsonElementExtensions.cs b/LibGit/Extensions/JsonElementExtensions.cs new file mode 100644 index 0000000..dd97013 --- /dev/null +++ b/LibGit/Extensions/JsonElementExtensions.cs @@ -0,0 +1,22 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace LibGit.Extensions; + +public static class JsonElementExtensions +{ + public static void FindExtraJsonFields([DisallowNull] this JsonElement? res, Type t) + { + var props = t.GetProperties(); + var unknownPropertyFound = false; + foreach (var field in res.Value.EnumerateObject()) + { + if (props.Any(x => x.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name == field.Name)) continue; + Console.WriteLine($"[!!] Unknown property {field.Name} in {t.Name}!"); + unknownPropertyFound = true; + } + if(unknownPropertyFound) Console.WriteLine(res.Value.ToJson()); + } +} \ No newline at end of file diff --git a/LibGit/Extensions/ObjectExtensions.cs b/LibGit/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..2cdeccd --- /dev/null +++ b/LibGit/Extensions/ObjectExtensions.cs @@ -0,0 +1,15 @@ +using System.Text.Json; + +namespace LibGit.Extensions; + +public static class ObjectExtensions +{ + public static string ToJson(this object obj, bool indent = true, bool ignoreNull = false, bool unsafeContent = false) + { + var jso = new JsonSerializerOptions(); + if(indent) jso.WriteIndented = true; + if(ignoreNull) jso.IgnoreNullValues = true; + if(unsafeContent) jso.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + return JsonSerializer.Serialize(obj, jso); + } +} \ No newline at end of file diff --git a/LibGit/Extensions/QueueExtensions.cs b/LibGit/Extensions/QueueExtensions.cs new file mode 100644 index 0000000..45b8093 --- /dev/null +++ b/LibGit/Extensions/QueueExtensions.cs @@ -0,0 +1,26 @@ +namespace LibGit.Extensions; + +public static class QueueExtensions +{ +public static IEnumerable<T> Dequeue<T>(this Queue<T> queue, int count) +{ + for (int i = 0; i < count; i++) + { + yield return queue.Dequeue(); + } +} + public static IEnumerable<T> Peek<T>(this Queue<T> queue, int count) + { + for (int i = 0; i < count; i++) + { + yield return queue.ElementAt(i); + } + } + public static void Drop<T>(this Queue<T> queue, int count) + { + for (int i = 0; i < count; i++) + { + queue.Dequeue(); + } + } +} \ No newline at end of file diff --git a/LibGit/Extensions/StreamExtensions.cs b/LibGit/Extensions/StreamExtensions.cs new file mode 100644 index 0000000..6555783 --- /dev/null +++ b/LibGit/Extensions/StreamExtensions.cs @@ -0,0 +1,211 @@ +using System.IO.Compression; + +namespace LibGit.Extensions; + +public static class StreamExtensions +{ + private const bool _debug = false; + + public static long Remaining(this Stream stream) + { + //if (_debug) Console.WriteLine($"stream pos: {stream.Position}, stream len: {stream.Length}, stream rem: {stream.Length - stream.Position}"); + return stream.Length - stream.Position; + } + + public static int Peek(this Stream stream) + { + if (!stream.CanRead) + throw new InvalidOperationException("Can't peek a non-readable stream"); + if (!stream.CanSeek) + throw new InvalidOperationException("Can't peek a non-seekable stream"); + + int peek = stream.ReadByte(); + if (peek != -1) + stream.Seek(-1, SeekOrigin.Current); + + return peek; + } + + public static IEnumerable<byte> Peek(this Stream stream, long count) + { + if (!stream.CanRead) + throw new InvalidOperationException("Can't peek a non-readable stream"); + if (!stream.CanSeek) + throw new InvalidOperationException("Can't peek a non-seekable stream"); + long i; + for (i = 0; i < count; i++) + { + int peek = stream.ReadByte(); + if (peek == -1) + { + if(_debug) Console.WriteLine($"Can't peek {count} bytes, only {i} bytes remaining"); + stream.Seek(-i, SeekOrigin.Current); + yield break; + } + + yield return (byte)peek; + } + stream.Seek(-i, SeekOrigin.Current); + } + + public static IEnumerable<byte> ReadBytes(this Stream stream, long count) + { + if (!stream.CanRead) + throw new InvalidOperationException("Can't read a non-readable stream"); + if (!stream.CanSeek) + throw new InvalidOperationException("Can't read a non-seekable stream"); + + for (long i = 0; i < count; i++) + { + int read = stream.ReadByte(); + if (read == -1) + yield break; + yield return (byte)read; + } + } + + public static bool StartsWith(this Stream stream, IEnumerable<byte> sequence) + { + if (!stream.CanRead) + throw new InvalidOperationException("Can't read a non-readable stream"); + if (!stream.CanSeek) + throw new InvalidOperationException("Can't read a non-seekable stream"); + + if (_debug) + { + Console.WriteLine($"Expected: {sequence.AsHexString()} ({sequence.AsString()})"); + Console.WriteLine($"Actual: {stream.Peek(sequence.Count()).AsHexString()} ({stream.Peek(sequence.Count()).AsString()})"); + } + + int readCount = 0; + foreach (int b in sequence) + { + int read = stream.ReadByte(); + readCount++; + if (read == -1) + { + stream.Seek(-readCount, SeekOrigin.Current); + return false; + } + + if (read != b) + { + if (_debug) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("^^".PadLeft(readCount * 3 + 9)); + Console.ResetColor(); + } + + stream.Seek(-readCount, SeekOrigin.Current); + return false; + } + } + stream.Seek(-readCount, SeekOrigin.Current); + + return true; + } + + public static bool StartsWith(this Stream stream, string ascii_seq) + { + return stream.StartsWith(ascii_seq.Select(x => (byte)x)); + } + + public static Stream Skip(this Stream stream, long count = 1) + { + if (!stream.CanSeek) + throw new InvalidOperationException("Can't skip a non-seekable stream"); + stream.Seek(count, SeekOrigin.Current); + return stream; + } + + public static IEnumerable<byte> ReadNullTerminatedField(this Stream stream, IEnumerable<byte>? binaryPrefix = null, string? asciiPrefix = null) => ReadTerminatedField(stream: stream, terminator: 0x00, binaryPrefix: binaryPrefix, asciiPrefix: asciiPrefix); + public static IEnumerable<byte> ReadSpaceTerminatedField(this Stream stream, IEnumerable<byte>? binaryPrefix = null, string? asciiPrefix = null) => ReadTerminatedField(stream: stream, terminator: 0x20, binaryPrefix: binaryPrefix, asciiPrefix: asciiPrefix); + public static IEnumerable<byte> ReadTerminatedField(this Stream stream, byte terminator, IEnumerable<byte>? binaryPrefix = null, string? asciiPrefix = null) + { + if (!stream.CanRead) + throw new InvalidOperationException("Can't read a non-readable stream"); + if (binaryPrefix != null) + if (!stream.StartsWith(binaryPrefix)) + throw new InvalidDataException($"Binary prefix {stream.Peek(binaryPrefix.Count()).AsHexString()} does not match expected value of {binaryPrefix.AsHexString()}!"); + else stream.Skip(binaryPrefix.Count()); + else if (asciiPrefix != null) + if (!stream.StartsWith(asciiPrefix)) + throw new InvalidDataException($"Text prefix {stream.Peek(asciiPrefix.Length).AsHexString()} ({stream.Peek(asciiPrefix.Length).AsString()}) does not match expected value of {asciiPrefix.AsBytes().AsHexString()} ({asciiPrefix})!"); + else stream.Skip(asciiPrefix.Length); + + var read = 0; + while (stream.Peek() != terminator) + { + if (_debug) Console.WriteLine($"ReadTerminatedField -- pos: {stream.Position}/+{stream.Remaining()}/{stream.Length} | next: {(char)stream.Peek()} | Length: {read}"); + if (stream.Peek() == -1) + { + Console.WriteLine($"Warning: Reached end of stream while reading null-terminated field"); + yield break; + } + + read++; + yield return (byte)stream.ReadByte(); + } + + if (stream.Peek() == terminator) stream.Skip(); + } + + public static IEnumerable<byte> ReadToEnd(this Stream stream) + { + if (!stream.CanRead) + throw new InvalidOperationException("Can't read a non-readable stream"); + + while (stream.Peek() != -1) + yield return (byte)stream.ReadByte(); + } + + public static int ReadInt32BE(this Stream stream) + { + if (!stream.CanRead) + throw new InvalidOperationException("Can't read a non-readable stream"); + + var bytes = stream.ReadBytes(4).ToArray(); + + if (BitConverter.IsLittleEndian) + Array.Reverse(bytes); + + Console.WriteLine("ReadInt32BE: " + bytes.AsHexString() + " => " + BitConverter.ToInt32(bytes)); + return BitConverter.ToInt32(bytes); + } + + //read variable length number + public static int ReadVLQ(this Stream stream) + { + if (!stream.CanRead) + throw new InvalidOperationException("Can't read a non-readable stream"); + + int result = 0; + int shift = 0; + byte b; + do + { + b = (byte)stream.ReadByte(); + result |= (b & 0b0111_1111) << shift; + shift += 7; + } while ((b & 0b1000_0000) != 0); + + return result; + } + public static int ReadVLQBigEndian(this Stream stream) + { + if (!stream.CanRead) + throw new InvalidOperationException("Can't read a non-readable stream"); + + int result = 0; + int shift = 0; + byte b; + do + { + b = (byte)stream.ReadByte(); + result = (result << 7) | (b & 0b0111_1111); + } while ((b & 0b1000_0000) != 0); + + return result; + } +} \ No newline at end of file diff --git a/LibGit/Extensions/StringExtensions.cs b/LibGit/Extensions/StringExtensions.cs new file mode 100644 index 0000000..f33bd4e --- /dev/null +++ b/LibGit/Extensions/StringExtensions.cs @@ -0,0 +1,8 @@ +using System.Text; + +namespace LibGit.Extensions; + +public static class StringExtensions +{ + public static IEnumerable<byte> AsBytes(this string str) => Encoding.UTF8.GetBytes(str); +} \ No newline at end of file diff --git a/LibGit/GitPack.cs b/LibGit/GitPack.cs new file mode 100644 index 0000000..58c6edc --- /dev/null +++ b/LibGit/GitPack.cs @@ -0,0 +1,135 @@ +using LibGit.Extensions; + +namespace LibGit; + +public class GitPack +{ + public string PackId { get; set; } + public GitRepo Repo { get; set; } + + public int Version { get; set; } + public int ObjectCount { get; set; } + public List<GitPackObject> Objects { get; set; } = new List<GitPackObject>(); + + public GitPack Read(Stream stream) + { + stream.Peek(12).HexDump(16); + + Console.Write(" Header: "); stream.Peek(04).ToArray()[0..].HexDump(4); + Console.Write("Version: "); stream.Peek(08).ToArray()[4..].HexDump(4); + Console.Write(" ObjCnt: "); stream.Peek(12).ToArray()[8..].HexDump(4); + + if(!stream.StartsWith("PACK")) + throw new Exception("Invalid pack file header"); + + stream.Skip(4); + + Version = stream.ReadInt32BE(); + ObjectCount = stream.ReadInt32BE(); + Console.WriteLine($"Got git v{Version} pack with {ObjectCount} objects"); + for (int i = 0; i < ObjectCount; i++) + { + Objects.Add(new GitPackObject().Read(stream)); + } + + return this; + } + + public GitPack(string packId, GitRepo repo) + { + PackId = packId; + Repo = repo; + } +} + +public class GitPackIndex +{ + public int Version { get; set; } + public int[] fanOutTable = new int[256]; + public GitPackIndex Read(Stream stream) + { + if(!stream.StartsWith(new byte[]{0xff,0x74,0x4f,0x63})) + throw new Exception("Invalid pack index file header or pack is v1"); + + stream.Skip(4); + Version = stream.ReadInt32BE(); + Console.WriteLine($"Got git v{Version} pack index"); + + //fan-out table + for (int i = 0; i < 256; i++) + { + fanOutTable[i] = stream.ReadInt32BE(); + } + + + + + return this; + } +} + + +public class GitPackObject +{ + private const bool _debug = true; + public GitPackObject Read(Stream stream) + { + stream.Peek(64).HexDump(32); + var header = stream.ReadBytes(4).ToArray(); + ObjType = (GitObjectType)((header[0] & 0b0111_0000) >> 4); + if(ObjType == 0 || (int)ObjType == 5 || (int)ObjType > 7) + throw new Exception($"Invalid object type: {(int)ObjType}"); + Size = header[0] & 0b0000_1111; + + Offset = 0; + for (int i = 1; i < 4; i++) + { + Offset <<= 8; + Offset |= header[i]; + } + + if ((Size & 0b0000_1000) != 0) + { + Size <<= 4; + Size |= stream.ReadVLQ(); + } + + // ObjType = Type switch + // { + // 1 => GitObjectType.Commit, + // 2 => GitObjectType.Tree, + // 3 => GitObjectType.Blob, + // 4 => GitObjectType.Tag, + // 5 => GitObjectType.Invalid, + // 6 => GitObjectType.OffsDelta, + // 7 => GitObjectType.RefDelta, + // _ => throw new Exception($"Invalid object type {Type}") + // }; + + if(_debug) Console.WriteLine($"pack obj type: {ObjType} ({(int)ObjType}), size: {Size}, offset: {Offset}, sizeBytes: {SizeBytes}"); + Console.WriteLine("Data: "); + stream.Peek(Size).Take(16).ToArray().HexDump(16); + stream.ReadBytes(Size).ZlibDecompress().Take(16).HexDump(16); + + return this; + } + + public GitObjectType ObjType { get; set; } + + public int SizeBytes { get; set; } + + public int Size { get; set; } + + public int Offset { get; set; } +} + +public enum GitObjectType +{ + Commit = 1, + Tree, + Blob, + Tag, + Invalid, // Reserved for future expansion, see https://git-scm.com/docs/pack-format#_object_types + OffsDelta, + RefDelta +} \ No newline at end of file diff --git a/LibGit/GitRepo.cs b/LibGit/GitRepo.cs new file mode 100644 index 0000000..a58bc38 --- /dev/null +++ b/LibGit/GitRepo.cs @@ -0,0 +1,141 @@ +using LibGit.Extensions; +using LibGit.Interfaces; + +namespace LibGit; + +public class GitRepo +{ + public IRepoSource RepoSource; + + public string Name { get; set; } + // public string Description + // { + // get => File.ReadAllText(Path.Join(RepoPath, "description")); + // set => File.WriteAllText(Path.Join(RepoPath, "description"), value); + // } + + public async Task<CommitObject> GetCommit(string commitId) + { + if (!commitId.Length.Equals(40)) commitId = await ResolveRef(commitId); + commitId = commitId.Trim(' ', '\n', '\r', '\t'); + string path = Path.Join("objects", commitId[..2], commitId[2..]); + var commit = new CommitObject(RepoSource, commitId).ReadFromZlibCompressedObjFile(await RepoSource.GetFileStream(path)); + return commit; + } + + public async IAsyncEnumerable<CommitObject> GetCommits(string commitId, int limit = Int32.MaxValue) + { + if (!commitId.Length.Equals(40)) commitId = await ResolveRef(commitId); + commitId = commitId.Trim(' ', '\n', '\r', '\t'); + var commit = await GetCommit(commitId); + int i = 0; + while (commit.ParentIds.Count > 0 && i++ < limit) + { + yield return commit; + commit = await GetCommit(commit.ParentIds[0]); + } + + yield return commit; + Console.WriteLine($"Reached last commit: {commit.CommitId} ({commit.Message})"); + } + + public async IAsyncEnumerable<GitRef> GetRefs() + { + var fs = await RepoSource.GetFileStream("info/refs"); + while (fs.Remaining() > 0) + { + yield return new GitRef( + commitId: fs.ReadTerminatedField((byte)'\t').AsString(), + name: fs.ReadTerminatedField((byte)'\n').AsString(), + repo: this + ); + } + } + + public async IAsyncEnumerable<GitPack> GetPacks() + { + var fs = await RepoSource.GetFileStream("objects/info/packs"); + Console.WriteLine("Found packs file:"); + fs.Peek(32).HexDump(32); + if (fs.Length <= 1) + { + Console.WriteLine("WARNING: No packs found!"); + yield break; + } + while (fs.Remaining() > 0 && fs.Peek() != 0x0A) + { + //example: P pack-24bd1c46d657f74f40629503d8e5083a9ad36a67.pack + var line = fs.ReadTerminatedField((byte)'\n').AsString(); + if (line.StartsWith("P ")) + { + new GitPackIndex().Read(await RepoSource.GetFileStream($"objects/pack/{line[2..].Replace(".pack", ".idx")}")); + yield return new GitPack( + packId: line[2..], + repo: this + ).Read(await RepoSource.GetFileStream($"objects/pack/{line[2..]}")); + } + else + { + Console.WriteLine($"WARNING: Unknown pack line: {line}"); + } + } + } + + + //utilities + public async Task<string> ResolveRef(string commitRef) + { + if (commitRef == "HEAD") + { + var head = (await RepoSource.GetFileStream("HEAD")).ReadToEnd().AsString(); + if (head.StartsWith("ref: ")) + { + return await ResolveRef(head[5..].Trim(' ', '\n', '\r', '\t')); + } + else + { + Console.WriteLine($"Found unknown HEAD style: {head}"); + return head; + } + } + + if (commitRef.StartsWith("refs/")) + { + return (await RepoSource.GetFileStream(commitRef)).ReadToEnd().AsString().Trim(' ', '\n', '\r', '\t'); + } + + throw new ArgumentException($"Unknown commit ref: {commitRef}"); + } + + public GitRepo(IRepoSource repoSource) + { + RepoSource = repoSource; + } +} + +public class GitRef +{ + public string Name { get; set; } + public string CommitId { get; set; } + public GitRepo Repo { get; set; } + + public GitRef(string name, string commitId, GitRepo repo) + { + Name = name; + CommitId = commitId; + Repo = repo; + } + + public async Task<CommitObject> GetCommit() + { + return await Repo.GetCommit(CommitId); + } + + public async IAsyncEnumerable<CommitObject> GetCommits(int limit = Int32.MaxValue) + { + await foreach (var commit in Repo.GetCommits(CommitId, limit)) + { + yield return commit; + } + } +} \ No newline at end of file diff --git a/LibGit/Interfaces/IRepoSource.cs b/LibGit/Interfaces/IRepoSource.cs new file mode 100644 index 0000000..e276f60 --- /dev/null +++ b/LibGit/Interfaces/IRepoSource.cs @@ -0,0 +1,9 @@ +namespace LibGit.Interfaces; + +public interface IRepoSource +{ + public string BasePath { get; set; } + public Task<Stream> GetFileStream(string path); + + public Task<Stream> GetObjectStreamById(string objectId) => GetFileStream(Path.Join("objects", objectId[..2], objectId[2..])); +} \ No newline at end of file diff --git a/LibGit/LibGit.csproj b/LibGit/LibGit.csproj new file mode 100644 index 0000000..ac74b55 --- /dev/null +++ b/LibGit/LibGit.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net7.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> + <DebugType>none</DebugType> + <DebugSymbols>false</DebugSymbols> + <Optimize>true</Optimize> + <OutputPath>bin\Release\</OutputPath> + <WholeProgramOptimization>true</WholeProgramOptimization> + </PropertyGroup> + +</Project> diff --git a/LibGit/Static.cs b/LibGit/Static.cs new file mode 100644 index 0000000..ec67750 --- /dev/null +++ b/LibGit/Static.cs @@ -0,0 +1,6 @@ +namespace LibGit; + +public class Static +{ + public const bool Debug = false; +} \ No newline at end of file diff --git a/LibGit/TreeObject.cs b/LibGit/TreeObject.cs new file mode 100644 index 0000000..7d5eb36 --- /dev/null +++ b/LibGit/TreeObject.cs @@ -0,0 +1,101 @@ +using System.IO.Compression; +using System.Text.Json.Serialization; +using LibGit.Extensions; +using LibGit.Interfaces; + +namespace LibGit; + +public class TreeObject +{ + [JsonIgnore] public IRepoSource RepoSource { get; } + public string ObjectId { get; } + + public TreeObject(IRepoSource repoSource, string objectId) + { + RepoSource = repoSource; + ObjectId = objectId; + } + + private const bool _debug = false; + + public string Length { get; set; } + public Dictionary<string, TreeObjectEntry> Entries { get; set; } = new(); + + public TreeObject ReadFromZlibCompressedObjFile(Stream bytes) + { + if (_debug) Console.WriteLine($"Decompressing {GetType().Name}"); + using ZLibStream stream = new ZLibStream(bytes, CompressionMode.Decompress); + using var result = new MemoryStream(); + stream.CopyTo(result); + stream.Flush(); + stream.Close(); + return ReadFromDecompressedObjFile(result); + } + + public TreeObject ReadFromDecompressedObjFile(Stream data) + { + if (_debug) Console.WriteLine("Parsing tree object"); + // var data = new Queue<byte>(bytes.ToArray()); + int iters = 0; + data.Seek(0, SeekOrigin.Begin); + if (_debug) Console.WriteLine($"Iteration {iters}: starting pos: {data.Position}/+{data.Remaining()}/{data.Length}"); + Length = data.ReadNullTerminatedField(asciiPrefix: "tree ").AsString(); + while (data.Remaining() > 20) + { + if (_debug) Console.WriteLine($"readTree.Iteration {iters} ({data.Position}/+{data.Remaining()}/{data.Length})"); + + var entry = ReadFileEntry(data); + Entries.Add(entry.Key, entry.Value); + } + + if (data.Remaining() > 0) + { + Console.WriteLine($"--parseTree: Unparsed data after {iters} iteration(s) of parsing TreeObject--"); + Console.WriteLine(this.ToJson()); + Console.WriteLine("--HexDump of remaining data--"); + data.Peek(data.Remaining()).HexDump(); + //Console.WriteLine($"Unparsed data: {Encoding.UTF8.GetString(data.ToArray())}"); + } + + data.Close(); + + if (_debug) + { + Console.WriteLine($"-- Read tree object of size {Length} --"); + foreach (var x in Entries.ToList()) + { + Console.WriteLine($"{x.Value.Mode} {x.Value.Hash} {x.Key}"); + } + + foreach (var x in Entries.ToList()) + { + Console.WriteLine($"Path: {x.Key}"); + Console.WriteLine($"Mode: {x.Value.Mode}"); + Console.WriteLine($"Hash: {x.Value.Hash}"); + } + } + + return this; + } + + //parsing + + private static KeyValuePair<string, TreeObjectEntry> ReadFileEntry(Stream data) + { + if (_debug) Console.WriteLine($"--tree.ReadFileEntry--"); + var path = ""; + TreeObjectEntry entry = new(); + //entry format: <mode> <path>\0<hash> + entry.Mode = data.ReadSpaceTerminatedField().AsString(); + path = data.ReadNullTerminatedField().AsString(); + entry.Hash = string.Join("", data.ReadBytes(20).Select(x => $"{x:x2}")); + + return new KeyValuePair<string, TreeObjectEntry>(path, entry); + } + + public class TreeObjectEntry + { + public string Mode { get; set; } + public string Hash { get; set; } + } +} \ No newline at end of file |