hrx 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|