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

Commit

Permalink
Merge 773e9e0 into e0d77ec
Browse files Browse the repository at this point in the history
  • Loading branch information
scouten committed Aug 28, 2019
2 parents e0d77ec + 773e9e0 commit 730edd9
Show file tree
Hide file tree
Showing 8 changed files with 372 additions and 334 deletions.
16 changes: 7 additions & 9 deletions lib/xgit/core/dir_cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ defmodule Xgit.Core.DirCache do
use Bitwise
use Xgit.Core.FileMode

alias Xgit.Core.FilePath
alias Xgit.Util.Comparison

@typedoc ~S"""
Expand Down Expand Up @@ -60,6 +61,8 @@ defmodule Xgit.Core.DirCache do

use Xgit.Core.FileMode

alias Xgit.Core.FileMode
alias Xgit.Core.FilePath
alias Xgit.Core.ObjectId

@typedoc ~S"""
Expand All @@ -73,7 +76,7 @@ defmodule Xgit.Core.DirCache do
## Struct Members
* `name`: entry path name, relative to top-level directory (without leading slash)
* `name`: (FilePath.t) entry path name, relative to top-level directory (without leading slash)
* `stage`: 0..3 merge status
* `object_id`: (ObjectId.t) SHA-1 for the represented object
* `mode`: (FileMode.t)
Expand All @@ -92,7 +95,7 @@ defmodule Xgit.Core.DirCache do
* `intent_to_add?`: (boolean)
"""
@type t :: %__MODULE__{
name: [byte],
name: FilePath.t(),
stage: 0..3,
object_id: ObjectId.t(),
mode: FileMode.t(),
Expand Down Expand Up @@ -133,10 +136,6 @@ defmodule Xgit.Core.DirCache do
intent_to_add?: false
]

alias Xgit.Core.FileMode
alias Xgit.Core.ObjectId
alias Xgit.Core.ValidatePath

@doc ~S"""
Return `true` if this entry struct describes a valid dir cache entry.
"""
Expand Down Expand Up @@ -182,8 +181,7 @@ defmodule Xgit.Core.DirCache do
is_boolean(extended?) and
is_boolean(skip_worktree?) and
is_boolean(intent_to_add?) do
ValidatePath.check_path(name) == :ok && ObjectId.valid?(object_id) &&
object_id != ObjectId.zero()
FilePath.valid?(name) && ObjectId.valid?(object_id) && object_id != ObjectId.zero()
end

def valid?(_), do: false
Expand Down Expand Up @@ -320,7 +318,7 @@ defmodule Xgit.Core.DirCache do
@typedoc ~S"""
An entry for the `remove` option for `remove_entries/2`.
"""
@type entry_to_remove :: {path :: [byte], stage :: 0..3 | :all}
@type entry_to_remove :: {path :: FilePath.t(), stage :: 0..3 | :all}

@typedoc ~S"""
Error reason codes returned by `remove_entries/2`.
Expand Down
164 changes: 158 additions & 6 deletions lib/xgit/core/validate_path.ex → lib/xgit/core/file_path.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Copyright (C) 2008-2010, Google Inc.
# Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
# Copyright (C) 2016, 2018 Google Inc.
# and other copyright owners as documented in the project's IP log.
#
# Elixir adaptation from jgit file:
# Elixir adaptation from jgit files:
# org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
# org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java
#
# Copyright (C) 2019, Eric Scouten <eric+xgit@scouten.com>
#
Expand Down Expand Up @@ -45,10 +47,56 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

defmodule Xgit.Core.ValidatePath do
defmodule Xgit.Core.FilePath do
@moduledoc ~S"""
Verifies that a path is an acceptable part of a tree structure.
Describes a file path as stored in a git repo.
Paths are always stored as a list of bytes. The git specification
does not explicitly specify an encoding, but most commonly the
path is interpreted as UTF-8.
We use byte lists here to avoid confusion and possible misintepretation
in Elixir's `String` type for non-UTF-8 paths.
Paths are alternately referred to in git as "file name," "path,"
"path name," and "object name." We're using the name `FilePath`
to avoid collision with Elixir's built-in `Path` module and to make
it clear that we're talking about the path to where a file is stored
on disk.
"""

