hrx 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +31 -0
- data/.rdoc_options +23 -0
- data/.rspec +1 -0
- data/CONTRIBUTING.md +28 -0
- data/Gemfile +6 -0
- data/LICENSE +202 -0
- data/README.md +35 -0
- data/hrx.gemspec +30 -0
- data/lib/hrx.rb +22 -0
- data/lib/hrx/archive.rb +461 -0
- data/lib/hrx/directory.rb +77 -0
- data/lib/hrx/error.rb +16 -0
- data/lib/hrx/file.rb +86 -0
- data/lib/hrx/ordered_node.rb +66 -0
- data/lib/hrx/parse_error.rb +40 -0
- data/lib/hrx/util.rb +81 -0
- data/spec/archive_spec.rb +871 -0
- data/spec/child_archive_spec.rb +304 -0
- data/spec/directory_spec.rb +57 -0
- data/spec/file_spec.rb +60 -0
- data/spec/ordered_node_spec.rb +60 -0
- data/spec/parse_spec.rb +346 -0
- data/spec/spec_helper.rb +95 -0
- data/spec/validates_path.rb +49 -0
- metadata +81 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
# Copyright 2018 Google Inc
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require_relative 'util'
|
16
|
+
|
17
|
+
# A directory in an HRX archive.
|
18
|
+
class HRX::Directory
|
19
|
+
# The comment that appeared before this directory, or `nil` if it had no
|
20
|
+
# preceding comment.
|
21
|
+
#
|
22
|
+
# HRX file contents are always encoded as UTF-8.
|
23
|
+
#
|
24
|
+
# This string is frozen.
|
25
|
+
attr_reader :comment
|
26
|
+
|
27
|
+
# The path to this file, relative to the archive's root, including the
|
28
|
+
# trailing `/`.
|
29
|
+
#
|
30
|
+
# HRX paths are always `/`-separated and always encoded as UTF-8.
|
31
|
+
#
|
32
|
+
# This string is frozen.
|
33
|
+
attr_reader :path
|
34
|
+
|
35
|
+
# Creates a new file with the given paths and comment.
|
36
|
+
#
|
37
|
+
# Throws an HRX::ParseError if `path` is invalid, or an EncodingError if
|
38
|
+
# either argument can't be converted to UTF-8.
|
39
|
+
#
|
40
|
+
# The `path` may or may not end with a `/`. If it doesn't a `/` will be added.
|
41
|
+
def initialize(path, comment: nil)
|
42
|
+
@comment = comment&.clone&.encode("UTF-8")&.freeze
|
43
|
+
@path = HRX::Util.scan_path(StringScanner.new(path.encode("UTF-8")))
|
44
|
+
@path << "/" unless @path.end_with?("/")
|
45
|
+
@path.freeze
|
46
|
+
end
|
47
|
+
|
48
|
+
# Like ::new, but doesn't verify that the arguments are valid.
|
49
|
+
def self._new_without_checks(path, comment) # :nodoc:
|
50
|
+
allocate.tap do |dir|
|
51
|
+
dir._initialize_without_checks(path, comment)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Like #initialize, but doesn't verify that the arguments are valid.
|
56
|
+
def _initialize_without_checks(path, comment) # :nodoc:
|
57
|
+
@comment = comment.freeze
|
58
|
+
@path = path.freeze
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns a copy of this entry with the path modified to be relative to
|
62
|
+
# `root`.
|
63
|
+
#
|
64
|
+
# If `root` is `nil`, returns this as-is.
|
65
|
+
def _relative(root) # :nodoc:
|
66
|
+
return self unless root
|
67
|
+
HRX::Directory._new_without_checks(HRX::Util.relative(root, path), comment)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns a copy of this entry with `root` added tothe beginning of the path.
|
71
|
+
#
|
72
|
+
# If `root` is `nil`, returns this as-is.
|
73
|
+
def _absolute(root) # :nodoc:
|
74
|
+
return self unless root
|
75
|
+
HRX::Directory._new_without_checks(root + path, comment)
|
76
|
+
end
|
77
|
+
end
|
data/lib/hrx/error.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Copyright 2018 Google Inc
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
# An error thrown by the HRX package.
|
16
|
+
class HRX::Error < StandardError; end
|
data/lib/hrx/file.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# Copyright 2018 Google Inc
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require_relative 'util'
|
16
|
+
|
17
|
+
# A file in an HRX archive.
|
18
|
+
class HRX::File
|
19
|
+
# The comment that appeared before this file, or `nil` if it had no
|
20
|
+
# preceding comment.
|
21
|
+
#
|
22
|
+
# HRX comments are always encoded as UTF-8.
|
23
|
+
#
|
24
|
+
# This string is frozen.
|
25
|
+
attr_reader :comment
|
26
|
+
|
27
|
+
# The path to this file, relative to the archive's root.
|
28
|
+
#
|
29
|
+
# HRX paths are always `/`-separated and always encoded as UTF-8.
|
30
|
+
#
|
31
|
+
# This string is frozen.
|
32
|
+
attr_reader :path
|
33
|
+
|
34
|
+
# The contents of the file.
|
35
|
+
#
|
36
|
+
# HRX file contents are always encoded as UTF-8.
|
37
|
+
#
|
38
|
+
# This string is frozen.
|
39
|
+
attr_reader :content
|
40
|
+
|
41
|
+
# Creates a new file with the given path, content, and comment.
|
42
|
+
#
|
43
|
+
# Throws an HRX::ParseError if `path` is invalid, or an EncodingError if any
|
44
|
+
# argument can't be converted to UTF-8.
|
45
|
+
def initialize(path, content, comment: nil)
|
46
|
+
@comment = comment&.clone&.encode("UTF-8")&.freeze
|
47
|
+
@path = HRX::Util.scan_path(StringScanner.new(path.encode("UTF-8"))).freeze
|
48
|
+
|
49
|
+
if @path.end_with?("/")
|
50
|
+
raise HRX::ParseError.new("path \"#{path}\" may not end with \"/\"", 1, path.length - 1)
|
51
|
+
end
|
52
|
+
|
53
|
+
@content = content.clone.encode("UTF-8").freeze
|
54
|
+
end
|
55
|
+
|
56
|
+
# Like ::new, but doesn't verify that the arguments are valid.
|
57
|
+
def self._new_without_checks(path, content, comment) # :nodoc:
|
58
|
+
allocate.tap do |file|
|
59
|
+
file._initialize_without_checks(path, content, comment)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Like #initialize, but doesn't verify that the arguments are valid.
|
64
|
+
def _initialize_without_checks(path, content, comment) # :nodoc:
|
65
|
+
@comment = comment.freeze
|
66
|
+
@path = path.freeze
|
67
|
+
@content = content.freeze
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns a copy of this entry with the path modified to be relative to
|
71
|
+
# `root`.
|
72
|
+
#
|
73
|
+
# If `root` is `nil`, returns this as-is.
|
74
|
+
def _relative(root) # :nodoc:
|
75
|
+
return self unless root
|
76
|
+
HRX::File._new_without_checks(HRX::Util.relative(root, path), content, comment)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns a copy of this entry with `root` added tothe beginning of the path.
|
80
|
+
#
|
81
|
+
# If `root` is `nil`, returns this as-is.
|
82
|
+
def _absolute(root) # :nodoc:
|
83
|
+
return self unless root
|
84
|
+
HRX::File._new_without_checks(root + path, content, comment)
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# Copyright 2018 Google Inc
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'linked-list'
|
16
|
+
|
17
|
+
# A linked list node that tracks its order reltaive to other nodes.
|
18
|
+
#
|
19
|
+
# This assumes that, while nodes may be added or removed from a given list, a
|
20
|
+
# given node object will only ever have one position in the list. This invariant
|
21
|
+
# is maintained by all methods of LinkedList::List other than
|
22
|
+
# LinkedList::List#reverse and LinkedList::List#reverse!.
|
23
|
+
#
|
24
|
+
# We use this to efficiently determine where to insert a new file relative to
|
25
|
+
# existing files with HRX#write.
|
26
|
+
class HRX::OrderedNode < LinkedList::Node # :nodoc:
|
27
|
+
def initialize(data)
|
28
|
+
super
|
29
|
+
@order = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
# The relative order of this node.
|
33
|
+
#
|
34
|
+
# This is guaranteed to be greater than the order of all nodes before this in
|
35
|
+
# the list, and less than the order of all nodes after it. Otherwise it
|
36
|
+
# provides no guarantees.
|
37
|
+
#
|
38
|
+
# This is not guaranteed to be stale over time.
|
39
|
+
def order
|
40
|
+
@order || 0
|
41
|
+
end
|
42
|
+
|
43
|
+
def next=(other)
|
44
|
+
@order ||=
|
45
|
+
if other.nil?
|
46
|
+
nil
|
47
|
+
elsif other.prev
|
48
|
+
(other.prev.order + other.order) / 2.0
|
49
|
+
else
|
50
|
+
other.order - 1
|
51
|
+
end
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
def prev=(other)
|
56
|
+
@order ||=
|
57
|
+
if other.nil?
|
58
|
+
nil
|
59
|
+
elsif other.next&.order
|
60
|
+
(other.next.order + other.order) / 2.0
|
61
|
+
else
|
62
|
+
other.order + 1
|
63
|
+
end
|
64
|
+
super
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Copyright 2018 Google Inc
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require_relative 'error'
|
16
|
+
|
17
|
+
# An error caused by an HRX file failing to parse correctly.
|
18
|
+
class HRX::ParseError < HRX::Error
|
19
|
+
# The 1-based line of the document on which the error occurred.
|
20
|
+
attr_reader :line
|
21
|
+
|
22
|
+
# The 1-based column of the line on which the error occurred.
|
23
|
+
attr_reader :column
|
24
|
+
|
25
|
+
# The file which failed to parse, or `nil` if the filename isn't known.
|
26
|
+
attr_reader :file
|
27
|
+
|
28
|
+
def initialize(message, line, column, file: nil)
|
29
|
+
super(message)
|
30
|
+
@line = line
|
31
|
+
@column = column
|
32
|
+
@file = file
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
buffer = String.new("Parse error on line #{line}, column #{column}")
|
37
|
+
buffer << " of #{file}" if file
|
38
|
+
buffer << ": #{super.to_s}"
|
39
|
+
end
|
40
|
+
end
|
data/lib/hrx/util.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# Copyright 2018 Google Inc
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require_relative 'parse_error'
|
16
|
+
|
17
|
+
module HRX::Util # :nodoc:
|
18
|
+
class << self
|
19
|
+
# Scans a single HRX path from `scanner` and returns it.
|
20
|
+
#
|
21
|
+
# Throws an ArgumentError if no valid path is available to scan. If
|
22
|
+
# `assert_done` is `true`, throws an ArgumentError if there's any text after
|
23
|
+
# the path.
|
24
|
+
def scan_path(scanner, assert_done: true, file: nil)
|
25
|
+
start = scanner.pos
|
26
|
+
while _scan_component(scanner, file) && scanner.scan(%r{/}); end
|
27
|
+
|
28
|
+
if assert_done && !scanner.eos?
|
29
|
+
parse_error(scanner, "Paths may not contain newlines", file: file)
|
30
|
+
elsif scanner.pos == start
|
31
|
+
parse_error(scanner, "Expected a path", file: file)
|
32
|
+
end
|
33
|
+
|
34
|
+
scanner.string.byteslice(start...scanner.pos)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Emits an ArgumentError with the given `message` and line and column
|
38
|
+
# information from the current position of `scanner`.
|
39
|
+
def parse_error(scanner, message, file: nil)
|
40
|
+
before = scanner.string.byteslice(0...scanner.pos)
|
41
|
+
line = before.count("\n") + 1
|
42
|
+
column = (before[/^.*\z/] || "").length + 1
|
43
|
+
|
44
|
+
raise HRX::ParseError.new(message, line, column, file: file)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns `child` relative to `parent`.
|
48
|
+
#
|
49
|
+
# Assumes `parent` ends with `/`, and `child` is beneath `parent`.
|
50
|
+
#
|
51
|
+
# If `parent` is `nil`, returns `child` as-is.
|
52
|
+
def relative(parent, child)
|
53
|
+
return child unless parent
|
54
|
+
child[parent.length..-1]
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Scans a single HRX path component from `scanner`.
|
60
|
+
#
|
61
|
+
# Returns whether or not a component could be found, or throws an
|
62
|
+
# HRX::ParseError if an invalid component was encountered.
|
63
|
+
def _scan_component(scanner, file)
|
64
|
+
return unless component = scanner.scan(%r{[^\u0000-\u001F\u007F/:\\]+})
|
65
|
+
if component == "." || component == ".."
|
66
|
+
scanner.unscan
|
67
|
+
parse_error(scanner, "Invalid path component \"#{component}\"", file: file)
|
68
|
+
end
|
69
|
+
|
70
|
+
if char = scanner.scan(/[\u0000-\u0009\u000B-\u001F\u007F]/)
|
71
|
+
scanner.unscan
|
72
|
+
parse_error(scanner, "Invalid character U+00#{char.ord.to_s(16).rjust(2, "0").upcase}", file: file)
|
73
|
+
elsif char = scanner.scan(/[\\:]/)
|
74
|
+
scanner.unscan
|
75
|
+
parse_error(scanner, "Invalid character \"#{char}\"", file: file)
|
76
|
+
end
|
77
|
+
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,871 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# Copyright 2018 Google Inc
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require 'rspec/temp_dir'
|
17
|
+
|
18
|
+
require 'hrx'
|
19
|
+
|
20
|
+
RSpec.describe HRX::Archive do
|
21
|
+
subject {HRX::Archive.new}
|
22
|
+
|
23
|
+
context "::load" do
|
24
|
+
include_context "uses temp dir"
|
25
|
+
|
26
|
+
it "parses a file from disk" do
|
27
|
+
File.write("#{temp_dir}/archive.hrx", <<END, mode: "wb")
|
28
|
+
<===> file
|
29
|
+
contents
|
30
|
+
END
|
31
|
+
|
32
|
+
archive = HRX::Archive.load("#{temp_dir}/archive.hrx")
|
33
|
+
expect(archive.entries.length).to be == 1
|
34
|
+
expect(archive.entries.last.path).to be == "file"
|
35
|
+
expect(archive.entries.last.content).to be == "contents\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "parses a file as UTF-8" do
|
39
|
+
File.write("#{temp_dir}/archive.hrx", "<===> 👭\n", mode: "wb")
|
40
|
+
archive = HRX::Archive.load("#{temp_dir}/archive.hrx")
|
41
|
+
expect(archive.entries.last.path).to be == "👭"
|
42
|
+
end
|
43
|
+
|
44
|
+
it "parses a file as UTF-8 despite Encoding.default_external" do
|
45
|
+
File.write("#{temp_dir}/archive.hrx", "<===> föö\n", mode: "wb")
|
46
|
+
|
47
|
+
with_external_encoding("iso-8859-1") do
|
48
|
+
archive = HRX::Archive.load("#{temp_dir}/archive.hrx")
|
49
|
+
expect(archive.entries.last.path).to be == "föö"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
it "fails to parse a file that's invalid UTF-8" do
|
54
|
+
File.write("#{temp_dir}/archive.hrx", "<===> \xc3\x28\n".b, mode: "wb")
|
55
|
+
expect {HRX::Archive.load("#{temp_dir}/archive.hrx")}.to raise_error(EncodingError)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "includes the filename in parse errors" do
|
59
|
+
File.write("#{temp_dir}/archive.hrx", "wrong", mode: "wb")
|
60
|
+
expect {HRX::Archive.load("#{temp_dir}/archive.hrx")}.to raise_error(HRX::ParseError, /archive\.hrx/)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when first initialized" do
|
65
|
+
it "has no entries" do
|
66
|
+
expect(subject.entries).to be_empty
|
67
|
+
end
|
68
|
+
|
69
|
+
context "#read" do
|
70
|
+
it "fails for any path" do
|
71
|
+
expect {subject.read("path")}.to raise_error(HRX::Error)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "#write" do
|
76
|
+
before(:each) {subject.write("path", "contents\n")}
|
77
|
+
|
78
|
+
it "adds a file to the end of the archive" do
|
79
|
+
expect(subject.entries.last.path).to be == "path"
|
80
|
+
expect(subject.entries.last.content).to be == "contents\n"
|
81
|
+
end
|
82
|
+
|
83
|
+
it "adds a file that's readable by name" do
|
84
|
+
expect(subject.read("path")).to be == "contents\n"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context "#child_archive" do
|
89
|
+
it "fails for any path" do
|
90
|
+
expect {subject.child_archive("path")}.to raise_error(HRX::Error)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "#initialize" do
|
96
|
+
it "should forbid boundary_length 0" do
|
97
|
+
expect {HRX::Archive.new(boundary_length: 0)}.to raise_error(ArgumentError)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should forbid negative boundary_length" do
|
101
|
+
expect {HRX::Archive.new(boundary_length: -1)}.to raise_error(ArgumentError)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context "#entries" do
|
106
|
+
it "is frozen" do
|
107
|
+
expect do
|
108
|
+
subject.entries << HRX::Directory.new("dir")
|
109
|
+
end.to raise_error(RuntimeError)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "reflects new entries" do
|
113
|
+
expect(subject.entries).to be_empty
|
114
|
+
dir = HRX::Directory.new("dir")
|
115
|
+
subject << dir
|
116
|
+
expect(subject.entries).to be == [dir]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "#last_comment=" do
|
121
|
+
it "requires the comment to be convertible to UTF-8" do
|
122
|
+
expect do
|
123
|
+
subject.last_comment = "\xc3\x28".b
|
124
|
+
end.to raise_error(EncodingError)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "converts a comment to UTF-8" do
|
128
|
+
subject.last_comment = "いか".encode("SJIS")
|
129
|
+
expect(subject.last_comment).to be == "いか"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "with files and directories in the archive" do
|
134
|
+
subject {HRX::Archive.parse(<<END)}
|
135
|
+
<===> file
|
136
|
+
file contents
|
137
|
+
|
138
|
+
<===> dir/
|
139
|
+
<===>
|
140
|
+
comment contents
|
141
|
+
|
142
|
+
<===> super/sub
|
143
|
+
sub contents
|
144
|
+
|
145
|
+
<===> very/deeply/
|
146
|
+
<===> very/deeply/nested/file
|
147
|
+
nested contents
|
148
|
+
|
149
|
+
<===> last
|
150
|
+
the last file
|
151
|
+
END
|
152
|
+
|
153
|
+
context "#[]" do
|
154
|
+
it "doesn't return an empty path" do
|
155
|
+
expect(subject[""]).to be_nil
|
156
|
+
end
|
157
|
+
|
158
|
+
it "doesn't return a path that's not in the archive" do
|
159
|
+
expect(subject["non/existent/file"]).to be_nil
|
160
|
+
end
|
161
|
+
|
162
|
+
it "doesn't return an implicit directory" do
|
163
|
+
expect(subject["super"]).to be_nil
|
164
|
+
end
|
165
|
+
|
166
|
+
it "doesn't return a file wih a slash" do
|
167
|
+
expect(subject["super/sub/"]).to be_nil
|
168
|
+
end
|
169
|
+
|
170
|
+
it "returns a file at the root level" do
|
171
|
+
expect(subject["file"].content).to be == "file contents\n"
|
172
|
+
end
|
173
|
+
|
174
|
+
it "returns a file in a directory" do
|
175
|
+
expect(subject["super/sub"].content).to be == "sub contents\n"
|
176
|
+
end
|
177
|
+
|
178
|
+
it "returns an explicit directory" do
|
179
|
+
expect(subject["dir"].path).to be == "dir/"
|
180
|
+
end
|
181
|
+
|
182
|
+
it "returns an explicit directory with a leading slash" do
|
183
|
+
expect(subject["dir/"].path).to be == "dir/"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context "#read" do
|
188
|
+
it "throws for an empty path" do
|
189
|
+
expect {subject.read("")}.to raise_error(HRX::Error, 'There is no file at ""')
|
190
|
+
end
|
191
|
+
|
192
|
+
it "throws for a path that's not in the archive" do
|
193
|
+
expect {subject.read("non/existent/file")}.to(
|
194
|
+
raise_error(HRX::Error, 'There is no file at "non/existent/file"'))
|
195
|
+
end
|
196
|
+
|
197
|
+
it "throws for an implicit directory" do
|
198
|
+
expect {subject.read("super")}.to raise_error(HRX::Error, 'There is no file at "super"')
|
199
|
+
end
|
200
|
+
|
201
|
+
it "throws for a file wih a slash" do
|
202
|
+
expect {subject.read("super/sub/")}.to(
|
203
|
+
raise_error(HRX::Error, 'There is no file at "super/sub/"'))
|
204
|
+
end
|
205
|
+
|
206
|
+
it "throws for a directory" do
|
207
|
+
expect {subject.read("dir")}.to raise_error(HRX::Error, '"dir/" is a directory')
|
208
|
+
end
|
209
|
+
|
210
|
+
it "returns the contents of a file at the root level" do
|
211
|
+
expect(subject.read("file")).to be == "file contents\n"
|
212
|
+
end
|
213
|
+
|
214
|
+
it "returns the contents of a file in a directory" do
|
215
|
+
expect(subject.read("super/sub")).to be == "sub contents\n"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
context "#glob" do
|
220
|
+
it "returns nothing for an empty glob" do
|
221
|
+
expect(subject.glob("")).to be_empty
|
222
|
+
end
|
223
|
+
|
224
|
+
it "returns nothing for a path that's not in the archive" do
|
225
|
+
expect(subject.glob("non/existent/file")).to be_empty
|
226
|
+
end
|
227
|
+
|
228
|
+
it "doesn't return implicit directories" do
|
229
|
+
expect(subject.glob("super")).to be_empty
|
230
|
+
end
|
231
|
+
|
232
|
+
it "doesn't return a file with a slash" do
|
233
|
+
expect(subject.glob("super/sub/")).to be_empty
|
234
|
+
end
|
235
|
+
|
236
|
+
it "doesn't return an explicit directory without a leading slash" do
|
237
|
+
expect(subject.glob("dir")).to be_empty
|
238
|
+
end
|
239
|
+
|
240
|
+
it "returns a file at the root level" do
|
241
|
+
result = subject.glob("file")
|
242
|
+
expect(result.length).to be == 1
|
243
|
+
expect(result.first.path).to be == "file"
|
244
|
+
end
|
245
|
+
|
246
|
+
it "returns a file in a directory" do
|
247
|
+
result = subject.glob("super/sub")
|
248
|
+
expect(result.length).to be == 1
|
249
|
+
expect(result.first.path).to be == "super/sub"
|
250
|
+
end
|
251
|
+
|
252
|
+
it "returns an explicit directory" do
|
253
|
+
result = subject.glob("dir/")
|
254
|
+
expect(result.length).to be == 1
|
255
|
+
expect(result.first.path).to be == "dir/"
|
256
|
+
end
|
257
|
+
|
258
|
+
it "returns all matching files at the root level" do
|
259
|
+
result = subject.glob("*")
|
260
|
+
expect(result.length).to be == 2
|
261
|
+
expect(result.first.path).to be == "file"
|
262
|
+
expect(result.last.path).to be == "last"
|
263
|
+
end
|
264
|
+
|
265
|
+
it "returns all matching files in a directory" do
|
266
|
+
result = subject.glob("super/*")
|
267
|
+
expect(result.length).to be == 1
|
268
|
+
expect(result.first.path).to be == "super/sub"
|
269
|
+
end
|
270
|
+
|
271
|
+
it "returns all matching entries recursively in a directory" do
|
272
|
+
result = subject.glob("very/**/*")
|
273
|
+
expect(result.length).to be == 2
|
274
|
+
expect(result.first.path).to be == "very/deeply/"
|
275
|
+
expect(result.last.path).to be == "very/deeply/nested/file"
|
276
|
+
end
|
277
|
+
|
278
|
+
it "respects glob flags" do
|
279
|
+
result = subject.glob("FILE", File::FNM_CASEFOLD)
|
280
|
+
expect(result.length).to be == 1
|
281
|
+
expect(result.first.path).to be == "file"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
context "#child_archive" do
|
286
|
+
it "throws for an empty path" do
|
287
|
+
expect {subject.child_archive("")}.to raise_error(HRX::Error, 'There is no directory at ""')
|
288
|
+
end
|
289
|
+
|
290
|
+
it "throws for a path that's not in the archive" do
|
291
|
+
expect {subject.child_archive("non/existent/dir")}.to(
|
292
|
+
raise_error(HRX::Error, 'There is no directory at "non/existent/dir"'))
|
293
|
+
end
|
294
|
+
|
295
|
+
it "throws for a file" do
|
296
|
+
expect {subject.child_archive("super/sub")}.to(
|
297
|
+
raise_error(HRX::Error, '"super/sub" is a file'))
|
298
|
+
end
|
299
|
+
|
300
|
+
context "for an explicit directory with no children" do
|
301
|
+
let(:child) {subject.child_archive("dir")}
|
302
|
+
|
303
|
+
it "returns an empty archive" do
|
304
|
+
expect(child.entries).to be_empty
|
305
|
+
end
|
306
|
+
|
307
|
+
it "serializes to an empty string" do
|
308
|
+
expect(child.to_hrx).to be_empty
|
309
|
+
end
|
310
|
+
|
311
|
+
it "doesn't return the root directory" do
|
312
|
+
expect(child[""]).to be_nil
|
313
|
+
expect(child["/"]).to be_nil
|
314
|
+
expect(child["dir"]).to be_nil
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
context "#write" do
|
320
|
+
it "validates the path" do
|
321
|
+
expect {subject.write("super/./sub", "")}.to raise_error(HRX::ParseError)
|
322
|
+
end
|
323
|
+
|
324
|
+
it "rejects a path that ends in a slash" do
|
325
|
+
expect {subject.write("file/", "")}.to raise_error(HRX::ParseError)
|
326
|
+
end
|
327
|
+
|
328
|
+
it "fails if a parent directory is a file" do
|
329
|
+
expect {subject.write("file/sub", "")}.to raise_error(HRX::Error)
|
330
|
+
end
|
331
|
+
|
332
|
+
it "fails if the path is an explicit directory" do
|
333
|
+
expect {subject.write("dir", "")}.to raise_error(HRX::Error)
|
334
|
+
end
|
335
|
+
|
336
|
+
it "fails if the path is an implicit directory" do
|
337
|
+
expect {subject.write("super", "")}.to raise_error(HRX::Error)
|
338
|
+
end
|
339
|
+
|
340
|
+
context "with a top-level file" do
|
341
|
+
before(:each) {subject.write("new", "new contents\n")}
|
342
|
+
|
343
|
+
it "adds to the end of the archive" do
|
344
|
+
expect(subject.entries.last.path).to be == "new"
|
345
|
+
expect(subject.entries.last.content).to be == "new contents\n"
|
346
|
+
end
|
347
|
+
|
348
|
+
it "adds a file that's readable by name" do
|
349
|
+
expect(subject.read("new")).to be == "new contents\n"
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
context "with a file in a new directory tree" do
|
354
|
+
before(:each) {subject.write("new/sub/file", "new contents\n")}
|
355
|
+
|
356
|
+
it "adds to the end of the archive" do
|
357
|
+
expect(subject.entries.last.path).to be == "new/sub/file"
|
358
|
+
expect(subject.entries.last.content).to be == "new contents\n"
|
359
|
+
end
|
360
|
+
|
361
|
+
it "adds a file that's readable by name" do
|
362
|
+
expect(subject.read("new/sub/file")).to be == "new contents\n"
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
context "with a file in an explicit directory" do
|
367
|
+
before(:each) {subject.write("dir/new", "new contents\n")}
|
368
|
+
|
369
|
+
it "adds to the end of the directory" do
|
370
|
+
new_index = subject.entries.find_index {|e| e.path == "dir/"} + 1
|
371
|
+
expect(subject.entries[new_index].path).to be == "dir/new"
|
372
|
+
expect(subject.entries[new_index].content).to be == "new contents\n"
|
373
|
+
end
|
374
|
+
|
375
|
+
it "adds a file that's readable by name" do
|
376
|
+
expect(subject.read("dir/new")).to be == "new contents\n"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
context "with a file in an implicit directory" do
|
381
|
+
before(:each) {subject.write("super/another", "new contents\n")}
|
382
|
+
|
383
|
+
it "adds to the end of the directory" do
|
384
|
+
new_index = subject.entries.find_index {|e| e.path == "super/sub"} + 1
|
385
|
+
expect(subject.entries[new_index].path).to be == "super/another"
|
386
|
+
expect(subject.entries[new_index].content).to be == "new contents\n"
|
387
|
+
end
|
388
|
+
|
389
|
+
it "adds a file that's readable by name" do
|
390
|
+
expect(subject.read("super/another")).to be == "new contents\n"
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
context "with a file in an implicit directory that's not a sibling" do
|
395
|
+
before(:each) {subject.write("very/different/nesting", "new contents\n")}
|
396
|
+
|
397
|
+
it "adds after its cousin" do
|
398
|
+
new_index = subject.entries.find_index {|e| e.path == "very/deeply/nested/file"} + 1
|
399
|
+
expect(subject.entries[new_index].path).to be == "very/different/nesting"
|
400
|
+
expect(subject.entries[new_index].content).to be == "new contents\n"
|
401
|
+
end
|
402
|
+
|
403
|
+
it "adds a file that's readable by name" do
|
404
|
+
expect(subject.read("very/different/nesting")).to be == "new contents\n"
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
context "with an existing filename" do
|
409
|
+
let (:old_index) {subject.entries.find_index {|e| e.path == "super/sub"}}
|
410
|
+
before(:each) {subject.write("super/sub", "new contents\n")}
|
411
|
+
|
412
|
+
it "overwrites that file" do
|
413
|
+
expect(subject.read("super/sub")).to be == "new contents\n"
|
414
|
+
end
|
415
|
+
|
416
|
+
it "uses the same location as that file" do
|
417
|
+
expect(subject.entries[old_index].path).to be == "super/sub"
|
418
|
+
expect(subject.entries[old_index].content).to be == "new contents\n"
|
419
|
+
end
|
420
|
+
|
421
|
+
it "removes the comment" do
|
422
|
+
expect(subject.entries[old_index].comment).to be_nil
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
context "with a comment" do
|
427
|
+
it "writes the comment" do
|
428
|
+
subject.write("new", "", comment: "new comment\n")
|
429
|
+
expect(subject["new"].comment).to be == "new comment\n"
|
430
|
+
end
|
431
|
+
|
432
|
+
it "overwrites an existing comment" do
|
433
|
+
subject.write("super/sub", "", comment: "new comment\n")
|
434
|
+
expect(subject["super/sub"].comment).to be == "new comment\n"
|
435
|
+
end
|
436
|
+
|
437
|
+
it "re-uses an existing comment with :copy" do
|
438
|
+
subject.write("super/sub", "", comment: :copy)
|
439
|
+
expect(subject["super/sub"].comment).to be == "comment contents\n"
|
440
|
+
end
|
441
|
+
|
442
|
+
it "ignores :copy for a new file" do
|
443
|
+
subject.write("new", "", comment: :copy)
|
444
|
+
expect(subject["new"].comment).to be_nil
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
context "#delete" do
|
450
|
+
it "throws an error if the file doesn't exist" do
|
451
|
+
expect {subject.delete("nothing")}.to(
|
452
|
+
raise_error(HRX::Error, '"nothing" doesn\'t exist'))
|
453
|
+
end
|
454
|
+
|
455
|
+
it "throws an error if the file is in a directory that doesn't exist" do
|
456
|
+
expect {subject.delete("does/not/exist")}.to(
|
457
|
+
raise_error(HRX::Error, '"does/not/exist" doesn\'t exist'))
|
458
|
+
end
|
459
|
+
|
460
|
+
it "throws an error if a file has a trailing slash" do
|
461
|
+
expect {subject.delete("file/")}.to raise_error(HRX::Error, '"file/" is a file')
|
462
|
+
end
|
463
|
+
|
464
|
+
it "refuses to delete an implicit directory" do
|
465
|
+
expect {subject.delete("super/")}.to(
|
466
|
+
raise_error(HRX::Error, '"super/" is not an explicit directory and recursive isn\'t set'))
|
467
|
+
end
|
468
|
+
|
469
|
+
it "deletes a top-level file" do
|
470
|
+
length_before = subject.entries.length
|
471
|
+
subject.delete("file")
|
472
|
+
expect(subject["file"]).to be_nil
|
473
|
+
expect(subject.entries.length).to be == length_before - 1
|
474
|
+
end
|
475
|
+
|
476
|
+
it "deletes a nested file" do
|
477
|
+
length_before = subject.entries.length
|
478
|
+
subject.delete("super/sub")
|
479
|
+
expect(subject["super/sub"]).to be_nil
|
480
|
+
expect(subject.entries.length).to be == length_before - 1
|
481
|
+
end
|
482
|
+
|
483
|
+
it "deletes an explicit directory without a slash" do
|
484
|
+
length_before = subject.entries.length
|
485
|
+
subject.delete("dir")
|
486
|
+
expect(subject["dir/"]).to be_nil
|
487
|
+
expect(subject.entries.length).to be == length_before - 1
|
488
|
+
end
|
489
|
+
|
490
|
+
it "deletes an explicit directory with a slash" do
|
491
|
+
length_before = subject.entries.length
|
492
|
+
subject.delete("dir/")
|
493
|
+
expect(subject["dir/"]).to be_nil
|
494
|
+
expect(subject.entries.length).to be == length_before - 1
|
495
|
+
end
|
496
|
+
|
497
|
+
it "deletes an explicit directory with children" do
|
498
|
+
length_before = subject.entries.length
|
499
|
+
subject.delete("very/deeply")
|
500
|
+
expect(subject["very/deeply"]).to be_nil
|
501
|
+
expect(subject.entries.length).to be == length_before - 1
|
502
|
+
end
|
503
|
+
|
504
|
+
it "recursively deletes an implicit directory" do
|
505
|
+
length_before = subject.entries.length
|
506
|
+
subject.delete("very/", recursive: true)
|
507
|
+
expect(subject["very/deeply"]).to be_nil
|
508
|
+
expect(subject["very/deeply/nested/file"]).to be_nil
|
509
|
+
expect(subject.entries.length).to be == length_before - 2
|
510
|
+
end
|
511
|
+
|
512
|
+
it "recursively deletes an explicit directory" do
|
513
|
+
length_before = subject.entries.length
|
514
|
+
subject.delete("very/deeply", recursive: true)
|
515
|
+
expect(subject["very/deeply"]).to be_nil
|
516
|
+
expect(subject["very/deeply/nested/file"]).to be_nil
|
517
|
+
expect(subject.entries.length).to be == length_before - 2
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
context "#add" do
|
522
|
+
it "adds a file to the end of the archive" do
|
523
|
+
file = HRX::File.new("other", "")
|
524
|
+
subject << file
|
525
|
+
expect(subject.entries.last).to be == file
|
526
|
+
end
|
527
|
+
|
528
|
+
it "adds a file in an existing directory to the end of the archive" do
|
529
|
+
file = HRX::File.new("dir/other", "")
|
530
|
+
subject << file
|
531
|
+
expect(subject.entries.last).to be == file
|
532
|
+
end
|
533
|
+
|
534
|
+
it "allows an implicit directory to be made explicit" do
|
535
|
+
dir = HRX::Directory.new("super")
|
536
|
+
subject << dir
|
537
|
+
expect(subject.entries.last).to be == dir
|
538
|
+
end
|
539
|
+
|
540
|
+
it "throws an error for a duplicate file" do
|
541
|
+
expect do
|
542
|
+
subject << HRX::File.new("file", "")
|
543
|
+
end.to raise_error(HRX::Error, '"file" defined twice')
|
544
|
+
end
|
545
|
+
|
546
|
+
it "throws an error for a duplicate directory" do
|
547
|
+
expect do
|
548
|
+
subject << HRX::Directory.new("dir")
|
549
|
+
end.to raise_error(HRX::Error, '"dir/" defined twice')
|
550
|
+
end
|
551
|
+
|
552
|
+
it "throws an error for a file with a directory's name" do
|
553
|
+
expect do
|
554
|
+
subject << HRX::File.new("dir", "")
|
555
|
+
end.to raise_error(HRX::Error, '"dir" defined twice')
|
556
|
+
end
|
557
|
+
|
558
|
+
it "throws an error for a file with an implicit directory's name" do
|
559
|
+
expect do
|
560
|
+
subject << HRX::File.new("super", "")
|
561
|
+
end.to raise_error(HRX::Error, '"super" defined twice')
|
562
|
+
end
|
563
|
+
|
564
|
+
it "throws an error for a directory with a file's name" do
|
565
|
+
expect do
|
566
|
+
subject << HRX::Directory.new("file")
|
567
|
+
end.to raise_error(HRX::Error, '"file/" defined twice')
|
568
|
+
end
|
569
|
+
|
570
|
+
context "with :before" do
|
571
|
+
it "adds the new entry before the given file" do
|
572
|
+
subject.add HRX::File.new("other", ""), before: "super/sub"
|
573
|
+
expect(subject.entries[2].path).to be == "other"
|
574
|
+
end
|
575
|
+
|
576
|
+
it "adds the new entry before the given directory" do
|
577
|
+
subject.add HRX::File.new("other", ""), before: "dir/"
|
578
|
+
expect(subject.entries[1].path).to be == "other"
|
579
|
+
end
|
580
|
+
|
581
|
+
it "adds the new entry before the given directory without a /" do
|
582
|
+
subject.add HRX::File.new("other", ""), before: "dir"
|
583
|
+
expect(subject.entries[1].path).to be == "other"
|
584
|
+
end
|
585
|
+
|
586
|
+
it "fails if the path can't be found" do
|
587
|
+
expect do
|
588
|
+
subject.add HRX::File.new("other", ""), before: "asdf"
|
589
|
+
end.to raise_error(HRX::Error, 'There is no entry named "asdf"')
|
590
|
+
end
|
591
|
+
|
592
|
+
it "fails if the path is an implicit directory" do
|
593
|
+
expect do
|
594
|
+
subject.add HRX::File.new("other", ""), before: "super"
|
595
|
+
end.to raise_error(HRX::Error, 'There is no entry named "super"')
|
596
|
+
end
|
597
|
+
|
598
|
+
it "fails if a trailing slash is used for a file" do
|
599
|
+
expect do
|
600
|
+
subject.add HRX::File.new("other", ""), before: "file/"
|
601
|
+
end.to raise_error(HRX::Error, 'There is no entry named "file/"')
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
context "with :after" do
|
606
|
+
it "adds the new entry after the given file" do
|
607
|
+
subject.add HRX::File.new("other", ""), after: "super/sub"
|
608
|
+
expect(subject.entries[3].path).to be == "other"
|
609
|
+
end
|
610
|
+
|
611
|
+
it "adds the new entry after the given directory" do
|
612
|
+
subject.add HRX::File.new("other", ""), after: "dir/"
|
613
|
+
expect(subject.entries[2].path).to be == "other"
|
614
|
+
end
|
615
|
+
|
616
|
+
it "adds the new entry after the given directory without a /" do
|
617
|
+
subject.add HRX::File.new("other", ""), after: "dir"
|
618
|
+
expect(subject.entries[2].path).to be == "other"
|
619
|
+
end
|
620
|
+
|
621
|
+
it "fails if the path can't be found" do
|
622
|
+
expect do
|
623
|
+
subject.add HRX::File.new("other", ""), after: "asdf"
|
624
|
+
end.to raise_error(HRX::Error, 'There is no entry named "asdf"')
|
625
|
+
end
|
626
|
+
|
627
|
+
it "fails if the path is an implicit directory" do
|
628
|
+
expect do
|
629
|
+
subject.add HRX::File.new("other", ""), after: "super"
|
630
|
+
end.to raise_error(HRX::Error, 'There is no entry named "super"')
|
631
|
+
end
|
632
|
+
|
633
|
+
it "fails if a trailing slash is used for a file" do
|
634
|
+
expect do
|
635
|
+
subject.add HRX::File.new("other", ""), after: "file/"
|
636
|
+
end.to raise_error(HRX::Error, 'There is no entry named "file/"')
|
637
|
+
end
|
638
|
+
end
|
639
|
+
end
|
640
|
+
end
|
641
|
+
|
642
|
+
context "with physically distant files in the same directory" do
|
643
|
+
subject {HRX::Archive.parse(<<END)}
|
644
|
+
<===> dir/super/sub1
|
645
|
+
sub1 contents
|
646
|
+
|
647
|
+
<===> base 1
|
648
|
+
<===> dir/other/child1
|
649
|
+
child1 contents
|
650
|
+
|
651
|
+
<===> base 2
|
652
|
+
<===> dir/super/sub2
|
653
|
+
sub2 contents
|
654
|
+
|
655
|
+
<===> base 3
|
656
|
+
<===> dir/other/child2
|
657
|
+
child2 contents
|
658
|
+
|
659
|
+
<===> base 4
|
660
|
+
<===> dir/super/sub3
|
661
|
+
sub3 contents
|
662
|
+
|
663
|
+
<===> base 5
|
664
|
+
END
|
665
|
+
|
666
|
+
context "#write" do
|
667
|
+
context "with a file in an implicit directory" do
|
668
|
+
before(:each) {subject.write("dir/other/new", "new contents\n")}
|
669
|
+
|
670
|
+
it "adds after the last file in the directory" do
|
671
|
+
new_index = subject.entries.find_index {|e| e.path == "dir/other/child2"} + 1
|
672
|
+
expect(subject.entries[new_index].path).to be == "dir/other/new"
|
673
|
+
expect(subject.entries[new_index].content).to be == "new contents\n"
|
674
|
+
end
|
675
|
+
|
676
|
+
it "adds a file that's readable by name" do
|
677
|
+
expect(subject.read("dir/other/new")).to be == "new contents\n"
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
context "with a file in an implicit directory that's not a sibling" do
|
682
|
+
before(:each) {subject.write("dir/another/new", "new contents\n")}
|
683
|
+
|
684
|
+
it "adds to the end of the directory" do
|
685
|
+
new_index = subject.entries.find_index {|e| e.path == "dir/super/sub3"} + 1
|
686
|
+
expect(subject.entries[new_index].path).to be == "dir/another/new"
|
687
|
+
expect(subject.entries[new_index].content).to be == "new contents\n"
|
688
|
+
end
|
689
|
+
|
690
|
+
it "adds a file that's readable by name" do
|
691
|
+
expect(subject.read("dir/another/new")).to be == "new contents\n"
|
692
|
+
end
|
693
|
+
end
|
694
|
+
end
|
695
|
+
end
|
696
|
+
|
697
|
+
context "#to_hrx" do
|
698
|
+
it "returns the empty string for an empty file" do
|
699
|
+
expect(subject.to_hrx).to be_empty
|
700
|
+
end
|
701
|
+
|
702
|
+
it "writes a file's name and contents" do
|
703
|
+
subject << HRX::File.new("file", "contents\n")
|
704
|
+
expect(subject.to_hrx).to be == <<END
|
705
|
+
<===> file
|
706
|
+
contents
|
707
|
+
END
|
708
|
+
end
|
709
|
+
|
710
|
+
it "adds a newline to a middle file with a newline" do
|
711
|
+
subject << HRX::File.new("file 1", "contents 1\n")
|
712
|
+
subject << HRX::File.new("file 2", "contents 2\n")
|
713
|
+
expect(subject.to_hrx).to be == <<END
|
714
|
+
<===> file 1
|
715
|
+
contents 1
|
716
|
+
|
717
|
+
<===> file 2
|
718
|
+
contents 2
|
719
|
+
END
|
720
|
+
end
|
721
|
+
|
722
|
+
it "adds a newline to a middle file without a newline" do
|
723
|
+
subject << HRX::File.new("file 1", "contents 1")
|
724
|
+
subject << HRX::File.new("file 2", "contents 2\n")
|
725
|
+
expect(subject.to_hrx).to be == <<END
|
726
|
+
<===> file 1
|
727
|
+
contents 1
|
728
|
+
<===> file 2
|
729
|
+
contents 2
|
730
|
+
END
|
731
|
+
end
|
732
|
+
|
733
|
+
it "writes empty files" do
|
734
|
+
subject << HRX::File.new("file 1", "")
|
735
|
+
subject << HRX::File.new("file 2", "")
|
736
|
+
expect(subject.to_hrx).to be == <<END
|
737
|
+
<===> file 1
|
738
|
+
<===> file 2
|
739
|
+
END
|
740
|
+
end
|
741
|
+
|
742
|
+
it "doesn't add a newline to the last file" do
|
743
|
+
subject << HRX::File.new("file", "contents")
|
744
|
+
expect(subject.to_hrx).to be == "<===> file\ncontents"
|
745
|
+
end
|
746
|
+
|
747
|
+
it "writes a directory" do
|
748
|
+
subject << HRX::Directory.new("dir")
|
749
|
+
expect(subject.to_hrx).to be == "<===> dir/\n"
|
750
|
+
end
|
751
|
+
|
752
|
+
it "writes a comment on a file" do
|
753
|
+
subject << HRX::File.new("file", "contents\n", comment: "comment")
|
754
|
+
expect(subject.to_hrx).to be == <<END
|
755
|
+
<===>
|
756
|
+
comment
|
757
|
+
<===> file
|
758
|
+
contents
|
759
|
+
END
|
760
|
+
end
|
761
|
+
|
762
|
+
it "writes a comment on a directory" do
|
763
|
+
subject << HRX::Directory.new("dir", comment: "comment")
|
764
|
+
expect(subject.to_hrx).to be == <<END
|
765
|
+
<===>
|
766
|
+
comment
|
767
|
+
<===> dir/
|
768
|
+
END
|
769
|
+
end
|
770
|
+
|
771
|
+
it "uses a different boundary length to avoid conflicts" do
|
772
|
+
subject << HRX::File.new("file", "<===>\n")
|
773
|
+
expect(subject.to_hrx).to be == <<END
|
774
|
+
<====> file
|
775
|
+
<===>
|
776
|
+
END
|
777
|
+
end
|
778
|
+
|
779
|
+
it "uses a different boundary length to avoid conflicts in comments" do
|
780
|
+
subject << HRX::File.new("file", "", comment: "<===>")
|
781
|
+
expect(subject.to_hrx).to be == <<END
|
782
|
+
<====>
|
783
|
+
<===>
|
784
|
+
<====> file
|
785
|
+
END
|
786
|
+
end
|
787
|
+
|
788
|
+
it "uses a different boundary length to avoid multiple conflicts" do
|
789
|
+
subject << HRX::File.new("file", <<END)
|
790
|
+
<===>
|
791
|
+
<====> foo
|
792
|
+
<=====>
|
793
|
+
END
|
794
|
+
expect(subject.to_hrx).to be == <<END
|
795
|
+
<======> file
|
796
|
+
<===>
|
797
|
+
<====> foo
|
798
|
+
<=====>
|
799
|
+
END
|
800
|
+
end
|
801
|
+
|
802
|
+
it "uses a different boundary length to avoid multiple conflicts in multiple files" do
|
803
|
+
subject << HRX::File.new("file 1", "<===>\n")
|
804
|
+
subject << HRX::File.new("file 2", "<====>\n")
|
805
|
+
subject << HRX::File.new("file 3", "<=====>\n")
|
806
|
+
expect(subject.to_hrx).to be == <<END
|
807
|
+
<======> file 1
|
808
|
+
<===>
|
809
|
+
|
810
|
+
<======> file 2
|
811
|
+
<====>
|
812
|
+
|
813
|
+
<======> file 3
|
814
|
+
<=====>
|
815
|
+
END
|
816
|
+
end
|
817
|
+
|
818
|
+
context "with an explicit boundary length" do
|
819
|
+
subject {HRX::Archive.new(boundary_length: 1)}
|
820
|
+
|
821
|
+
it "uses it if possible" do
|
822
|
+
subject << HRX::File.new("file", "contents\n")
|
823
|
+
expect(subject.to_hrx).to be == <<END
|
824
|
+
<=> file
|
825
|
+
contents
|
826
|
+
END
|
827
|
+
end
|
828
|
+
|
829
|
+
it "doesn't use it if it conflicts" do
|
830
|
+
subject << HRX::File.new("file", "<=>\n")
|
831
|
+
expect(subject.to_hrx).to be == <<END
|
832
|
+
<==> file
|
833
|
+
<=>
|
834
|
+
END
|
835
|
+
end
|
836
|
+
end
|
837
|
+
end
|
838
|
+
|
839
|
+
context "#write!" do
|
840
|
+
include_context "uses temp dir"
|
841
|
+
|
842
|
+
it "saves the archive to disk" do
|
843
|
+
subject << HRX::File.new("file", "file contents\n")
|
844
|
+
subject << HRX::File.new("super/sub", "sub contents\n")
|
845
|
+
subject.write!("#{temp_dir}/archive.hrx")
|
846
|
+
|
847
|
+
expect(File.read("#{temp_dir}/archive.hrx", mode: "rb")).to be == <<END
|
848
|
+
<===> file
|
849
|
+
file contents
|
850
|
+
|
851
|
+
<===> super/sub
|
852
|
+
sub contents
|
853
|
+
END
|
854
|
+
end
|
855
|
+
|
856
|
+
it "saves the archive as UTF-8" do
|
857
|
+
subject << HRX::File.new("👭", "")
|
858
|
+
subject.write!("#{temp_dir}/archive.hrx")
|
859
|
+
expect(File.read("#{temp_dir}/archive.hrx", mode: "rb")).to be == "<===> \xF0\x9F\x91\xAD\n".b
|
860
|
+
end
|
861
|
+
|
862
|
+
it "saves the archive as UTF-8 despite Encoding.default_external" do
|
863
|
+
with_external_encoding("iso-8859-1") do
|
864
|
+
subject << HRX::File.new("föö", "")
|
865
|
+
subject.write!("#{temp_dir}/archive.hrx")
|
866
|
+
expect(File.read("#{temp_dir}/archive.hrx", mode: "rb")).to(
|
867
|
+
be == "<===> f\xC3\xB6\xC3\xB6\n".b)
|
868
|
+
end
|
869
|
+
end
|
870
|
+
end
|
871
|
+
end
|