Skip to content
This repository has been archived by the owner on Oct 8, 2020. It is now read-only.

Commit

Permalink
MInimal extension support for index file parsing. (#173)
Browse files Browse the repository at this point in the history
* MInimal extension support for index file parsing.

Skip all optional extensions; error out if any required extensions are present.

Closes #67. Introduces #172 (required extensions unsupported).

* Clean up test case handling.
  • Loading branch information
scouten committed Sep 24, 2019
1 parent 6e88c21 commit f758e8b
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 18 deletions.
53 changes: 45 additions & 8 deletions lib/xgit/repository/working_tree/parse_index_file.ex
Expand Up @@ -9,6 +9,8 @@ defmodule Xgit.Repository.WorkingTree.ParseIndexFile do

import Xgit.Util.ForceCoverage

require Logger

alias Xgit.Core.DirCache
alias Xgit.Core.DirCache.Entry, as: DirCacheEntry
alias Xgit.Core.ObjectId
Expand All @@ -23,7 +25,7 @@ defmodule Xgit.Repository.WorkingTree.ParseIndexFile do
| :invalid_format
| :unsupported_version
| :too_many_entries
| :extensions_not_supported
| :unsupported_extension
| :sha_hash_mismatch
| File.posix()

Expand All @@ -49,9 +51,10 @@ defmodule Xgit.Repository.WorkingTree.ParseIndexFile do
entries. This is an arbitrary limit to guard against malformed files and to
prevent overconsumption of memory. With experience, it could be revisited.
`{:error, :extensions_not_supported}` if any index file extensions are present.
Parsing extensions is not yet supported. (See
[issue #67](https://github.com/elixir-git/xgit/issues/67).)
`{:error, :unsupported_extension}` if any index file extensions are present
that can not be parsed. Optional extensions will be skipped, but no required
extensions are understood at this time. (See
[issue #172](https://github.com/elixir-git/xgit/issues/172).)
`{:error, :sha_hash_mismatch}` if the SHA-1 hash written at the end of the file
does not match the file contents.
Expand All @@ -67,9 +70,7 @@ defmodule Xgit.Repository.WorkingTree.ParseIndexFile do
{:entry_count, read_uint32(iodevice)},
{:entries, entries} when is_list(entries) <-
{:entries, read_entries(iodevice, version, entry_count)},
{:extensions, :eof} <- {:extensions, IO.binread(iodevice, 1)},
# TO DO: Parse extensions. For now, error out if any are present.
# https://github.com/elixir-git/xgit/issues/67
{:extensions, :ok} <- {:extensions, read_extensions(iodevice)},
{:sha_valid?, true} <- {:sha_valid?, TrailingHashDevice.valid_hash?(iodevice)} do
cover {:ok,
%DirCache{
Expand All @@ -84,7 +85,7 @@ defmodule Xgit.Repository.WorkingTree.ParseIndexFile do
{:entry_count, :invalid} -> cover {:error, :invalid_format}
{:entry_count, _} -> cover {:error, :too_many_entries}
{:entries, _} -> cover {:error, :invalid_format}
{:extensions, _} -> cover {:error, :extensions_not_supported}
{:extensions, error} -> cover {:error, error}
{:sha_valid?, _} -> cover {:error, :sha_hash_mismatch}
end
end
Expand Down Expand Up @@ -154,6 +155,42 @@ defmodule Xgit.Repository.WorkingTree.ParseIndexFile do
defp valid_entry?(%DirCacheEntry{}), do: cover(true)
defp valid_entry?(_), do: cover(false)

defp read_extensions(iodevice) do
case IO.binread(iodevice, 1) do
:eof ->
:ok

char when byte_size(char) == 1 and char >= "A" and char <= "Z" ->
read_optional_extension(iodevice, char)

char ->
read_required_extension(iodevice, char)
end
end

defp read_optional_extension(iodevice, char) do
signature = "#{char}#{IO.binread(iodevice, 3)}"
length = read_uint32(iodevice)

Logger.info(fn ->
"skipping extension with signature #{inspect(signature)}, #{length} bytes"
end)

IO.binread(iodevice, length)
read_extensions(iodevice)
end

defp read_required_extension(iodevice, char) do
signature = "#{char}#{IO.binread(iodevice, 3)}"
length = read_uint32(iodevice)

Logger.info(fn ->
"don't know how to read required extension with signature #{inspect(signature)}, #{length} bytes"
end)

:unsupported_extension
end

defp read_uint16(iodevice) do
case IO.binread(iodevice, 2) do
x when is_binary(x) and byte_size(x) == 2 ->
Expand Down
100 changes: 90 additions & 10 deletions test/xgit/repository/working_tree/parse_index_file_test.exs
Expand Up @@ -5,6 +5,8 @@ defmodule Xgit.Repository.WorkingTree.ParseIndexFileTest do
alias Xgit.Repository.WorkingTree.ParseIndexFile
alias Xgit.Util.TrailingHashDevice

import ExUnit.CaptureLog

describe "from_iodevice/1" do
test "happy path: can read from command-line git (empty index)", %{ref: ref} do
{_output, 0} =
Expand Down Expand Up @@ -199,6 +201,78 @@ defmodule Xgit.Repository.WorkingTree.ParseIndexFileTest do
end
end

test "happy path: can skip TREE data structure" do
Enum.each(@names, fn name ->
Temp.track!()
tmp = Temp.mkdir!()

{_output, 0} =
System.cmd(
"git",
["init"],
cd: tmp
)

{_output, 0} =
System.cmd(
"git",
[
"update-index",
"--add",
"--cacheinfo",
"100644",
"18832d35117ef2f013c4009f5b2128dfaeff354f",
name
],
cd: tmp
)

# Calling `git write-tree` causes git to add a `TREE` extension into the index file.
# Test that we know how to read that and skip it.

{_output, 0} =
System.cmd(
"git",
["write-tree", "--missing-ok"],
cd: tmp
)

assert capture_log(fn ->
assert {:ok, index_file} =
[tmp, ".git", "index"]
|> Path.join()
|> thd_open_file!()
|> ParseIndexFile.from_iodevice()
end) =~ ~s(skipping extension with signature "TREE", 25 bytes)

assert index_file = %DirCache{
entries: [
%DirCache.Entry{
assume_valid?: false,
ctime: 0,
ctime_ns: 0,
dev: 0,
extended?: false,
gid: 0,
ino: 0,
intent_to_add?: false,
mode: 0o100644,
mtime: 0,
mtime_ns: 0,
name: :binary.bin_to_list(name),
object_id: "18832d35117ef2f013c4009f5b2128dfaeff354f",
size: 0,
skip_worktree?: false,
stage: 0,
uid: 0
}
],
entry_count: 1,
version: 2
}
end)
end

test "error: iodevice isn't a TrailingHashDevice" do
{:ok, pid} = GenServer.start_link(NotValid, nil)
assert {:error, :not_sha_hash_device} = ParseIndexFile.from_iodevice(pid)
Expand Down Expand Up @@ -299,16 +373,22 @@ defmodule Xgit.Repository.WorkingTree.ParseIndexFileTest do
])
end

test "error: extensions not supported" do
assert {:error, :extensions_not_supported} =
parse_iodata_as_index_file([
v2_header_with_valid_entry_through_file_size(),
Enum.map(1..20, fn n -> n end),
[0, 9],
'hello.txt',
0,
'abcd'
])
test "error: required extensions not supported" do
assert capture_log(fn ->
assert {:error, :unsupported_extension} =
parse_iodata_as_index_file([
v2_header_with_valid_entry_through_file_size(),
Enum.map(1..20, fn n -> n end),
[0, 9],
'hello.txt',
0,
'abcd',
0,
0,
0,
25
])
end) =~ ~s(don't know how to read required extension with signature "abcd", 25 bytes)
end

test "error: incorrect SHA-1 hash" do
Expand Down

0 comments on commit f758e8b

Please sign in to comment.