use Bitwise
use Xgit.Core.FileMode

alias Xgit.Util.Comparison

@typedoc """
Representation of a file's path within a git repo.
Typically, though not necessarily, interpreted as UTF-8.
"""
@type t :: [byte]

@doc ~S"""
Return `true` if the value is a valid file path.
This performs the same checks as `check_path/2`, but folds away all of the potential
error values to `false`.
## Parameters
`path` is a UTF-8 byte list containing the path to be tested.
## Options
* `windows?`: `true` to additionally verify that the path is permissible on Windows file systems
* `macosx?`: `true` to additionally verify that the path is permissible on Mac OS X file systems
"""
@spec valid?(path :: any, windows?: boolean, macosx?: boolean) :: boolean
def valid?(path, opts \\ [])

def valid?(path, opts) when is_list(path) and is_list(opts), do: check_path(path, opts) == :ok
def valid?(_path, _opts), do: false

@typedoc ~S"""
Error codes which can be returned by `check_path/2`.
Expand Down Expand Up @@ -93,7 +141,7 @@ defmodule Xgit.Core.ValidatePath do
See also: error return values from `check_path_segment/2`.
"""
@spec check_path(path :: [byte], windows?: boolean, macosx?: boolean) ::
@spec check_path(path :: t, windows?: boolean, macosx?: boolean) ::
:ok | {:error, check_path_reason} | {:error, check_path_segment_reason}
def check_path(path, opts \\ [])

Expand Down Expand Up @@ -145,7 +193,7 @@ defmodule Xgit.Core.ValidatePath do
* `{:error, :windows_device_name}` if the name matches a Windows device name (`aux`, etc.)
(only when `windows?: true` is selected)
"""
@spec check_path_segment(path :: [byte], windows?: boolean, macosx?: boolean) ::
@spec check_path_segment(path :: t, windows?: boolean, macosx?: boolean) ::
:ok | {:error, check_path_segment_reason}
def check_path_segment(path, opts \\ [])

Expand Down Expand Up @@ -336,7 +384,7 @@ defmodule Xgit.Core.ValidatePath do
* `macosx?`: `true` to additionally check for any path that might be treated
as a `.gitmodules` file on Mac OS X file systems
"""
@spec gitmodules?(path :: [byte], windows?: boolean, macosx?: boolean) :: boolean
@spec gitmodules?(path :: t, windows?: boolean, macosx?: boolean) :: boolean
def gitmodules?(path, opts \\ [])

def gitmodules?('.gitmodules', opts) when is_list(opts), do: true
Expand Down Expand Up @@ -467,4 +515,108 @@ defmodule Xgit.Core.ValidatePath do

defp to_lower(b) when b >= ?A and b <= ?Z, do: b + 32
defp to_lower(b), do: b

@doc ~S"""
Remove trailing `/` if present.
"""
@spec strip_trailing_separator(path :: t) :: t
def strip_trailing_separator([]), do: []

def strip_trailing_separator(path) when is_list(path) do
if List.last(path) == ?/ do
path
|> Enum.reverse()
|> Enum.drop_while(&(&1 == ?/))
|> Enum.reverse()
else
path
end
end

@doc ~S"""
Compare two paths according to git path sort ordering rules.
## Return Value
* `:lt` if `path1` sorts before `path2`.
* `:eq` if they are the same.
* `:gt` if `path1` sorts after `path2`.
"""
@spec compare(
path1 :: t,
mode1 :: FileMode.t(),
path2 :: t,
mode2 :: FileMode.t()
) :: Comparison.result()
def compare(path1, mode1, path2, mode2)
when is_list(path1) and is_file_mode(mode1) and is_list(path2) and is_file_mode(mode2) do
case core_compare(path1, mode1, path2, mode2) do
:eq -> mode_compare(mode1, mode2)
x -> x
end
end

