hrx-1.0.0/ 0000755 0001750 0001750 00000000000 13641040325 010731 5 ustar foka foka hrx-1.0.0/lib/ 0000755 0001750 0001750 00000000000 13641040325 011477 5 ustar foka foka hrx-1.0.0/lib/hrx.rb 0000644 0001750 0001750 00000001607 13641040325 012631 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# The parent module for all HRX classes. See the README for more information and
# usage examples.
#
# Parse an archive from a string with HRX::Archive.parse, load it from disk with
# HRX::Archive.load, or create an empty archive with HRX::Archive.new.
module HRX; end
require_relative 'hrx/archive'
hrx-1.0.0/lib/hrx/ 0000755 0001750 0001750 00000000000 13641040325 012300 5 ustar foka foka hrx-1.0.0/lib/hrx/parse_error.rb 0000644 0001750 0001750 00000002407 13641040325 015153 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require_relative 'error'
# An error caused by an HRX file failing to parse correctly.
class HRX::ParseError < HRX::Error
# The 1-based line of the document on which the error occurred.
attr_reader :line
# The 1-based column of the line on which the error occurred.
attr_reader :column
# The file which failed to parse, or `nil` if the filename isn't known.
attr_reader :file
def initialize(message, line, column, file: nil)
super(message)
@line = line
@column = column
@file = file
end
def to_s
buffer = String.new("Parse error on line #{line}, column #{column}")
buffer << " of #{file}" if file
buffer << ": #{super.to_s}"
end
end
hrx-1.0.0/lib/hrx/file.rb 0000644 0001750 0001750 00000005317 13641040325 013552 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require_relative 'util'
# A file in an HRX archive.
class HRX::File
# The comment that appeared before this file, or `nil` if it had no
# preceding comment.
#
# HRX comments are always encoded as UTF-8.
#
# This string is frozen.
attr_reader :comment
# The path to this file, relative to the archive's root.
#
# HRX paths are always `/`-separated and always encoded as UTF-8.
#
# This string is frozen.
attr_reader :path
# The contents of the file.
#
# HRX file contents are always encoded as UTF-8.
#
# This string is frozen.
attr_reader :content
# Creates a new file with the given path, content, and comment.
#
# Throws an HRX::ParseError if `path` is invalid, or an EncodingError if any
# argument can't be converted to UTF-8.
def initialize(path, content, comment: nil)
@comment = comment&.clone&.encode("UTF-8")&.freeze
@path = HRX::Util.scan_path(StringScanner.new(path.encode("UTF-8"))).freeze
if @path.end_with?("/")
raise HRX::ParseError.new("path \"#{path}\" may not end with \"/\"", 1, path.length - 1)
end
@content = content.clone.encode("UTF-8").freeze
end
# Like ::new, but doesn't verify that the arguments are valid.
def self._new_without_checks(path, content, comment) # :nodoc:
allocate.tap do |file|
file._initialize_without_checks(path, content, comment)
end
end
# Like #initialize, but doesn't verify that the arguments are valid.
def _initialize_without_checks(path, content, comment) # :nodoc:
@comment = comment.freeze
@path = path.freeze
@content = content.freeze
end
# Returns a copy of this entry with the path modified to be relative to
# `root`.
#
# If `root` is `nil`, returns this as-is.
def _relative(root) # :nodoc:
return self unless root
HRX::File._new_without_checks(HRX::Util.relative(root, path), content, comment)
end
# Returns a copy of this entry with `root` added tothe beginning of the path.
#
# If `root` is `nil`, returns this as-is.
def _absolute(root) # :nodoc:
return self unless root
HRX::File._new_without_checks(root + path, content, comment)
end
end
hrx-1.0.0/lib/hrx/archive.rb 0000644 0001750 0001750 00000035652 13641040325 014261 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'linked-list'
require 'set'
require 'strscan'
require_relative 'directory'
require_relative 'error'
require_relative 'file'
require_relative 'ordered_node'
require_relative 'parse_error'
# An HRX archive.
#
# Parse an archive from a string with ::parse, load it from disk with ::load, or
# create an empty archive with ::new.
class HRX::Archive
class << self
# Parses an HRX file's text.
#
# If `file` is passed, it's used as the file name for error reporting.
#
# Throws an HRX::ParseError if `text` isn't valid HRX. Throws an
# EncodingError if `text` can't be converted to UTF-8.
def parse(text, file: nil)
text = text.encode("UTF-8")
return new if text.empty?
scanner = StringScanner.new(text)
unless boundary = scanner.scan(/<=+>/)
HRX::Util.parse_error(scanner, "Expected boundary", file: file)
end
boundary_length = boundary.length - 2
hrx = HRX::Archive.new(boundary_length: boundary_length)
boundary_regexp = /^<={#{boundary_length}}>/m
loop do
if scanner.scan(/\n/)
if comment_plus_boundary = scanner.scan_until(boundary_regexp)
comment = comment_plus_boundary[0...-boundary_length - 3]
else
hrx.last_comment = scanner.rest
return hrx
end
end
unless scanner.scan(/ /)
HRX::Util.parse_error(scanner, "Expected space", file: file)
end
before_path = scanner.pos
path = HRX::Util.scan_path(scanner, assert_done: false, file: file)
unless scanner.scan(/\n/)
HRX::Util.parse_error(scanner, "Expected newline", file: file)
end
begin
if path.end_with?("/")
hrx << HRX::Directory._new_without_checks(path, comment)
return hrx if scanner.eos?
next if scanner.scan(boundary_regexp)
HRX::Util.parse_error(scanner, "Expected boundary", file: file)
end
if content_plus_boundary = scanner.scan_until(boundary_regexp)
content = content_plus_boundary[0...-boundary_length - 3]
hrx << HRX::File._new_without_checks(path, content, comment)
else
hrx << HRX::File._new_without_checks(path, scanner.rest, comment)
return hrx
end
rescue HRX::ParseError => e
raise e
rescue HRX::Error => e
scanner.pos = before_path
HRX::Util.parse_error(scanner, e.message, file: file)
end
end
end
# Loads an HRX::Archive from the given `file`.
#
# Throws an HRX::ParseError if the file isn't valid HRX. Throws an
# EncodingError if the file isn't valid UTF-8.
def load(file)
text = File.read(file, mode: "rb", encoding: "UTF-8")
# If the encoding is invalid, force an InvalidByteSequenceError.
text.encode("UTF-16") unless text.valid_encoding?
parse(text, file: file)
end
# Creates an archive as a child of an existing archive.
#
# The `root` is the path to the root of this archive, relative to its
# outermost ancestor. The `entries` is the outermost ancestor's entries
# list. The `entries_by_path` is the subtree of the outermost ancestor's
# entries tree that corresponds to this child.
def _new_child(root, boundary_length, entries, entries_by_path) # :nodoc:
allocate.tap do |archive|
archive._initialize_child(root, boundary_length, entries, entries_by_path)
end
end
end
# The last comment in the document, or `nil` if it has no final comment.
#
# HRX comments are always encoded as UTF-8.
attr_reader :last_comment
# Creates a new, empty archive.
#
# The `boundary_length` is the number of `=` signs to include in the boundary
# when #to_hrx is called, unless a file already contains that boundary.
def initialize(boundary_length: 3)
if boundary_length && boundary_length < 1
raise ArgumentError.new("boundary_length must be 1 or greater")
end
@root = nil
@boundary_length = boundary_length
@entries = LinkedList::List.new
@entries_by_path = {}
end
# See _new_child.
def _initialize_child(root, boundary_length, entries, entries_by_path) # :nodoc:
@root = root.end_with?("/") ? root : root + "/"
@boundary_length = boundary_length
@entries = entries
@entries_by_path = entries_by_path
end
# A frozen array of the HRX::File and/or HRX::Directory objects that this
# archive contains.
#
# Note that a new array is created every time this method is called, so try to
# avoid calling this many times in a tight loop.
def entries
return @entries.to_a.freeze unless @root
@entries.
each.
select {|e| e.path.start_with?(@root) && e.path != @root}.
map {|e| e._relative(@root)}.
freeze
end
# Returns the HRX::File or HRX::Directory at the given `path` in this archive,
# or `nil` if there's no entry at that path.
#
# This doesn't verify that `path` is well-formed, but instead just returns
# `nil`.
#
# If `path` ends with `"/"`, returns `nil` if the entry at the given path is a
# file rather than a directory.
def [](path)
_find_node(path)&.data&._relative(@root)
end
# Returns all HRX::File or HRX::Directory objects in this archive that match
# `pattern`. See also Dir::glob. This always uses the option File::FNM_PATHNAME.
#
# This only returns HRX::Directory objects if `pattern` ends in `/` or
# includes `**`.
def glob(pattern, flags = 0)
entries.select {|e| File.fnmatch?(pattern, e.path, flags | File::FNM_PATHNAME)}
end
# Returns the contents of the file at `path` in the archive as a frozen
# string.
#
# Throws an HRX::Error if there is no file at `path`, or if `path` is invalid
# (including if it ends with `/`).
def read(path)
raise HRX::Error.new("There is no file at \"#{path}\"") unless node = _find_node(path)
unless node.data.is_a?(HRX::File)
raise HRX::Error.new("\"#{node.data._relative(@root).path}\" is a directory")
end
node.data.content
end
# Returns an HRX::Archive that provides access to the entries in `path` as
# though they were at the root of the archive.
#
# Any modifications to the child archive will be reflected in the parent as
# well. The HRX::File and HRX::Directory objects returned by the child archive
# will have their paths adjusted to be relative to the child's root.
def child_archive(path)
components = path.split("/")
raise HRX::Error.new('There is no directory at ""') if components.empty?
child_entries_by_path = @entries_by_path.dig(*components)
raise HRX::Error.new("There is no directory at \"#{path}\"") unless child_entries_by_path
if child_entries_by_path.is_a?(LinkedList::Node)
raise HRX::Error.new("\"#{child_entries_by_path.data._relative(@root).path}\" is a file")
end
HRX::Archive._new_child(_absolute(path), @boundary_length, @entries, child_entries_by_path)
end
# Writes `content` to the file at `path`.
#
# If there's already a file at `path`, overwrites it. Otherwise, creates a new
# file after the nearest file in the archive.
#
# If `comment` is passed, it's used as the comment for the new file. The
# special value `:copy` copies the existing comment for the file, if there is
# one.
#
# Throws an HRX::ParseError if `path` is invalid.
#
# Throws an HRX::Error if there's a directory at `path`.
def write(path, content, comment: nil)
components = path.split("/")
nearest_dir = nil
parent = components[0...-1].inject(@entries_by_path) do |hash, component|
entry = hash[component]
if entry.is_a?(LinkedList::Node)
raise HRX::Error.new("\"#{entry.data._relative(@root).path}\" is a file")
end
# Even though both branches of this if are assignments, their return
# values are used by #inject.
if entry
nearest_dir = entry
else
hash[component] = {}
end
end
nearest_dir = parent unless parent.empty?
previous = parent[components.last]
if previous.is_a?(Hash)
raise HRX::Error.new("\"#{path}/\" is a directory")
end
if previous.is_a?(LinkedList::Node)
comment = previous.data.comment if comment == :copy
previous.data = HRX::File.new(_absolute(path), content, comment: comment)
return
end
comment = nil if comment == :copy
node = HRX::OrderedNode.new(HRX::File.new(_absolute(path), content, comment: comment))
if nearest_dir.nil?
@entries << node
else
# Add the new file after its closest pre-existing cousin. Start looking
# for siblings in `nearest_dir`, and then work down through its children.
if last_cousin = _each_entry(nearest_dir).max_by {|n| n.order}
@entries.insert_after_node(node, last_cousin)
else
@entries << node
end
end
parent[components.last] = node
nil
end
# Deletes the file or directory at `path`.
#
# Throws an HRX::Error if there's no entry at `path`.
def delete(path, recursive: false)
# The outermost parent directory hash that contains only the entry at
# `path`, from which key_to_delete should be deleted
parent_to_delete_from = nil
key_to_delete = nil
components = path.split("/")
parent = components[0...-1].inject(@entries_by_path) do |hash, component|
entry = hash[component]
if entry.is_a?(LinkedList::Node)
raise HRX::Error.new("\"#{entry.data._relative(@root).path}\" is a file")
end
if entry.nil?
raise HRX::Error.new("\"#{path}\" doesn't exist")
elsif entry.size == 1
parent_to_delete_from ||= hash
key_to_delete ||= component
else
parent_to_delete_from = nil
key_to_delete = nil
end
hash[component] ||= {}
end
parent_to_delete_from ||= parent
key_to_delete ||= components.last
node = parent[components.last]
if node.nil?
raise HRX::Error.new("\"#{path}\" doesn't exist")
elsif node.is_a?(Hash)
if recursive
_each_entry(node) {|n| @entries.delete(n)}
else
unless node = node[:dir]
raise HRX::Error.new("\"#{path}\" is not an explicit directory and recursive isn't set")
end
@entries.delete(node)
end
elsif path.end_with?("/")
raise HRX::Error.new("\"#{path}\" is a file")
else
@entries.delete(node)
end
parent_to_delete_from.delete(key_to_delete)
end
# Sets the text of the last comment in the document.
#
# Throws an EncodingError if `comment` can't be converted to UTF-8.
def last_comment=(comment)
@last_comment = comment.encode("UTF-8")
end
# Adds an HRX::File or HRX::Directory to this archive.
#
# If `before` or `after` is passed, this adds `entry` before or after the
# entry with the given path in the archive. If the archive has no entry with
# the given path, this throws an HRX::Error. If `before` and `after` are
# *both* passed, this throws an ArgumentError.
#
# Throws an HRX::Error if the entry conflicts with an existing entry.
def add(entry, before: nil, after: nil)
raise ArgumentError.new("before and after may not both be passed") if before && after
node = HRX::OrderedNode.new(entry._absolute(@root))
path = entry.path.split("/")
parent = path[0...-1].inject(@entries_by_path) do |hash, component|
if hash[component].is_a?(LinkedList::Node)
raise HRX::Error.new("\"#{hash[component].data._relative(@root).path}\" defined twice")
end
hash[component] ||= {}
end
if parent[path.last].is_a?(LinkedList::Node)
raise HRX::Error.new("\"#{entry.path}\" defined twice")
end
if entry.is_a?(HRX::Directory)
dir = (parent[path.last] ||= {})
if dir.is_a?(LinkedList::Node) || dir[:dir]
raise HRX::Error.new("\"#{entry.path}\" defined twice")
end
dir[:dir] = node
else
raise HRX::Error.new("\"#{entry.path}\" defined twice") if parent.has_key?(path.last)
parent[path.last] = node
end
if before || after
reference = _find_node(before || after)
raise HRX::Error.new("There is no entry named \"#{before || after}\"") if reference.nil?
if before
@entries.insert_before_node(node, reference)
else
@entries.insert_after_node(node, reference)
end
else
@entries << node
end
nil
end
alias_method :<<, :add
# Returns this archive, serialized to text in HRX format.
def to_hrx
buffer = String.new.encode("UTF-8")
boundary = "<#{"=" * _choose_boundary_length}>"
entries.each_with_index do |e, i|
buffer << boundary << "\n" << e.comment << "\n" if e.comment
buffer << boundary << " " << e.path << "\n"
if e.respond_to?(:content) && !e.content.empty?
buffer << e.content
buffer << "\n" unless i == entries.length - 1
end
end
buffer << boundary << "\n" << last_comment if last_comment
buffer.freeze
end
# Writes this archive to disk at `file`.
def write!(file)
File.write(file, to_hrx, mode: "wb")
end
private
# Adds `@root` to the beginning of `path` if `@root` isn't `nil`.
def _absolute(path)
@root ? @root + path : path
end
# Returns the LinkedList::Node at the given `path`, or `nil` if there is no
# node at that path.
def _find_node(path)
components = path.split("/")
return if components.empty?
result = @entries_by_path.dig(*components)
return result[:dir] if result.is_a?(Hash)
return result unless path.end_with?("/")
end
# Returns each entry in or beneath the directory hash `dir`, in no particular
# order.
def _each_entry(dir)
return to_enum(__method__, dir) unless block_given?
dir.values.each do |entry|
if entry.is_a?(Hash)
_each_entry(entry) {|e| yield e}
else
yield entry
end
end
end
# Returns a boundary length for a serialized archive that doesn't conflict
# with any of the files that archive contains.
def _choose_boundary_length
forbidden_boundary_lengths = Set.new
entries.each do |e|
[
(e.content if e.respond_to?(:content)),
e.comment
].each do |text|
next unless text
text.scan(/^<(=+)>/m).each do |(equals)|
forbidden_boundary_lengths << equals.length
end
end
end
boundary_length = @boundary_length
while forbidden_boundary_lengths.include?(boundary_length)
boundary_length += 1
end
boundary_length
end
end
hrx-1.0.0/lib/hrx/ordered_node.rb 0000644 0001750 0001750 00000003572 13641040325 015265 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'linked-list'
# A linked list node that tracks its order reltaive to other nodes.
#
# This assumes that, while nodes may be added or removed from a given list, a
# given node object will only ever have one position in the list. This invariant
# is maintained by all methods of LinkedList::List other than
# LinkedList::List#reverse and LinkedList::List#reverse!.
#
# We use this to efficiently determine where to insert a new file relative to
# existing files with HRX#write.
class HRX::OrderedNode < LinkedList::Node # :nodoc:
def initialize(data)
super
@order = nil
end
# The relative order of this node.
#
# This is guaranteed to be greater than the order of all nodes before this in
# the list, and less than the order of all nodes after it. Otherwise it
# provides no guarantees.
#
# This is not guaranteed to be stale over time.
def order
@order || 0
end
def next=(other)
@order ||=
if other.nil?
nil
elsif other.prev
(other.prev.order + other.order) / 2.0
else
other.order - 1
end
super
end
def prev=(other)
@order ||=
if other.nil?
nil
elsif other.next&.order
(other.next.order + other.order) / 2.0
else
other.order + 1
end
super
end
end
hrx-1.0.0/lib/hrx/util.rb 0000644 0001750 0001750 00000005520 13641040325 013604 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require_relative 'parse_error'
module HRX::Util # :nodoc:
class << self
# Scans a single HRX path from `scanner` and returns it.
#
# Throws an ArgumentError if no valid path is available to scan. If
# `assert_done` is `true`, throws an ArgumentError if there's any text after
# the path.
def scan_path(scanner, assert_done: true, file: nil)
start = scanner.pos
while _scan_component(scanner, file) && scanner.scan(%r{/}); end
if assert_done && !scanner.eos?
parse_error(scanner, "Paths may not contain newlines", file: file)
elsif scanner.pos == start
parse_error(scanner, "Expected a path", file: file)
end
scanner.string.byteslice(start...scanner.pos)
end
# Emits an ArgumentError with the given `message` and line and column
# information from the current position of `scanner`.
def parse_error(scanner, message, file: nil)
before = scanner.string.byteslice(0...scanner.pos)
line = before.count("\n") + 1
column = (before[/^.*\z/] || "").length + 1
raise HRX::ParseError.new(message, line, column, file: file)
end
# Returns `child` relative to `parent`.
#
# Assumes `parent` ends with `/`, and `child` is beneath `parent`.
#
# If `parent` is `nil`, returns `child` as-is.
def relative(parent, child)
return child unless parent
child[parent.length..-1]
end
private
# Scans a single HRX path component from `scanner`.
#
# Returns whether or not a component could be found, or throws an
# HRX::ParseError if an invalid component was encountered.
def _scan_component(scanner, file)
return unless component = scanner.scan(%r{[^\u0000-\u001F\u007F/:\\]+})
if component == "." || component == ".."
scanner.unscan
parse_error(scanner, "Invalid path component \"#{component}\"", file: file)
end
if char = scanner.scan(/[\u0000-\u0009\u000B-\u001F\u007F]/)
scanner.unscan
parse_error(scanner, "Invalid character U+00#{char.ord.to_s(16).rjust(2, "0").upcase}", file: file)
elsif char = scanner.scan(/[\\:]/)
scanner.unscan
parse_error(scanner, "Invalid character \"#{char}\"", file: file)
end
true
end
end
end
hrx-1.0.0/lib/hrx/error.rb 0000644 0001750 0001750 00000001246 13641040325 013761 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# An error thrown by the HRX package.
class HRX::Error < StandardError; end
hrx-1.0.0/lib/hrx/directory.rb 0000644 0001750 0001750 00000004770 13641040325 014641 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require_relative 'util'
# A directory in an HRX archive.
class HRX::Directory
# The comment that appeared before this directory, or `nil` if it had no
# preceding comment.
#
# HRX file contents are always encoded as UTF-8.
#
# This string is frozen.
attr_reader :comment
# The path to this file, relative to the archive's root, including the
# trailing `/`.
#
# HRX paths are always `/`-separated and always encoded as UTF-8.
#
# This string is frozen.
attr_reader :path
# Creates a new file with the given paths and comment.
#
# Throws an HRX::ParseError if `path` is invalid, or an EncodingError if
# either argument can't be converted to UTF-8.
#
# The `path` may or may not end with a `/`. If it doesn't a `/` will be added.
def initialize(path, comment: nil)
@comment = comment&.clone&.encode("UTF-8")&.freeze
@path = HRX::Util.scan_path(StringScanner.new(path.encode("UTF-8")))
@path << "/" unless @path.end_with?("/")
@path.freeze
end
# Like ::new, but doesn't verify that the arguments are valid.
def self._new_without_checks(path, comment) # :nodoc:
allocate.tap do |dir|
dir._initialize_without_checks(path, comment)
end
end
# Like #initialize, but doesn't verify that the arguments are valid.
def _initialize_without_checks(path, comment) # :nodoc:
@comment = comment.freeze
@path = path.freeze
end
# Returns a copy of this entry with the path modified to be relative to
# `root`.
#
# If `root` is `nil`, returns this as-is.
def _relative(root) # :nodoc:
return self unless root
HRX::Directory._new_without_checks(HRX::Util.relative(root, path), comment)
end
# Returns a copy of this entry with `root` added tothe beginning of the path.
#
# If `root` is `nil`, returns this as-is.
def _absolute(root) # :nodoc:
return self unless root
HRX::Directory._new_without_checks(root + path, comment)
end
end
hrx-1.0.0/.gitignore 0000644 0001750 0001750 00000001041 13641040325 012715 0 ustar foka foka *.gem
*.rbc
/.config
/coverage/
/InstalledFiles
/pkg/
/spec/reports/
/spec/examples.txt
/test/tmp/
/test/version_tmp/
/tmp/
## Documentation cache and generated files:
/.yardoc/
/_yardoc/
/doc/
/rdoc/
## Environment normalization:
/.bundle/
/vendor/bundle
/lib/bundler/man/
# for a library or gem, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
Gemfile.lock
.ruby-version
.ruby-gemset
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
.rvmrc hrx-1.0.0/.rdoc_options 0000644 0001750 0001750 00000000574 13641040325 013442 0 ustar foka foka --- !ruby/object:RDoc::Options
encoding: UTF-8
static_path: []
rdoc_include:
- "."
- "/home/nweiz/goog/hrx-ruby"
charset: UTF-8
exclude:
hyperlink_all: false
line_numbers: false
locale:
locale_dir: locale
locale_name:
main_page:
markup: markdown
output_decoration: true
page_dir:
show_hash: false
tab_width: 8
template_stylesheets: []
title:
visibility: :protected
webcvs:
hrx-1.0.0/CONTRIBUTING.md 0000644 0001750 0001750 00000002115 13641040325 013161 0 ustar foka foka # How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## Community Guidelines
This project follows [Google's Open Source Community
Guidelines](https://opensource.google.com/conduct/).
hrx-1.0.0/spec/ 0000755 0001750 0001750 00000000000 13641040325 011663 5 ustar foka foka hrx-1.0.0/spec/archive_spec.rb 0000644 0001750 0001750 00000065240 13641040325 014652 0 ustar foka foka # coding: utf-8
# Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'rspec/temp_dir'
require 'hrx'
RSpec.describe HRX::Archive do
subject {HRX::Archive.new}
context "::load" do
include_context "uses temp dir"
it "parses a file from disk" do
File.write("#{temp_dir}/archive.hrx", < file
contents
END
archive = HRX::Archive.load("#{temp_dir}/archive.hrx")
expect(archive.entries.length).to be == 1
expect(archive.entries.last.path).to be == "file"
expect(archive.entries.last.content).to be == "contents\n"
end
it "parses a file as UTF-8" do
File.write("#{temp_dir}/archive.hrx", "<===> π\n", mode: "wb")
archive = HRX::Archive.load("#{temp_dir}/archive.hrx")
expect(archive.entries.last.path).to be == "π"
end
it "parses a file as UTF-8 despite Encoding.default_external" do
File.write("#{temp_dir}/archive.hrx", "<===> fΓΆΓΆ\n", mode: "wb")
with_external_encoding("iso-8859-1") do
archive = HRX::Archive.load("#{temp_dir}/archive.hrx")
expect(archive.entries.last.path).to be == "fΓΆΓΆ"
end
end
it "fails to parse a file that's invalid UTF-8" do
File.write("#{temp_dir}/archive.hrx", "<===> \xc3\x28\n".b, mode: "wb")
expect {HRX::Archive.load("#{temp_dir}/archive.hrx")}.to raise_error(EncodingError)
end
it "includes the filename in parse errors" do
File.write("#{temp_dir}/archive.hrx", "wrong", mode: "wb")
expect {HRX::Archive.load("#{temp_dir}/archive.hrx")}.to raise_error(HRX::ParseError, /archive\.hrx/)
end
end
context "when first initialized" do
it "has no entries" do
expect(subject.entries).to be_empty
end
context "#read" do
it "fails for any path" do
expect {subject.read("path")}.to raise_error(HRX::Error)
end
end
context "#write" do
before(:each) {subject.write("path", "contents\n")}
it "adds a file to the end of the archive" do
expect(subject.entries.last.path).to be == "path"
expect(subject.entries.last.content).to be == "contents\n"
end
it "adds a file that's readable by name" do
expect(subject.read("path")).to be == "contents\n"
end
end
context "#child_archive" do
it "fails for any path" do
expect {subject.child_archive("path")}.to raise_error(HRX::Error)
end
end
end
context "#initialize" do
it "should forbid boundary_length 0" do
expect {HRX::Archive.new(boundary_length: 0)}.to raise_error(ArgumentError)
end
it "should forbid negative boundary_length" do
expect {HRX::Archive.new(boundary_length: -1)}.to raise_error(ArgumentError)
end
end
context "#entries" do
it "is frozen" do
expect do
subject.entries << HRX::Directory.new("dir")
end.to raise_error(RuntimeError)
end
it "reflects new entries" do
expect(subject.entries).to be_empty
dir = HRX::Directory.new("dir")
subject << dir
expect(subject.entries).to be == [dir]
end
end
context "#last_comment=" do
it "requires the comment to be convertible to UTF-8" do
expect do
subject.last_comment = "\xc3\x28".b
end.to raise_error(EncodingError)
end
it "converts a comment to UTF-8" do
subject.last_comment = "γγ".encode("SJIS")
expect(subject.last_comment).to be == "γγ"
end
end
context "with files and directories in the archive" do
subject {HRX::Archive.parse(< file
file contents
<===> dir/
<===>
comment contents
<===> super/sub
sub contents
<===> very/deeply/
<===> very/deeply/nested/file
nested contents
<===> last
the last file
END
context "#[]" do
it "doesn't return an empty path" do
expect(subject[""]).to be_nil
end
it "doesn't return a path that's not in the archive" do
expect(subject["non/existent/file"]).to be_nil
end
it "doesn't return an implicit directory" do
expect(subject["super"]).to be_nil
end
it "doesn't return a file wih a slash" do
expect(subject["super/sub/"]).to be_nil
end
it "returns a file at the root level" do
expect(subject["file"].content).to be == "file contents\n"
end
it "returns a file in a directory" do
expect(subject["super/sub"].content).to be == "sub contents\n"
end
it "returns an explicit directory" do
expect(subject["dir"].path).to be == "dir/"
end
it "returns an explicit directory with a leading slash" do
expect(subject["dir/"].path).to be == "dir/"
end
end
context "#read" do
it "throws for an empty path" do
expect {subject.read("")}.to raise_error(HRX::Error, 'There is no file at ""')
end
it "throws for a path that's not in the archive" do
expect {subject.read("non/existent/file")}.to(
raise_error(HRX::Error, 'There is no file at "non/existent/file"'))
end
it "throws for an implicit directory" do
expect {subject.read("super")}.to raise_error(HRX::Error, 'There is no file at "super"')
end
it "throws for a file wih a slash" do
expect {subject.read("super/sub/")}.to(
raise_error(HRX::Error, 'There is no file at "super/sub/"'))
end
it "throws for a directory" do
expect {subject.read("dir")}.to raise_error(HRX::Error, '"dir/" is a directory')
end
it "returns the contents of a file at the root level" do
expect(subject.read("file")).to be == "file contents\n"
end
it "returns the contents of a file in a directory" do
expect(subject.read("super/sub")).to be == "sub contents\n"
end
end
context "#glob" do
it "returns nothing for an empty glob" do
expect(subject.glob("")).to be_empty
end
it "returns nothing for a path that's not in the archive" do
expect(subject.glob("non/existent/file")).to be_empty
end
it "doesn't return implicit directories" do
expect(subject.glob("super")).to be_empty
end
it "doesn't return a file with a slash" do
expect(subject.glob("super/sub/")).to be_empty
end
it "doesn't return an explicit directory without a leading slash" do
expect(subject.glob("dir")).to be_empty
end
it "returns a file at the root level" do
result = subject.glob("file")
expect(result.length).to be == 1
expect(result.first.path).to be == "file"
end
it "returns a file in a directory" do
result = subject.glob("super/sub")
expect(result.length).to be == 1
expect(result.first.path).to be == "super/sub"
end
it "returns an explicit directory" do
result = subject.glob("dir/")
expect(result.length).to be == 1
expect(result.first.path).to be == "dir/"
end
it "returns all matching files at the root level" do
result = subject.glob("*")
expect(result.length).to be == 2
expect(result.first.path).to be == "file"
expect(result.last.path).to be == "last"
end
it "returns all matching files in a directory" do
result = subject.glob("super/*")
expect(result.length).to be == 1
expect(result.first.path).to be == "super/sub"
end
it "returns all matching entries recursively in a directory" do
result = subject.glob("very/**/*")
expect(result.length).to be == 2
expect(result.first.path).to be == "very/deeply/"
expect(result.last.path).to be == "very/deeply/nested/file"
end
it "respects glob flags" do
result = subject.glob("FILE", File::FNM_CASEFOLD)
expect(result.length).to be == 1
expect(result.first.path).to be == "file"
end
end
context "#child_archive" do
it "throws for an empty path" do
expect {subject.child_archive("")}.to raise_error(HRX::Error, 'There is no directory at ""')
end
it "throws for a path that's not in the archive" do
expect {subject.child_archive("non/existent/dir")}.to(
raise_error(HRX::Error, 'There is no directory at "non/existent/dir"'))
end
it "throws for a file" do
expect {subject.child_archive("super/sub")}.to(
raise_error(HRX::Error, '"super/sub" is a file'))
end
context "for an explicit directory with no children" do
let(:child) {subject.child_archive("dir")}
it "returns an empty archive" do
expect(child.entries).to be_empty
end
it "serializes to an empty string" do
expect(child.to_hrx).to be_empty
end
it "doesn't return the root directory" do
expect(child[""]).to be_nil
expect(child["/"]).to be_nil
expect(child["dir"]).to be_nil
end
end
end
context "#write" do
it "validates the path" do
expect {subject.write("super/./sub", "")}.to raise_error(HRX::ParseError)
end
it "rejects a path that ends in a slash" do
expect {subject.write("file/", "")}.to raise_error(HRX::ParseError)
end
it "fails if a parent directory is a file" do
expect {subject.write("file/sub", "")}.to raise_error(HRX::Error)
end
it "fails if the path is an explicit directory" do
expect {subject.write("dir", "")}.to raise_error(HRX::Error)
end
it "fails if the path is an implicit directory" do
expect {subject.write("super", "")}.to raise_error(HRX::Error)
end
context "with a top-level file" do
before(:each) {subject.write("new", "new contents\n")}
it "adds to the end of the archive" do
expect(subject.entries.last.path).to be == "new"
expect(subject.entries.last.content).to be == "new contents\n"
end
it "adds a file that's readable by name" do
expect(subject.read("new")).to be == "new contents\n"
end
end
context "with a file in a new directory tree" do
before(:each) {subject.write("new/sub/file", "new contents\n")}
it "adds to the end of the archive" do
expect(subject.entries.last.path).to be == "new/sub/file"
expect(subject.entries.last.content).to be == "new contents\n"
end
it "adds a file that's readable by name" do
expect(subject.read("new/sub/file")).to be == "new contents\n"
end
end
context "with a file in an explicit directory" do
before(:each) {subject.write("dir/new", "new contents\n")}
it "adds to the end of the directory" do
new_index = subject.entries.find_index {|e| e.path == "dir/"} + 1
expect(subject.entries[new_index].path).to be == "dir/new"
expect(subject.entries[new_index].content).to be == "new contents\n"
end
it "adds a file that's readable by name" do
expect(subject.read("dir/new")).to be == "new contents\n"
end
end
context "with a file in an implicit directory" do
before(:each) {subject.write("super/another", "new contents\n")}
it "adds to the end of the directory" do
new_index = subject.entries.find_index {|e| e.path == "super/sub"} + 1
expect(subject.entries[new_index].path).to be == "super/another"
expect(subject.entries[new_index].content).to be == "new contents\n"
end
it "adds a file that's readable by name" do
expect(subject.read("super/another")).to be == "new contents\n"
end
end
context "with a file in an implicit directory that's not a sibling" do
before(:each) {subject.write("very/different/nesting", "new contents\n")}
it "adds after its cousin" do
new_index = subject.entries.find_index {|e| e.path == "very/deeply/nested/file"} + 1
expect(subject.entries[new_index].path).to be == "very/different/nesting"
expect(subject.entries[new_index].content).to be == "new contents\n"
end
it "adds a file that's readable by name" do
expect(subject.read("very/different/nesting")).to be == "new contents\n"
end
end
context "with an existing filename" do
let (:old_index) {subject.entries.find_index {|e| e.path == "super/sub"}}
before(:each) {subject.write("super/sub", "new contents\n")}
it "overwrites that file" do
expect(subject.read("super/sub")).to be == "new contents\n"
end
it "uses the same location as that file" do
expect(subject.entries[old_index].path).to be == "super/sub"
expect(subject.entries[old_index].content).to be == "new contents\n"
end
it "removes the comment" do
expect(subject.entries[old_index].comment).to be_nil
end
end
context "with a comment" do
it "writes the comment" do
subject.write("new", "", comment: "new comment\n")
expect(subject["new"].comment).to be == "new comment\n"
end
it "overwrites an existing comment" do
subject.write("super/sub", "", comment: "new comment\n")
expect(subject["super/sub"].comment).to be == "new comment\n"
end
it "re-uses an existing comment with :copy" do
subject.write("super/sub", "", comment: :copy)
expect(subject["super/sub"].comment).to be == "comment contents\n"
end
it "ignores :copy for a new file" do
subject.write("new", "", comment: :copy)
expect(subject["new"].comment).to be_nil
end
end
end
context "#delete" do
it "throws an error if the file doesn't exist" do
expect {subject.delete("nothing")}.to(
raise_error(HRX::Error, '"nothing" doesn\'t exist'))
end
it "throws an error if the file is in a directory that doesn't exist" do
expect {subject.delete("does/not/exist")}.to(
raise_error(HRX::Error, '"does/not/exist" doesn\'t exist'))
end
it "throws an error if a file has a trailing slash" do
expect {subject.delete("file/")}.to raise_error(HRX::Error, '"file/" is a file')
end
it "refuses to delete an implicit directory" do
expect {subject.delete("super/")}.to(
raise_error(HRX::Error, '"super/" is not an explicit directory and recursive isn\'t set'))
end
it "deletes a top-level file" do
length_before = subject.entries.length
subject.delete("file")
expect(subject["file"]).to be_nil
expect(subject.entries.length).to be == length_before - 1
end
it "deletes a nested file" do
length_before = subject.entries.length
subject.delete("super/sub")
expect(subject["super/sub"]).to be_nil
expect(subject.entries.length).to be == length_before - 1
end
it "deletes an explicit directory without a slash" do
length_before = subject.entries.length
subject.delete("dir")
expect(subject["dir/"]).to be_nil
expect(subject.entries.length).to be == length_before - 1
end
it "deletes an explicit directory with a slash" do
length_before = subject.entries.length
subject.delete("dir/")
expect(subject["dir/"]).to be_nil
expect(subject.entries.length).to be == length_before - 1
end
it "deletes an explicit directory with children" do
length_before = subject.entries.length
subject.delete("very/deeply")
expect(subject["very/deeply"]).to be_nil
expect(subject.entries.length).to be == length_before - 1
end
it "recursively deletes an implicit directory" do
length_before = subject.entries.length
subject.delete("very/", recursive: true)
expect(subject["very/deeply"]).to be_nil
expect(subject["very/deeply/nested/file"]).to be_nil
expect(subject.entries.length).to be == length_before - 2
end
it "recursively deletes an explicit directory" do
length_before = subject.entries.length
subject.delete("very/deeply", recursive: true)
expect(subject["very/deeply"]).to be_nil
expect(subject["very/deeply/nested/file"]).to be_nil
expect(subject.entries.length).to be == length_before - 2
end
end
context "#add" do
it "adds a file to the end of the archive" do
file = HRX::File.new("other", "")
subject << file
expect(subject.entries.last).to be == file
end
it "adds a file in an existing directory to the end of the archive" do
file = HRX::File.new("dir/other", "")
subject << file
expect(subject.entries.last).to be == file
end
it "allows an implicit directory to be made explicit" do
dir = HRX::Directory.new("super")
subject << dir
expect(subject.entries.last).to be == dir
end
it "throws an error for a duplicate file" do
expect do
subject << HRX::File.new("file", "")
end.to raise_error(HRX::Error, '"file" defined twice')
end
it "throws an error for a duplicate directory" do
expect do
subject << HRX::Directory.new("dir")
end.to raise_error(HRX::Error, '"dir/" defined twice')
end
it "throws an error for a file with a directory's name" do
expect do
subject << HRX::File.new("dir", "")
end.to raise_error(HRX::Error, '"dir" defined twice')
end
it "throws an error for a file with an implicit directory's name" do
expect do
subject << HRX::File.new("super", "")
end.to raise_error(HRX::Error, '"super" defined twice')
end
it "throws an error for a directory with a file's name" do
expect do
subject << HRX::Directory.new("file")
end.to raise_error(HRX::Error, '"file/" defined twice')
end
context "with :before" do
it "adds the new entry before the given file" do
subject.add HRX::File.new("other", ""), before: "super/sub"
expect(subject.entries[2].path).to be == "other"
end
it "adds the new entry before the given directory" do
subject.add HRX::File.new("other", ""), before: "dir/"
expect(subject.entries[1].path).to be == "other"
end
it "adds the new entry before the given directory without a /" do
subject.add HRX::File.new("other", ""), before: "dir"
expect(subject.entries[1].path).to be == "other"
end
it "fails if the path can't be found" do
expect do
subject.add HRX::File.new("other", ""), before: "asdf"
end.to raise_error(HRX::Error, 'There is no entry named "asdf"')
end
it "fails if the path is an implicit directory" do
expect do
subject.add HRX::File.new("other", ""), before: "super"
end.to raise_error(HRX::Error, 'There is no entry named "super"')
end
it "fails if a trailing slash is used for a file" do
expect do
subject.add HRX::File.new("other", ""), before: "file/"
end.to raise_error(HRX::Error, 'There is no entry named "file/"')
end
end
context "with :after" do
it "adds the new entry after the given file" do
subject.add HRX::File.new("other", ""), after: "super/sub"
expect(subject.entries[3].path).to be == "other"
end
it "adds the new entry after the given directory" do
subject.add HRX::File.new("other", ""), after: "dir/"
expect(subject.entries[2].path).to be == "other"
end
it "adds the new entry after the given directory without a /" do
subject.add HRX::File.new("other", ""), after: "dir"
expect(subject.entries[2].path).to be == "other"
end
it "fails if the path can't be found" do
expect do
subject.add HRX::File.new("other", ""), after: "asdf"
end.to raise_error(HRX::Error, 'There is no entry named "asdf"')
end
it "fails if the path is an implicit directory" do
expect do
subject.add HRX::File.new("other", ""), after: "super"
end.to raise_error(HRX::Error, 'There is no entry named "super"')
end
it "fails if a trailing slash is used for a file" do
expect do
subject.add HRX::File.new("other", ""), after: "file/"
end.to raise_error(HRX::Error, 'There is no entry named "file/"')
end
end
end
end
context "with physically distant files in the same directory" do
subject {HRX::Archive.parse(< dir/super/sub1
sub1 contents
<===> base 1
<===> dir/other/child1
child1 contents
<===> base 2
<===> dir/super/sub2
sub2 contents
<===> base 3
<===> dir/other/child2
child2 contents
<===> base 4
<===> dir/super/sub3
sub3 contents
<===> base 5
END
context "#write" do
context "with a file in an implicit directory" do
before(:each) {subject.write("dir/other/new", "new contents\n")}
it "adds after the last file in the directory" do
new_index = subject.entries.find_index {|e| e.path == "dir/other/child2"} + 1
expect(subject.entries[new_index].path).to be == "dir/other/new"
expect(subject.entries[new_index].content).to be == "new contents\n"
end
it "adds a file that's readable by name" do
expect(subject.read("dir/other/new")).to be == "new contents\n"
end
end
context "with a file in an implicit directory that's not a sibling" do
before(:each) {subject.write("dir/another/new", "new contents\n")}
it "adds to the end of the directory" do
new_index = subject.entries.find_index {|e| e.path == "dir/super/sub3"} + 1
expect(subject.entries[new_index].path).to be == "dir/another/new"
expect(subject.entries[new_index].content).to be == "new contents\n"
end
it "adds a file that's readable by name" do
expect(subject.read("dir/another/new")).to be == "new contents\n"
end
end
end
end
context "#to_hrx" do
it "returns the empty string for an empty file" do
expect(subject.to_hrx).to be_empty
end
it "writes a file's name and contents" do
subject << HRX::File.new("file", "contents\n")
expect(subject.to_hrx).to be == < file
contents
END
end
it "adds a newline to a middle file with a newline" do
subject << HRX::File.new("file 1", "contents 1\n")
subject << HRX::File.new("file 2", "contents 2\n")
expect(subject.to_hrx).to be == < file 1
contents 1
<===> file 2
contents 2
END
end
it "adds a newline to a middle file without a newline" do
subject << HRX::File.new("file 1", "contents 1")
subject << HRX::File.new("file 2", "contents 2\n")
expect(subject.to_hrx).to be == < file 1
contents 1
<===> file 2
contents 2
END
end
it "writes empty files" do
subject << HRX::File.new("file 1", "")
subject << HRX::File.new("file 2", "")
expect(subject.to_hrx).to be == < file 1
<===> file 2
END
end
it "doesn't add a newline to the last file" do
subject << HRX::File.new("file", "contents")
expect(subject.to_hrx).to be == "<===> file\ncontents"
end
it "writes a directory" do
subject << HRX::Directory.new("dir")
expect(subject.to_hrx).to be == "<===> dir/\n"
end
it "writes a comment on a file" do
subject << HRX::File.new("file", "contents\n", comment: "comment")
expect(subject.to_hrx).to be == <
comment
<===> file
contents
END
end
it "writes a comment on a directory" do
subject << HRX::Directory.new("dir", comment: "comment")
expect(subject.to_hrx).to be == <
comment
<===> dir/
END
end
it "uses a different boundary length to avoid conflicts" do
subject << HRX::File.new("file", "<===>\n")
expect(subject.to_hrx).to be == < file
<===>
END
end
it "uses a different boundary length to avoid conflicts in comments" do
subject << HRX::File.new("file", "", comment: "<===>")
expect(subject.to_hrx).to be == <
<===>
<====> file
END
end
it "uses a different boundary length to avoid multiple conflicts" do
subject << HRX::File.new("file", <
<====> foo
<=====>
END
expect(subject.to_hrx).to be == < file
<===>
<====> foo
<=====>
END
end
it "uses a different boundary length to avoid multiple conflicts in multiple files" do
subject << HRX::File.new("file 1", "<===>\n")
subject << HRX::File.new("file 2", "<====>\n")
subject << HRX::File.new("file 3", "<=====>\n")
expect(subject.to_hrx).to be == < file 1
<===>
<======> file 2
<====>
<======> file 3
<=====>
END
end
context "with an explicit boundary length" do
subject {HRX::Archive.new(boundary_length: 1)}
it "uses it if possible" do
subject << HRX::File.new("file", "contents\n")
expect(subject.to_hrx).to be == < file
contents
END
end
it "doesn't use it if it conflicts" do
subject << HRX::File.new("file", "<=>\n")
expect(subject.to_hrx).to be == < file
<=>
END
end
end
end
context "#write!" do
include_context "uses temp dir"
it "saves the archive to disk" do
subject << HRX::File.new("file", "file contents\n")
subject << HRX::File.new("super/sub", "sub contents\n")
subject.write!("#{temp_dir}/archive.hrx")
expect(File.read("#{temp_dir}/archive.hrx", mode: "rb")).to be == < file
file contents
<===> super/sub
sub contents
END
end
it "saves the archive as UTF-8" do
subject << HRX::File.new("π", "")
subject.write!("#{temp_dir}/archive.hrx")
expect(File.read("#{temp_dir}/archive.hrx", mode: "rb")).to be == "<===> \xF0\x9F\x91\xAD\n".b
end
it "saves the archive as UTF-8 despite Encoding.default_external" do
with_external_encoding("iso-8859-1") do
subject << HRX::File.new("fΓΆΓΆ", "")
subject.write!("#{temp_dir}/archive.hrx")
expect(File.read("#{temp_dir}/archive.hrx", mode: "rb")).to(
be == "<===> f\xC3\xB6\xC3\xB6\n".b)
end
end
end
end
hrx-1.0.0/spec/ordered_node_spec.rb 0000644 0001750 0001750 00000003341 13641040325 015654 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'linked-list'
require 'hrx'
RSpec.describe HRX::OrderedNode do
context "a list of ordered nodes" do
subject do
LinkedList::List.new <<
HRX::OrderedNode.new("do") <<
HRX::OrderedNode.new("re") <<
HRX::OrderedNode.new("me") <<
HRX::OrderedNode.new("fa") <<
HRX::OrderedNode.new("so")
end
def it_should_be_ordered
subject.each_node.each_cons(2) do |n1, n2|
expect(n1.order).to be < n2.order
end
end
it "should begin ordered" do
it_should_be_ordered
end
it "should remain ordered when a node is deleted" do
subject.delete "me"
it_should_be_ordered
end
it "should remain ordered when a node is added at the beginning" do
subject.unshift HRX::OrderedNode.new("ti")
it_should_be_ordered
end
it "should remain ordered when a node is added in the middle" do
subject.insert HRX::OrderedNode.new("re#"), after: "re"
it_should_be_ordered
end
it "should remain ordered when a node is added at the end" do
subject << HRX::OrderedNode.new("la")
it_should_be_ordered
end
end
end
hrx-1.0.0/spec/directory_spec.rb 0000644 0001750 0001750 00000003416 13641040325 015232 0 ustar foka foka # coding: utf-8
# Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'hrx'
require_relative 'validates_path'
RSpec.describe HRX::Directory, "#initialize" do
let(:constructor) {lambda {|path| HRX::Directory.new(path)}}
include_examples "validates paths"
it "requires the path to be convertible to UTF-8" do
expect do
HRX::Directory.new("\xc3\x28".b)
end.to raise_error(EncodingError)
end
it "requires the comment to be convertible to UTF-8" do
expect do
HRX::Directory.new("dir", comment: "\xc3\x28".b)
end.to raise_error(EncodingError)
end
context "with arguments that are convertible to UTF-8" do
subject do
ika = "γγ".encode("SJIS")
HRX::Directory.new(ika, comment: ika)
end
it("converts #path") {expect(subject.path).to be == "γγ/"}
it("converts #comment") {expect(subject.comment).to be == "γγ"}
end
it "forbids a path with a newline" do
expect {HRX::Directory.new("di\nr")}.to raise_error(HRX::ParseError)
end
it "doesn't add a slash to a path that has one" do
expect(HRX::Directory.new("dir/").path).to be == "dir/"
end
it "adds a slash to a path without one" do
expect(HRX::Directory.new("dir").path).to be == "dir/"
end
end
hrx-1.0.0/spec/parse_spec.rb 0000644 0001750 0001750 00000020445 13641040325 014341 0 ustar foka foka # coding: utf-8
# Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'hrx'
require_relative 'validates_path'
RSpec.describe HRX, ".parse" do
it "parses an empty file" do
expect(HRX::Archive.parse("").entries).to be_empty
end
it "converts the file to UTF-8" do
hrx = HRX::Archive.parse("<===> γγ\n".encode("SJIS"))
expect(hrx.entries.first.path).to be == "γγ"
end
it "requires the file to be convetible to UTF-8" do
expect do
HRX::Archive.parse("<===> \xc3\x28\n".b)
end.to raise_error(EncodingError)
end
context "with a single file" do
subject {HRX::Archive.parse(< file
contents
END
it "parses one entry" do
expect(subject.entries.length).to be == 1
end
it "parses the filename" do
expect(subject.entries.first.path).to be == "file"
end
it "parses the contents" do
expect(subject.entries.first.content).to be == "contents\n"
end
it "parses contents without a newline" do
hrx = HRX::Archive.parse("<===> file\ncontents")
expect(hrx.entries.first.content).to be == "contents"
end
it "parses contents with boundary-like sequences" do
hrx = HRX::Archive.parse(< file
<==>
inline <===>
<====>
END
expect(hrx.entries.first.content).to be == <
inline <===>
<====>
END
end
context "with a comment" do
subject {HRX::Archive.parse(<
comment
<===> file
contents
END
it "parses one entry" do
expect(subject.entries.length).to be == 1
end
it "parses the filename" do
expect(subject.entries.first.path).to be == "file"
end
it "parses the contents" do
expect(subject.entries.first.content).to be == "contents\n"
end
it "parses the comment" do
expect(subject.entries.first.comment).to be == "comment"
end
end
end
context "with multiple files" do
subject {HRX::Archive.parse(< file 1
contents 1
<===> file 2
contents 2
END
it "parses two entries" do
expect(subject.entries.length).to be == 2
end
it "parses the first filename" do
expect(subject.entries.first.path).to be == "file 1"
end
it "parses the first contents" do
expect(subject.entries.first.content).to be == "contents 1\n"
end
it "parses the second filename" do
expect(subject.entries.last.path).to be == "file 2"
end
it "parses the second contents" do
expect(subject.entries.last.content).to be == "contents 2\n"
end
it "allows an explicit parent directory" do
hrx = HRX::Archive.parse(< dir/
<===> dir/file
contents
END
expect(hrx.entries.last.content).to be == "contents\n"
end
it "parses contents without a newline" do
hrx = HRX::Archive.parse(< file 1
contents 1
<===> file 2
contents 2
END
expect(hrx.entries.first.content).to be == "contents 1"
end
it "parses contents with boundary-like sequences" do
hrx = HRX::Archive.parse(< file 1
<==>
inline <===>
<====>
<===> file 2
contents
END
expect(hrx.entries.first.content).to be == <
inline <===>
<====>
END
end
context "with a comment" do
subject {HRX::Archive.parse(< file 1
contents 1
<===>
comment
<===> file 2
contents 2
END
it "parses two entries" do
expect(subject.entries.length).to be == 2
end
it "parses the first filename" do
expect(subject.entries.first.path).to be == "file 1"
end
it "parses the first contents" do
expect(subject.entries.first.content).to be == "contents 1\n"
end
it "parses the second filename" do
expect(subject.entries.last.path).to be == "file 2"
end
it "parses the second contents" do
expect(subject.entries.last.content).to be == "contents 2\n"
end
it "parses the comment" do
expect(subject.entries.last.comment).to be == "comment"
end
end
end
it "parses a file that only contains a comment" do
expect(HRX::Archive.parse(<
contents
END
end
it "parses a file that only contains a comment with boundary-like sequences" do
expect(HRX::Archive.parse(<
<==>
inline <===>
<====>
HRX
<==>
inline <===>
<====>
CONTENTS
end
context "with a file and a trailing comment" do
subject {HRX::Archive.parse(< file
contents
<===>
comment
END
it "parses one entry" do
expect(subject.entries.length).to be == 1
end
it "parses the filename" do
expect(subject.entries.first.path).to be == "file"
end
it "parses the contents" do
expect(subject.entries.first.content).to be == "contents\n"
end
it "parses the trailing comment" do
expect(subject.last_comment).to be == "comment\n"
end
end
context "with a single directory" do
subject {HRX::Archive.parse("<===> dir/\n")}
it "parses one entry" do
expect(subject.entries.length).to be == 1
end
it "parses a directory" do
expect(subject.entries.first).to be_a(HRX::Directory)
end
it "parses the filename" do
expect(subject.entries.first.path).to be == "dir/"
end
end
it "serializes in source order" do
hrx = HRX::Archive.parse(< foo
<===> dir/bar
<===> baz
<===> dir/qux
END
expect(hrx.to_hrx).to be == < foo
<===> dir/bar
<===> baz
<===> dir/qux
END
end
it "serializes with the source boundary" do
hrx = HRX::Archive.parse("<=> file\n")
expect(hrx.to_hrx).to be == "<=> file\n"
end
let(:constructor) {lambda {|path| HRX::Archive.parse("<===> #{path}\n")}}
include_examples "validates paths"
context "forbids an HRX file that" do
# Specifies that the given HRX archive, with the given human-readable
# description, can't be parsed.
def self.that(description, text, message)
it description do
expect {HRX::Archive.parse(text)}.to raise_error(HRX::ParseError, message)
end
end
that "doesn't start with a boundary", "file\n", /Expected boundary/
that "starts with an unclosed boundary", "<== file\n", /Expected boundary/
that "starts with an unopened boundary", "==> file\n", /Expected boundary/
that "starts with a malformed boundary", "<> file\n", /Expected boundary/
that "has a directory with contents", "<===> dir/\ncontents", /Expected boundary/
that "has duplicate files", "<=> file\n<=> file\n", /"file" defined twice/
that "has duplicate directories", "<=> dir/\n<=> dir/\n", %r{"dir/" defined twice}
that "has file with the same name as a directory", "<=> foo/\n<=> foo\n", /"foo" defined twice/
that "has file with the same name as an earlier implicit directory", "<=> foo/bar\n<=> foo\n", /"foo" defined twice/
that "has file with the same name as a later implicit directory", "<=> foo\n<=> foo/bar\n", /"foo" defined twice/
context "has a boundary that" do
that "isn't followed by a space", "<=>file\n", /Expected space/
that "isn't followed by a path", "<=> \n", /Expected a path/
that "has a file without a newline", "<=> file", /Expected newline/
end
context "has a middle boundary that" do
that "isn't followed by a space", "<=> file 1\n<=>file 2\n", /Expected space/
that "isn't followed by a path", "<=> file 1\n<=> \n", /Expected a path/
that "has a file without a newline", "<=> file 1\n<=> file", /Expected newline/
end
context "has multiple comments that" do
that "come before a file", <
comment 1
<=>
comment 2
<=> file
END
that "come after a file", < file
<=>
comment 1
<=>
comment 2
END
that "appear on their own", <
comment 1
<=>
comment 2
END
end
end
end
hrx-1.0.0/spec/file_spec.rb 0000644 0001750 0001750 00000003604 13641040325 014144 0 ustar foka foka # coding: utf-8
# Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'hrx'
require_relative 'validates_path'
RSpec.describe HRX::File, "#initialize" do
let(:constructor) {lambda {|path| HRX::File.new(path, "")}}
include_examples "validates paths"
it "requires the path to be convertible to UTF-8" do
expect do
HRX::File.new("\xc3\x28".b, "")
end.to raise_error(EncodingError)
end
it "requires the content to be convertible to UTF-8" do
expect do
HRX::File.new("file", "\xc3\x28".b)
end.to raise_error(EncodingError)
end
it "requires the comment to be convertible to UTF-8" do
expect do
HRX::File.new("file", "", comment: "\xc3\x28".b)
end.to raise_error(EncodingError)
end
it "forbids a path with a trailing slash" do
expect {HRX::File.new("file/", "")}.to raise_error(HRX::ParseError)
end
it "forbids a path with a newline" do
expect {HRX::File.new("fi\nle", "")}.to raise_error(HRX::ParseError)
end
context "with arguments that are convertible to UTF-8" do
subject do
ika = "γγ".encode("SJIS")
HRX::File.new(ika, ika, comment: ika)
end
it("converts #path") {expect(subject.path).to be == "γγ"}
it("converts #content") {expect(subject.content).to be == "γγ"}
it("converts #comment") {expect(subject.comment).to be == "γγ"}
end
end
hrx-1.0.0/spec/validates_path.rb 0000644 0001750 0001750 00000003777 13641040325 015216 0 ustar foka foka # coding: utf-8
# Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
RSpec.shared_examples "validates paths" do
# Specifies that the given `path`, described by `description`, is allowed.
def self.allows_a_path_that(description, path)
it "allows a path that #{description}" do
expect { constructor[path] }.not_to raise_error
end
end
# Specifies that the given `path`, described by `description`, is not allowed.
def self.forbids_a_path_that(description, path)
it "forbids a path that #{description}" do
expect { constructor[path] }.to raise_error(HRX::ParseError)
end
end
allows_a_path_that "contains one component", "foo"
allows_a_path_that "contains multiple components", "foo/bar/baz"
allows_a_path_that "contains three dots", "..."
allows_a_path_that "starts with a dot", ".foo"
allows_a_path_that "contains non-alphanumeric characters", '~`!@#$%^&*()_-+= {}[]|;"\'<,>.?'
allows_a_path_that "contains non-ASCII characters", "β"
forbids_a_path_that "is empty", ""
forbids_a_path_that 'is "."', "."
forbids_a_path_that 'is ".."', ".."
forbids_a_path_that "is only a separator", "/"
forbids_a_path_that "begins with a separator", "/foo"
forbids_a_path_that "contains multiple separators in a row", "foo//bar"
forbids_a_path_that "contains an invalid component", "foo/../bar"
[*0x00..0x09, *0x0B..0x1F, 0x3A, 0x5C, 0x7F].each do |c|
forbids_a_path_that "contains U+00#{c.to_s(16).rjust(2, "0")}", "fo#{c.chr}o"
end
end
hrx-1.0.0/spec/child_archive_spec.rb 0000644 0001750 0001750 00000022040 13641040325 016004 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'hrx'
RSpec.describe HRX::Archive, "as a child" do
let(:parent) {HRX::Archive.parse(< before
file before dir
<===> dir/top
top-level child
<===> dir/sub/mid
mid-level child
<===> dir/sub/dir/
<===> interrupt
interruption not in dir
<===> dir/sub/lower/bottom
bottom-level child
<===> after
file after dir
END
let(:child) {parent.child_archive("dir")}
context "#entries" do
it "is frozen" do
expect do
child.entries << HRX::Directory.new("dir")
end.to raise_error(RuntimeError)
end
it "should only contain entries in the directory" do
expect(child.entries.length).to be == 4
expect(child.entries[0].path).to be == "top"
expect(child.entries[1].path).to be == "sub/mid"
expect(child.entries[2].path).to be == "sub/dir/"
expect(child.entries[3].path).to be == "sub/lower/bottom"
end
it "should reflect changes to the child archive" do
child << HRX::File.new("another", "")
expect(child.entries.length).to be == 5
expect(child.entries.last.path).to be == "another"
end
it "should reflect changes to the parent archive" do
parent << HRX::File.new("dir/another", "")
expect(child.entries.length).to be == 5
expect(child.entries.last.path).to be == "another"
end
end
context "#[]" do
it "should return a top-level entry" do
expect(child["top"].path).to be == "top"
expect(child["top"].content).to be == "top-level child\n"
end
it "should return a nested entry" do
expect(child["sub/lower/bottom"].path).to be == "sub/lower/bottom"
expect(child["sub/lower/bottom"].content).to be == "bottom-level child\n"
end
it "should return an explicit directory" do
expect(child["sub/dir"].path).to be == "sub/dir/"
expect(child["sub/dir"]).not_to respond_to(:content)
end
it "shouldn't return a file that isn't in the directory" do
expect(child["interrupt"]).to be_nil
end
it "should reflect changes to the child archive" do
child << HRX::File.new("another", "another contents\n")
expect(child["another"].path).to be == "another"
expect(child["another"].content).to be == "another contents\n"
end
it "should reflect changes to the parent archive" do
parent << HRX::File.new("dir/another", "another contents\n")
expect(child["another"].path).to be == "another"
expect(child["another"].content).to be == "another contents\n"
end
end
context "#read" do
it "should read a top-level file" do
expect(child.read("top")).to be == "top-level child\n"
end
it "should read a nested file" do
expect(child.read("sub/lower/bottom")).to be == "bottom-level child\n"
end
it "shouldn't read a file that isn't in the directory" do
expect {child.read("interrupt")}.to(
raise_error(HRX::Error, 'There is no file at "interrupt"'))
end
it "should reflect changes to the child archive" do
child << HRX::File.new("another", "another contents\n")
expect(child.read("another")).to be == "another contents\n"
end
it "should reflect changes to the parent archive" do
parent << HRX::File.new("dir/another", "another contents\n")
expect(child.read("another")).to be == "another contents\n"
end
end
context "#child_archive" do
let(:grandchild) {child.child_archive("sub")}
it "should access an even more nested entry" do
expect(grandchild.read("mid")).to be == "mid-level child\n"
end
it "should reflect changes in the parent archive" do
parent << HRX::File.new("dir/sub/another", "another contents\n")
expect(grandchild.entries.length).to be == 4
expect(grandchild.entries.last.path).to be == "another"
expect(grandchild.read("another")).to be == "another contents\n"
end
it "should propagate changes to the parent archive" do
grandchild << HRX::File.new("another", "another contents\n")
expect(parent.entries.length).to be == 8
expect(parent.entries.last.path).to be == "dir/sub/another"
expect(parent.read("dir/sub/another")).to be == "another contents\n"
end
end
context "#write" do
before(:each) {child.write("another", "another contents\n")}
it "should be visible in the child archive" do
expect(child.entries.length).to be == 5
expect(child.entries.last.path).to be == "another"
expect(child.read("another")).to be == "another contents\n"
end
it "should be visible in the parent archive" do
expect(parent.entries.length).to be == 8
previous_path = "dir/" + child.entries[-2].path
new_index = parent.entries.find_index {|e| e.path == previous_path} + 1
expect(parent.entries[new_index].path).to be == "dir/another"
expect(parent.read("dir/another")).to be == "another contents\n"
end
end
context "#delete" do
context "for a single file" do
before(:each) {child.delete("top")}
it "should be visible in the child archive" do
expect(child.entries.length).to be == 3
expect(child["top"]).to be_nil
end
it "should be visible in the parent archive" do
expect(parent.entries.length).to be == 6
expect(parent["dir/top"]).to be_nil
end
end
context "recursively" do
before(:each) {child.delete("sub", recursive: true)}
it "should be visible in the child archive" do
expect(child.entries.length).to be == 1
expect(child["sub/mid"]).to be_nil
expect(child["sub/dir"]).to be_nil
expect(child["sub/lower/bottom"]).to be_nil
end
it "should be visible in the parent archive" do
expect(parent.entries.length).to be == 4
expect(child["dir/sub/mid"]).to be_nil
expect(child["dir/sub/dir"]).to be_nil
expect(child["dir/sub/lower/bottom"]).to be_nil
end
end
end
context "#last_comment=" do
before(:each) {child.last_comment = "comment\n"}
it "sets the #last_comment field" do
expect(child.last_comment).to be == "comment\n"
end
it "affects the child's #to_hrx" do
expect(child.to_hrx).to end_with("<===>\ncomment\n")
end
it "doesn't affect the parent's #to_hrx" do
expect(parent.to_hrx).not_to include("\ncomment\n")
end
end
context "#add" do
context "with no position" do
before(:each) {child.add(HRX::File.new("another", "another contents\n"))}
it "should be visible in the child archive" do
expect(child.entries.length).to be == 5
expect(child.entries.last.path).to be == "another"
expect(child.read("another")).to be == "another contents\n"
end
it "should be visible in the parent archive" do
expect(parent.entries.length).to be == 8
expect(parent.entries.last.path).to be == "dir/another"
expect(parent.read("dir/another")).to be == "another contents\n"
end
end
context "with a position" do
before(:each) do
child.add(HRX::File.new("another", "another contents\n"), after: "sub/mid")
end
it "should be visible in the child archive" do
expect(child.entries.length).to be == 5
new_index = child.entries.find_index {|e| e.path == "sub/mid"} + 1
expect(child.entries[new_index].path).to be == "another"
expect(child.read("another")).to be == "another contents\n"
end
it "should be visible in the parent archive" do
expect(parent.entries.length).to be == 8
new_index = parent.entries.find_index {|e| e.path == "dir/sub/mid"} + 1
expect(parent.entries[new_index].path).to be == "dir/another"
expect(parent.read("dir/another")).to be == "another contents\n"
end
end
end
context "#to_hrx" do
it "should only serialize entries in the directory" do
expect(child.to_hrx).to be == < top
top-level child
<===> sub/mid
mid-level child
<===> sub/dir/
<===> sub/lower/bottom
bottom-level child
END
end
it "should reflect changes to the child archive" do
child << HRX::File.new("another", "")
expect(child.to_hrx).to be == < top
top-level child
<===> sub/mid
mid-level child
<===> sub/dir/
<===> sub/lower/bottom
bottom-level child
<===> another
END
end
it "should reflect changes to the parent archive" do
parent << HRX::File.new("dir/another", "")
expect(child.to_hrx).to be == < top
top-level child
<===> sub/mid
mid-level child
<===> sub/dir/
<===> sub/lower/bottom
bottom-level child
<===> another
END
end
end
end
hrx-1.0.0/spec/spec_helper.rb 0000644 0001750 0001750 00000007470 13641040325 014511 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'stringio'
module Helpers
# Runs a block with warnings disabled.
def silence_warnings
old_stderr = $stderr
$stderr = StringIO.new
yield
ensure
$stderr = old_stderr if old_stderr
end
# Runs a block with `encoding` as Encoding.default_external.
def with_external_encoding(encoding)
old_encoding = Encoding.default_external
silence_warnings {Encoding.default_external = "iso-8859-1"}
ensure
silence_warnings {Encoding.default_external = old_encoding if old_encoding}
end
end
RSpec.configure do |config|
config.include Helpers
config.expect_with :rspec do |expectations|
# This option will default to `true` in RSpec 4. It makes the `description`
# and `failure_message` of custom matchers include text for helper methods
# defined using `chain`, e.g.:
# be_bigger_than(2).and_smaller_than(4).description
# # => "be bigger than 2 and smaller than 4"
# ...rather than:
# # => "be bigger than 2"
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
# Prevents you from mocking or stubbing a method that does not exist on
# a real object. This is generally recommended, and will default to
# `true` in RSpec 4.
mocks.verify_partial_doubles = true
end
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
# have no way to turn it off -- the option exists only for backwards
# compatibility in RSpec 3). It causes shared context metadata to be
# inherited by the metadata hash of host groups and examples, rather than
# triggering implicit auto-inclusion in groups with matching metadata.
config.shared_context_metadata_behavior = :apply_to_host_groups
# Allows RSpec to persist some state between runs in order to support
# the `--only-failures` and `--next-failure` CLI options. We recommend
# you configure your source control system to ignore this file.
config.example_status_persistence_file_path = "spec/examples.txt"
# Limits the available syntax to the non-monkey patched syntax that is
# recommended. For more details, see:
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
config.disable_monkey_patching!
# This setting enables warnings. It's recommended, but in some cases may
# be too noisy due to issues in dependencies.
config.warnings = true
# Many RSpec users commonly either run the entire suite or an individual
# file, and it's useful to allow more verbose output when running an
# individual spec file.
if config.files_to_run.one?
# Use the documentation formatter for detailed output,
# unless a formatter has already been configured
# (e.g. via a command-line flag).
config.default_formatter = "doc"
end
# Seed global randomization in this process using the `--seed` CLI option.
# Setting this allows you to use `--seed` to deterministically reproduce
# test failures related to randomization by passing the same `--seed` value
# as the one that triggered the failure.
Kernel.srand config.seed
end
hrx-1.0.0/LICENSE 0000644 0001750 0001750 00000026136 13641040325 011746 0 ustar foka foka
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
hrx-1.0.0/README.md 0000644 0001750 0001750 00000002460 13641040325 012212 0 ustar foka foka # HRX (Human Readable Archive)
This gem is a parser and serializer for the [HRX format][].
[HRX format]: https://github.com/google/hrx
```ruby
# Load an archive from a path on disk. You can also parse it directly from a
# string using HRX::Archive.parse, or create an empty archive using
# HRX::Archive.new.
archive = HRX::Archive.load("path/to/archive.hrx")
# You can read files directly from an archive as though it were a filesystem.
puts archive.read("dir/file.txt")
# You can also write to files. Writing to a file implicitly creates any
# directories above it. You can also overwrite an existing file.
archive.write("dir/new.txt", "New file contents!\n")
# You can access HRX::File or HRX::Directory objects directly using
# HRX::Archive#[]. Unlike HRX::Archive#read(), this will just return nil if the
# entry isn't found.
archive["dir/file.txt"] # => HRX::File
# You can add files to the end of the archive using HRX::Archive#<< or
# HRX::Archive#add. If you pass `before:` or `after:`, you can control where in
# the archive they're added.
archive << HRX::File.new("dir/newer.txt", "Newer file contents!\n")
# Write the file back to disk. You can also use HRX::Archive#to_hrx to serialize
# the archive to a string.
archive.write!("path/to/archive.hrx")
```
This is not an officially supported Google product.
hrx-1.0.0/.rspec 0000644 0001750 0001750 00000000026 13641040325 012044 0 ustar foka foka --require spec_helper
hrx-1.0.0/hrx.gemspec 0000644 0001750 0001750 00000002105 13641040325 013075 0 ustar foka foka # Copyright 2018 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Gem::Specification.new do |s|
s.name = "hrx"
s.version = "1.0.0"
s.license = "Apache-2.0"
s.homepage = "https://github.com/google/hrx-ruby"
s.summary = "An HRX parser and serializer"
s.description = "A parser and serializer for the HRX human-readable archive format."
s.authors = ["Natalie Weizenbaum"]
s.email = "nweiz@google.com"
s.files = `git ls-files -z`.split("\x0")
s.add_runtime_dependency "linked-list", "~> 0.0.13"
s.required_ruby_version = ">= 2.3.0"
end
hrx-1.0.0/Gemfile 0000644 0001750 0001750 00000000171 13641040325 012223 0 ustar foka foka source "https://rubygems.org"
gemspec
gem "rspec", "~> 3.0", group: :test
gem "rspec-temp_dir", "~> 1.0", group: :test