hrx-1.0.0/0000755000175000017500000000000013641040325010731 5ustar fokafokahrx-1.0.0/lib/0000755000175000017500000000000013641040325011477 5ustar fokafokahrx-1.0.0/lib/hrx.rb0000644000175000017500000000160713641040325012631 0ustar fokafoka# 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/0000755000175000017500000000000013641040325012300 5ustar fokafokahrx-1.0.0/lib/hrx/parse_error.rb0000644000175000017500000000240713641040325015153 0ustar fokafoka# 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.rb0000644000175000017500000000531713641040325013552 0ustar fokafoka# 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.rb0000644000175000017500000003565213641040325014261 0ustar fokafoka# 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.rb0000644000175000017500000000357213641040325015265 0ustar fokafoka# 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.rb0000644000175000017500000000552013641040325013604 0ustar fokafoka# 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.rb0000644000175000017500000000124613641040325013761 0ustar fokafoka# 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.rb0000644000175000017500000000477013641040325014641 0ustar fokafoka# 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/.gitignore0000644000175000017500000000104113641040325012715 0ustar fokafoka*.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: .rvmrchrx-1.0.0/.rdoc_options0000644000175000017500000000057413641040325013442 0ustar fokafoka--- !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.md0000644000175000017500000000211513641040325013161 0ustar fokafoka# 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/0000755000175000017500000000000013641040325011663 5ustar fokafokahrx-1.0.0/spec/archive_spec.rb0000644000175000017500000006524013641040325014652 0ustar fokafoka# 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.rb0000644000175000017500000000334113641040325015654 0ustar fokafoka# 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.rb0000644000175000017500000000341613641040325015232 0ustar fokafoka# 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.rb0000644000175000017500000002044513641040325014341 0ustar fokafoka# 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.rb0000644000175000017500000000360413641040325014144 0ustar fokafoka# 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.rb0000644000175000017500000000377713641040325015216 0ustar fokafoka# 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.rb0000644000175000017500000002204013641040325016004 0ustar fokafoka# 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.rb0000644000175000017500000000747013641040325014511 0ustar fokafoka# 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/LICENSE0000644000175000017500000002613613641040325011746 0ustar fokafoka 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.md0000644000175000017500000000246013641040325012212 0ustar fokafoka# 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/.rspec0000644000175000017500000000002613641040325012044 0ustar fokafoka--require spec_helper hrx-1.0.0/hrx.gemspec0000644000175000017500000000210513641040325013075 0ustar fokafoka# 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/Gemfile0000644000175000017500000000017113641040325012223 0ustar fokafokasource "https://rubygems.org" gemspec gem "rspec", "~> 3.0", group: :test gem "rspec-temp_dir", "~> 1.0", group: :test