@doc ~S"""
Compare two paths, checking for identical name.
Unlike `compare/4`, this method returns `:eq` when the paths have
the same characters in their names, even if the mode differs. It is
intended for use in validation routines detecting duplicate entries.
## Parameters
`mode2` is the mode of the second file. Trees are sorted as though
`List.last(path2) == ?/`, even if no such character exists.
## Return Value
Returns `:eq` if the names are identical and a conflict exists
between `path1` and `path2`, as they share the same name.
Returns `:lt` if all possible occurrences of `path1` sort
before `path2` and no conflict can happen. In a properly sorted
tree there are no other occurrences of `path1` and therefore there
are no duplicate names.
Returns `:gt` when it is possible for a duplicate occurrence of
`path1` to appear later, after `path2`. Callers should
continue to examine candidates for `path2` until the method returns
one of the other return values.
"""
@spec compare_same_name(path1 :: t, path2 :: t, mode2 :: FileMode.t()) ::
Comparison.result()
def compare_same_name(path1, path2, mode2),
do: core_compare(path1, FileMode.tree(), path2, mode2)

defp core_compare(path1, mode1, path2, mode2)

defp core_compare([c | rem1], mode1, [c | rem2], mode2),
do: core_compare(rem1, mode1, rem2, mode2)

defp core_compare([c1 | _rem1], _mode1, [c2 | _rem2], _mode2),
do: compare_chars(c1, c2)

defp core_compare([c1 | _rem1], _mode1, [], mode2),
do: compare_chars(band(c1, 0xFF), last_path_char(mode2))

defp core_compare([], mode1, [c2 | _], _mode2),
do: compare_chars(last_path_char(mode1), band(c2, 0xFF))

defp core_compare([], _mode1, [], _mode2), do: :eq

defp compare_chars(c, c), do: :eq
defp compare_chars(c1, c2) when c1 < c2, do: :lt
defp compare_chars(_, _), do: :gt

defp last_path_char(mode) do
if FileMode.tree?(mode),
do: ?/,
else: 0
end

defp mode_compare(mode1, mode2) do
if FileMode.gitlink?(mode1) or FileMode.gitlink?(mode2),
do: :eq,
else: compare_chars(last_path_char(mode1), last_path_char(mode2))
end
end
17 changes: 8 additions & 9 deletions lib/xgit/core/validate_object.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@ defmodule Xgit.Core.ValidateObject do
"""

alias Xgit.Core.FileMode
alias Xgit.Core.FilePath
alias Xgit.Core.Object
alias Xgit.Core.ObjectId
alias Xgit.Core.ValidatePath
alias Xgit.Util.Paths
alias Xgit.Util.RawParseUtils

import Xgit.Util.RawParseUtils, only: [after_prefix: 2]
Expand Down Expand Up @@ -163,14 +162,14 @@ defmodule Xgit.Core.ValidateObject do
`{:error, :invalid_mode}` if the object is a tree and one of the file modes is incomplete.
See also error responses from `Xgit.Core.ValidatePath.check_path/2` and
`Xgit.Core.ValidatePath.check_path_segment/2`.
See also error responses from `Xgit.Core.FilePath.check_path/2` and
`Xgit.Core.FilePath.check_path_segment/2`.
"""
@spec check(object :: Object.t(), opts :: Keyword.t()) ::
:ok
| {:error, reason :: check_reason}
| {:error, reason :: ValidatePath.check_path_reason()}
| {:error, reason :: ValidatePath.check_path_segment_reason()}
| {:error, reason :: FilePath.check_path_reason()}
| {:error, reason :: FilePath.check_path_segment_reason()}
def check(object, opts \\ [])

def check(%Object{type: :blob}, _opts), do: :ok
Expand Down Expand Up @@ -261,7 +260,7 @@ defmodule Xgit.Core.ValidateObject do
with {:file_mode, {:ok, file_mode, data}} <- {:file_mode, check_file_mode(data, 0)},
{:file_mode, true} <- {:file_mode, FileMode.valid?(file_mode)},
{:path_split, {path_segment, [0 | data]}} <- {:path_split, path_and_object_id(data)},
{:path_valid, :ok} <- {:path_valid, ValidatePath.check_path_segment(path_segment, opts)},
{:path_valid, :ok} <- {:path_valid, FilePath.check_path_segment(path_segment, opts)},
{:duplicate, false} <-
{:duplicate, maybe_mapset_member?(maybe_normalized_paths, path_segment, opts)},
{:duplicate, false} <- {:duplicate, duplicate_name?(path_segment, data)},
Expand Down Expand Up @@ -319,7 +318,7 @@ defmodule Xgit.Core.ValidateObject do

