]> gitweb.ps.run Git - ziggit/blob - git.zig
add decompress
[ziggit] / git.zig
1 const std = @import("std");
2
3 const Alloc = std.mem.Allocator;
4 const Reader = std.io.AnyReader;
5 const Writer = std.io.AnyWriter;
6
7 const MaxFileSize = 1024 * 1024;
8
9 const Id = u160;
10 const Commit = struct {
11     author: []u8,
12     message: []u8,
13     parent: Id,
14     tree: Id,
15 };
16 const Blob = struct {
17     data: []u8,
18 };
19 const Object = struct {
20     kind: u3,
21     data: []u8,
22
23     pub fn init(kind: u3, data: []u8) Object {
24         return .{
25             .kind = kind,
26             .data = data,
27         };
28     }
29     // pub fn getCommit(self: *Object) Commit {}
30     // pub fn getBlob(self: *Object) Blob {}
31 };
32
33 fn decompress(alloc: Alloc, r: Reader) ![]u8 {
34     var buffer = std.ArrayList(u8).init(alloc);
35
36     try std.compress.zlib.decompress(r, buffer.writer().any());
37
38     return alloc.realloc(buffer.allocatedSlice(), buffer.items.len);
39 }
40
41 const PackFile = struct {
42     alloc: Alloc,
43     idxFile: std.fs.File,
44     pckFile: std.fs.File,
45     objectOffsets: std.AutoArrayHashMap(Id, u32),
46
47     pub fn open(alloc: Alloc, dir: std.fs.Dir) !?PackFile {
48         var self = PackFile{
49             .alloc = alloc,
50             .idxFile = undefined,
51             .pckFile = undefined,
52             .objectOffsets = std.AutoArrayHashMap(Id, u32).init(alloc),
53         };
54
55         var packDir = try dir.openDir("objects/pack", .{ .iterate = true });
56         defer packDir.close();
57
58         var packFileFound = false;
59
60         var packIt = packDir.iterate();
61         while (try packIt.next()) |f| {
62             if (std.mem.endsWith(u8, f.name, ".idx")) {
63                 const idxFilename = f.name;
64                 var pckFilename = try std.BoundedArray(u8, std.fs.max_path_bytes).init(0);
65                 try std.fmt.format(
66                     pckFilename.writer(),
67                     "{s}.pack",
68                     .{idxFilename[0 .. idxFilename.len - 4]},
69                 );
70
71                 self.idxFile = try packDir.openFile(idxFilename, .{});
72                 self.pckFile = try packDir.openFile(pckFilename.constSlice(), .{});
73
74                 try self.parseIndex();
75
76                 packFileFound = true;
77             }
78         }
79
80         if (!packFileFound)
81             return null;
82
83         return self;
84     }
85
86     pub fn close(self: *PackFile) void {
87         self.objectOffsets.deinit();
88         self.idxFile.close();
89         self.pckFile.close();
90     }
91
92     pub fn parseIndex(self: *PackFile) !void {
93         const idxReader = self.idxFile.reader().any();
94
95         var fanoutTable: [256]u32 = undefined;
96
97         for (0..256) |i| {
98             try self.idxFile.seekTo(8 + i * 4);
99             fanoutTable[i] = try idxReader.readVarInt(u32, .big, 4);
100
101             const numObjects =
102                 if (i > 0) fanoutTable[i] - fanoutTable[i - 1] else fanoutTable[i];
103
104             for (0..numObjects) |j| {
105                 const idOffset =
106                     4 + 4 + 4 * 256 + (j + if (i > 0) fanoutTable[i - 1] else 0) * 20;
107                 try self.idxFile.seekTo(idOffset);
108                 const id = try idxReader.readVarInt(Id, .big, 20);
109
110                 try self.objectOffsets.put(id, 0);
111             }
112         }
113
114         const numObjects = self.objectOffsets.keys().len;
115         for (0..numObjects) |i| {
116             const offsetOffset =
117                 4 + 4 + 4 * 256 + numObjects * (20 + 4) + i * 4;
118             try self.idxFile.seekTo(offsetOffset);
119             const offset = try idxReader.readVarInt(u32, .big, 4);
120
121             self.objectOffsets.values()[i] = offset;
122         }
123     }
124
125     fn getSize(reader: Reader, ignoreTypeBits: bool) !struct { size: u64, bytelen: u64 } {
126         var size: u64 = 0;
127         var counter: u6 = 0;
128         while (true) {
129             const byte = try reader.readByte();
130
131             if (counter == 0) {
132                 if (ignoreTypeBits) {
133                     const bits: u4 = @truncate(byte);
134                     size = bits;
135                 } else {
136                     const bits: u7 = @truncate(byte);
137                     size = bits;
138                 }
139             } else {
140                 if (ignoreTypeBits) {
141                     const bits: u7 = @truncate(byte);
142                     size += @as(u64, bits) << (7 * (counter - 1) + 4);
143                 } else {
144                     const bits: u7 = @truncate(byte);
145                     size += @as(u64, bits) << (7 * (counter));
146                 }
147             }
148
149             if (byte & 0b10000000 == 0) {
150                 break;
151             }
152
153             counter += 1;
154         }
155
156         const nBytes = counter + 1;
157
158         return .{
159             .size = size,
160             .bytelen = nBytes,
161         };
162     }
163
164     fn getOffset(reader: Reader) !struct { offset: u64, bytelen: u64 } {
165         var offset: u64 = 0;
166         var counter: u4 = 0;
167         while (true) {
168             const byte = try reader.readByte();
169
170             const bits: u7 = @truncate(byte);
171             offset <<= 7;
172             offset += @as(u64, bits);
173
174             if (byte & 0b10000000 == 0) {
175                 break;
176             }
177
178             counter += 1;
179         }
180
181         const nBytes = counter + 1;
182
183         if (nBytes >= 2) {
184             for (1..nBytes) |i| {
185                 offset += std.math.pow(u64, 2, 7 * i);
186             }
187         }
188         return .{
189             .offset = offset,
190             .bytelen = nBytes,
191         };
192     }
193
194     fn applyDelta(alloc: Alloc, baseData: []const u8, deltData: []const u8) ![]u8 {
195         var fbs = std.io.fixedBufferStream(deltData);
196         const deltDataReader = fbs.reader().any();
197         const baseObjectSize = try getSize(deltDataReader, false);
198         const resultObjectSize = try getSize(deltDataReader, false);
199         const deltaDataOffset = baseObjectSize.bytelen + resultObjectSize.bytelen;
200
201         const result = try alloc.alloc(u8, resultObjectSize.size);
202         var resultCounter: u64 = 0;
203
204         var counter: u64 = 0;
205         while (true) {
206             const b = deltData[deltaDataOffset + counter];
207
208             if (b & 0b10000000 != 0) {
209                 var dataOffset: u64 = 0;
210                 var dataSize: u64 = 0;
211                 var bitsSet: u8 = 0;
212                 for (0..4) |i| { // offset bits
213                     if (b & (@as(u64, 1) << @min(3, i)) != 0) {
214                         dataOffset += @as(u64, deltData[deltaDataOffset + counter + 1 + bitsSet]) << @min(3 * 8, i * 8);
215                         bitsSet += 1;
216                     }
217                 }
218                 for (4..7) |i| { // size bits
219                     if (b & (@as(u64, 1) << @min(6, i)) != 0) {
220                         dataSize += @as(u64, deltData[deltaDataOffset + counter + 1 + bitsSet]) << @min(6 * 8, (i - 4) * 8);
221                         bitsSet += 1;
222                     }
223                 }
224                 counter += bitsSet;
225
226                 std.mem.copyForwards(
227                     u8,
228                     result[resultCounter..result.len],
229                     baseData[dataOffset .. dataOffset + dataSize],
230                 );
231
232                 resultCounter += dataSize;
233             } else {
234                 const dataSize: u7 = @truncate(b);
235
236                 std.mem.copyForwards(
237                     u8,
238                     result[resultCounter..result.len],
239                     deltData[deltaDataOffset + counter + 1 .. deltaDataOffset + counter + 1 + dataSize],
240                 );
241                 resultCounter += dataSize;
242                 counter += dataSize;
243             }
244
245             counter += 1;
246             if (deltaDataOffset + counter >= deltData.len)
247                 break;
248         }
249
250         return result;
251     }
252
253     fn ofsDelta(self: *PackFile, offset: i64) anyerror!Object {
254         const pckReader = self.pckFile.reader().any();
255
256         const pos = try self.pckFile.getPos();
257
258         try self.pckFile.seekBy(-offset);
259         const baseObject = try self.readObject(pckReader);
260         defer self.alloc.free(baseObject.data);
261
262         try self.pckFile.seekTo(pos);
263         const deltaData = try decompress(self.alloc, pckReader);
264         defer self.alloc.free(deltaData);
265
266         const objectData = try applyDelta(self.alloc, baseObject.data, deltaData);
267         return Object.init(baseObject.kind, objectData);
268     }
269
270     fn readObject(self: *PackFile, reader: Reader) anyerror!Object {
271         const firstByte = try reader.readByte();
272         const objectKind: u3 = @truncate(firstByte >> 4);
273         try self.pckFile.seekBy(-1);
274         const objectSize = try getSize(reader, true);
275
276         if (objectKind == 6) {
277             const offset = try getOffset(reader);
278             return try self.ofsDelta(
279                 @intCast(offset.offset + objectSize.bytelen + offset.bytelen),
280             );
281         } else {
282             const objectData = try decompress(self.alloc, reader);
283             return Object.init(objectKind, objectData);
284         }
285     }
286
287     pub fn getObject(self: *PackFile, id: Id) !?Object {
288         if (self.objectOffsets.get(id)) |offset| {
289             const pckReader = self.pckFile.reader().any();
290             try self.pckFile.seekTo(offset);
291
292             return try self.readObject(pckReader);
293         }
294         return null;
295     }
296 };
297
298 const Repo = struct {
299     alloc: Alloc,
300     dir: std.fs.Dir,
301     packfile: ?PackFile,
302
303     pub fn open(alloc: Alloc, path: []const u8) !Repo {
304         const dir = try std.fs.cwd().openDir(path, .{});
305
306         const packfile = try PackFile.open(alloc, dir);
307
308         return .{
309             .alloc = alloc,
310             .dir = dir,
311             .packfile = packfile,
312         };
313     }
314
315     pub fn close(self: *Repo) void {
316         self.dir.close();
317         if (self.packfile != null) {
318             self.packfile.?.close();
319         }
320     }
321
322     pub fn getHead(self: *Repo) !Id {
323         // read file HEAD
324         const head = try self.dir.readFileAlloc(self.alloc, "HEAD", 1024);
325         defer self.alloc.free(head);
326
327         // read file pointed at by HEAD
328         const headPath = head[5 .. head.len - 1];
329         var idBuffer: [40]u8 = undefined;
330         const idStr = try self.dir.readFile(headPath, &idBuffer);
331
332         // parse id from file
333         return try std.fmt.parseUnsigned(u160, idStr, 16);
334     }
335
336     pub fn getObject(self: *Repo, id: Id) !?Object {
337         if (self.packfile) |*packfile| {
338             return packfile.getObject(id);
339         }
340         return null;
341     }
342 };
343
344 test "print HEAD" {
345     var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
346     defer repo.close();
347
348     const head = try repo.getHead();
349
350     std.debug.print("HEAD: {}\n", .{head});
351 }
352
353 test "parse idx" {
354     var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
355     defer repo.close();
356
357     if (repo.packfile) |packfile| {
358         std.debug.print("{}\n", .{packfile.objectOffsets.keys().len});
359         std.debug.print("{}\n", .{packfile.objectOffsets.values().len});
360     }
361 }
362
363 test "get object" {
364     var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
365     defer repo.close();
366
367     const head = try repo.getHead();
368
369     if (try repo.getObject(head)) |o| {
370         defer std.testing.allocator.free(o.data);
371
372         std.debug.print("object: {s}\n", .{o.data});
373     }
374 }
375
376 test "get tree" {
377     var repo = try Repo.open(std.testing.allocator, "../imgui/.git");
378     defer repo.close();
379
380     if (try repo.getObject(0xceb2b2c62d6f8f3686dcacecd5be931839b02c77)) |o| {
381         defer std.testing.allocator.free(o.data);
382
383         std.debug.print("tree: {s}\n", .{o.data});
384     }
385 }
386 // test "list commits" {
387 //     var repo = Repo.open(std.testing.allocator, "../imgui/.git");
388 //     defer repo.close();
389
390 //     const head = repo.getObject(repo.head);
391 //     defer head.deinit();
392
393 //     var c = head.getCommit();
394 //     for (0..3) |_| {
395 //         std.debug.print("{}\n", .{c});
396 //         c = c.parent;
397 //     }
398 // }
399
400 // test "tree" {
401 //     var repo = Repo.open(std.testing.allocator, "../imgui/.git");
402 //     defer repo.close();
403
404 //     const head = repo.getObject(repo.head);
405 //     defer head.deinit();
406
407 //     const commit = head.getCommit();
408
409 //     std.debug.print("{}\n", .{commit.tree});
410 // }
411
412 // test "blob" {
413 //     var repo = Repo.open(std.testing.allocator, "../imgui/.git");
414 //     defer repo.close();
415
416 //     const head = repo.getObject(repo.head);
417 //     defer head.deinit();
418
419 //     const commit = head.getCommit();
420 //     const blob = repo.getBlob(commit.files[0].id);
421
422 //     std.debug.print("{}\n", .{blob});
423 // }