MarioFlach

MarioFlach

Parsing Git Packfile variant length headers

I’m implementing a pure Elixir Git server (supporting SSH and HTTP) for fun an profit.

One of the tedious tasks i’m currently working on is the parsing of Git packfiles.

The pack-format documentation states following:

  packed object header:
      1-byte size extension bit (MSB)
           type (next 3 bit)
           size0 (lower 4-bit)
      n-byte sizeN (as long as MSB is set, each 7-bit)
   		   size0..sizeN form 4+7+7+..+7 bit integer, size0
    	   is the least significant part, and sizeN is the
    	   most significant part.

With Elixir we have the possibility to pattern-match on bitstrings which is great. In order to find the variant size of an object, i came with following implementation:

  # MSB is not set, size is contained in four bits.
  defp unpack_obj_head(<<0::1, type::3, size::4, rest::binary>>) do
    {type, size, rest}
  end

  # MSB is set, read next byte
  defp unpack_obj_head(<<1::1, type::3, obj_num::bitstring-4, rest::binary>>) do
    {size, rest} = unpack_obj_size(rest, obj_num)
    {type, size, rest}
  end

  # MSB is not set, calculate size based on acc_num and obj_num
  defp unpack_obj_size(<<0::1, obj_num::bitstring-7, rest::binary>>, acc_num) do
    with acc <- <<acc_num::bitstring, obj_num::bitstring>>,
         len <- bit_size(acc),
       <<num::integer-size(len)>> <- acc, do: {num, rest}
  end

  # MSB is set, read next byte
  defp unpack_obj_size(<<1::1, obj_num::bitstring-7, rest::binary>>, acc_num) do
    unpack_obj_size(rest, <<acc_num::bitstring, obj_num::bitstring>>)
  end

But I’m missing something here, the result size does not match the size object body…
Clearly, I’m doing something wrong here:

with acc <- <<acc_num::bitstring, obj_num::bitstring>>,
     len <- bit_size(acc),
   # next line is not correct, we need to shift things here
   <<num::integer-size(len)>> <- acc, do: {num, rest}

I came across a great article, git clone in Haskell from the bottom up. It states:

The overall length is then: size0 + size1 + … + sizeN.
After shifting each part 0, 4 + (n-1) * 7 to the left. size0 is the least, sizeN the most significant part.

Is there a simple way to achieve this with Elixir?

Most Liked

MarioFlach

MarioFlach

With the help of @nox on the elixir IRC channel, I finally got it working properly.

The solution was to

  1. left-shift each length part with 4+7*i.
  2. add the result to the accumulator.
  defp unpack_obj_head(<<0::1, type::3, size::4, rest::binary>>) do
    {type, size, rest}
  end

  defp unpack_obj_head(<<1::1, type::3, num::4, rest::binary>>) do
    {size, rest} = unpack_obj_size(rest, num, 0)
    {type, size, rest}
  end

  defp unpack_obj_size(<<0::1, num::7, rest::binary>>, acc, i) do
    {acc + (num <<< 4+7*i), rest}
  end

  defp unpack_obj_size(<<1::1, num::7, rest::binary>>, acc, i) do
    unpack_obj_size(rest, acc + (num <<< (4+7*i)), i+1)
  end
MarioFlach

MarioFlach

If someone has some Haskell knowledges, I’m stuck with the next parsing problem: Extract the offset and size length in bits for delta copy commands.

Git deltas are a chain of copy/insert commands to apply to an existing Git object. Git uses a variant length to store offset and size.

Basically, my Elixir implementation so far looks like this:

  # MSB is not set, this is an INSERT command.
  defp unpack_obj_delta_hunk(<<0::1, size::7, data::binary-size(size), rest::binary>>, cmds) do
    IO.puts "insert #{inspect data}"
    unpack_obj_delta_hunk(rest, [{:insert, data}|cmds])
  end

  # MSB is set, this is a COPY command.
  defp unpack_obj_delta_hunk(<<1::1, _::1, 0::1, 1::1, offset::4, size::8, rest::binary>>, cmds) do
    IO.puts "copy from byte #{offset} to #{size}"
    unpack_obj_delta_hunk(rest, [{:copy, {offset, size}}|cmds])
  end

In order to parse correctly a COPY command, i need to parse offset and size length in bits.
This information is stored in the first byte:

Bit 1: MSB, used to check if the command is COPY or INSERT.
Bit 2: Is always 0
Bit 3-4: Big question mark, this seems to tell how long (in bits) offset and size are.
Bit 4-8: Offset (or not, depending on bits 3-4).

I do have an Haskell implementation code but I do not fully understand it:

I someone can help me on this, I would be more than happy =)

Where Next?

Popular in Questions Top

chokchit
** (DBConnection.ConnectionError) connection not available and request was dropped from queue after 2733ms. You can configure how long re...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&amp;query=perfume&amp;page=2, I would like to get: ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
yawaramin
In the Dialyzer docs ( dialyzer — OTP 29.0.2 (dialyzer 6.0.1) ), there is a way to turn off a specific warning for a function: -dialyzer...
New

Other popular topics Top

Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43806 214
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

We're in Beta

About us Mission Statement