data = Enum.drop(data, 1)

compare = Paths.compare_same_name(this_name, next_name, mode)
compare = FilePath.compare_same_name(this_name, next_name, mode)

cond do
Enum.empty?(mode_str) or Enum.empty?(next_name) -> false
Expand All @@ -339,7 +338,7 @@ defmodule Xgit.Core.ValidateObject do
defp correctly_sorted?([], _previous_mode, _this_name, _this_mode), do: true

defp correctly_sorted?(previous_name, previous_mode, this_name, this_mode),
do: Paths.compare(previous_name, previous_mode, this_name, this_mode) != :gt
do: FilePath.compare(previous_name, previous_mode, this_name, this_mode) != :gt

defp maybe_put_path(nil, _path_segment, _opts), do: nil

Expand Down
10 changes: 5 additions & 5 deletions lib/xgit/plumbing/hash_object.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ defmodule Xgit.Plumbing.HashObject do
"""

alias Xgit.Core.ContentSource
alias Xgit.Core.FilePath
alias Xgit.Core.Object
alias Xgit.Core.ObjectId
alias Xgit.Core.ObjectType
alias Xgit.Core.ValidateObject
alias Xgit.Core.ValidatePath
alias Xgit.Repository

@doc ~S"""
Expand Down Expand Up @@ -52,16 +52,16 @@ defmodule Xgit.Plumbing.HashObject do
`{:error, :reason}` if unable. The relevant reason codes may come from:
* `Xgit.Core.FilePath.check_path/2`
* `Xgit.Core.FilePath.check_path_segment/2`
* `Xgit.Core.ValidateObject.check/2`
* `Xgit.Core.ValidatePath.check_path/2`
* `Xgit.Core.ValidatePath.check_path_segment/2`
* `Xgit.Repository.put_loose_object/2`.
"""
@spec run(content :: ContentSource.t(), type: ObjectType.t() | nil) ::
{:ok, ObjectId.t()}
| {:error, reason :: ValidateObject.check_reason()}
| {:error, reason :: ValidatePath.check_path_reason()}
| {:error, reason :: ValidatePath.check_path_segment_reason()}
| {:error, reason :: FilePath.check_path_reason()}
| {:error, reason :: FilePath.check_path_segment_reason()}
| {:error, reason :: Repository.put_loose_object_reason()}
def run(content, opts \\ []) when not is_nil(content) and is_list(opts) do
%{type: type, validate?: validate?, repo: repo, write?: write?} = validate_options(opts)
Expand Down
9 changes: 4 additions & 5 deletions lib/xgit/plumbing/update_index/cache_info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ defmodule Xgit.Plumbing.UpdateIndex.CacheInfo do
use Xgit.Core.FileMode

alias Xgit.Core.DirCache.Entry, as: DirCacheEntry
alias Xgit.Core.FilePath
alias Xgit.Core.ObjectId
alias Xgit.Core.ValidatePath
alias Xgit.Repository
alias Xgit.Repository.WorkingTree

@typedoc ~S"""
Cache info tuple `{mode, object_id, path}` to add to the index file.
"""
@type add_entry :: {mode :: FileMode.t(), object_id :: ObjectId.t(), path :: [byte]}
@type add_entry :: {mode :: FileMode.t(), object_id :: ObjectId.t(), path :: FilePath.t()}

@typedoc ~S"""
Reason codes that can be returned by `run/2`.
Expand Down Expand Up @@ -82,9 +82,8 @@ defmodule Xgit.Plumbing.UpdateIndex.CacheInfo do
end

defp valid_add?({mode, object_id, path})
when is_file_mode(mode) and is_binary(object_id) and is_list(path) do
ObjectId.valid?(object_id) and ValidatePath.check_path(path)
end
when is_file_mode(mode) and is_binary(object_id) and is_list(path),
do: ObjectId.valid?(object_id) and FilePath.valid?(path)

defp valid_add?(_), do: false

Expand Down
Loading

0 comments on commit 730edd9

Please sign in to comment.