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
data/spec/parse_spec.rb
ADDED
@@ -0,0 +1,346 @@
|
|
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 'hrx'
|
17
|
+
|
18
|
+
require_relative 'validates_path'
|
19
|
+
|
20
|
+
RSpec.describe HRX, ".parse" do
|
21
|
+
it "parses an empty file" do
|
22
|
+
expect(HRX::Archive.parse("").entries).to be_empty
|
23
|
+
end
|
24
|
+
|
25
|
+
it "converts the file to UTF-8" do
|
26
|
+
hrx = HRX::Archive.parse("<===> いか\n".encode("SJIS"))
|
27
|
+
expect(hrx.entries.first.path).to be == "いか"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "requires the file to be convetible to UTF-8" do
|
31
|
+
expect do
|
32
|
+
HRX::Archive.parse("<===> \xc3\x28\n".b)
|
33
|
+
end.to raise_error(EncodingError)
|
34
|
+
end
|
35
|
+
|
36
|
+
context "with a single file" do
|
37
|
+
subject {HRX::Archive.parse(<<END)}
|
38
|
+
<===> file
|
39
|
+
contents
|
40
|
+
END
|
41
|
+
|
42
|
+
it "parses one entry" do
|
43
|
+
expect(subject.entries.length).to be == 1
|
44
|
+
end
|
45
|
+
|
46
|
+
it "parses the filename" do
|
47
|
+
expect(subject.entries.first.path).to be == "file"
|
48
|
+
end
|
49
|
+
|
50
|
+
it "parses the contents" do
|
51
|
+
expect(subject.entries.first.content).to be == "contents\n"
|
52
|
+
end
|
53
|
+
|
54
|
+
it "parses contents without a newline" do
|
55
|
+
hrx = HRX::Archive.parse("<===> file\ncontents")
|
56
|
+
expect(hrx.entries.first.content).to be == "contents"
|
57
|
+
end
|
58
|
+
|
59
|
+
it "parses contents with boundary-like sequences" do
|
60
|
+
hrx = HRX::Archive.parse(<<END)
|
61
|
+
<===> file
|
62
|
+
<==>
|
63
|
+
inline <===>
|
64
|
+
<====>
|
65
|
+
END
|
66
|
+
expect(hrx.entries.first.content).to be == <<END
|
67
|
+
<==>
|
68
|
+
inline <===>
|
69
|
+
<====>
|
70
|
+
END
|
71
|
+
end
|
72
|
+
|
73
|
+
context "with a comment" do
|
74
|
+
subject {HRX::Archive.parse(<<END)}
|
75
|
+
<===>
|
76
|
+
comment
|
77
|
+
<===> file
|
78
|
+
contents
|
79
|
+
END
|
80
|
+
|
81
|
+
it "parses one entry" do
|
82
|
+
expect(subject.entries.length).to be == 1
|
83
|
+
end
|
84
|
+
|
85
|
+
it "parses the filename" do
|
86
|
+
expect(subject.entries.first.path).to be == "file"
|
87
|
+
end
|
88
|
+
|
89
|
+
it "parses the contents" do
|
90
|
+
expect(subject.entries.first.content).to be == "contents\n"
|
91
|
+
end
|
92
|
+
|
93
|
+
it "parses the comment" do
|
94
|
+
expect(subject.entries.first.comment).to be == "comment"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context "with multiple files" do
|
100
|
+
subject {HRX::Archive.parse(<<END)}
|
101
|
+
<===> file 1
|
102
|
+
contents 1
|
103
|
+
|
104
|
+
<===> file 2
|
105
|
+
contents 2
|
106
|
+
END
|
107
|
+
|
108
|
+
it "parses two entries" do
|
109
|
+
expect(subject.entries.length).to be == 2
|
110
|
+
end
|
111
|
+
|
112
|
+
it "parses the first filename" do
|
113
|
+
expect(subject.entries.first.path).to be == "file 1"
|
114
|
+
end
|
115
|
+
|
116
|
+
it "parses the first contents" do
|
117
|
+
expect(subject.entries.first.content).to be == "contents 1\n"
|
118
|
+
end
|
119
|
+
|
120
|
+
it "parses the second filename" do
|
121
|
+
expect(subject.entries.last.path).to be == "file 2"
|
122
|
+
end
|
123
|
+
|
124
|
+
it "parses the second contents" do
|
125
|
+
expect(subject.entries.last.content).to be == "contents 2\n"
|
126
|
+
end
|
127
|
+
|
128
|
+
it "allows an explicit parent directory" do
|
129
|
+
hrx = HRX::Archive.parse(<<END)
|
130
|
+
<===> dir/
|
131
|
+
<===> dir/file
|
132
|
+
contents
|
133
|
+
END
|
134
|
+
|
135
|
+
expect(hrx.entries.last.content).to be == "contents\n"
|
136
|
+
end
|
137
|
+
|
138
|
+
it "parses contents without a newline" do
|
139
|
+
hrx = HRX::Archive.parse(<<END)
|
140
|
+
<===> file 1
|
141
|
+
contents 1
|
142
|
+
<===> file 2
|
143
|
+
contents 2
|
144
|
+
END
|
145
|
+
expect(hrx.entries.first.content).to be == "contents 1"
|
146
|
+
end
|
147
|
+
|
148
|
+
it "parses contents with boundary-like sequences" do
|
149
|
+
hrx = HRX::Archive.parse(<<END)
|
150
|
+
<===> file 1
|
151
|
+
<==>
|
152
|
+
inline <===>
|
153
|
+
<====>
|
154
|
+
|
155
|
+
<===> file 2
|
156
|
+
contents
|
157
|
+
END
|
158
|
+
expect(hrx.entries.first.content).to be == <<END
|
159
|
+
<==>
|
160
|
+
inline <===>
|
161
|
+
<====>
|
162
|
+
END
|
163
|
+
end
|
164
|
+
|
165
|
+
context "with a comment" do
|
166
|
+
subject {HRX::Archive.parse(<<END)}
|
167
|
+
<===> file 1
|
168
|
+
contents 1
|
169
|
+
|
170
|
+
<===>
|
171
|
+
comment
|
172
|
+
<===> file 2
|
173
|
+
contents 2
|
174
|
+
END
|
175
|
+
|
176
|
+
it "parses two entries" do
|
177
|
+
expect(subject.entries.length).to be == 2
|
178
|
+
end
|
179
|
+
|
180
|
+
it "parses the first filename" do
|
181
|
+
expect(subject.entries.first.path).to be == "file 1"
|
182
|
+
end
|
183
|
+
|
184
|
+
it "parses the first contents" do
|
185
|
+
expect(subject.entries.first.content).to be == "contents 1\n"
|
186
|
+
end
|
187
|
+
|
188
|
+
it "parses the second filename" do
|
189
|
+
expect(subject.entries.last.path).to be == "file 2"
|
190
|
+
end
|
191
|
+
|
192
|
+
it "parses the second contents" do
|
193
|
+
expect(subject.entries.last.content).to be == "contents 2\n"
|
194
|
+
end
|
195
|
+
|
196
|
+
it "parses the comment" do
|
197
|
+
expect(subject.entries.last.comment).to be == "comment"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
it "parses a file that only contains a comment" do
|
203
|
+
expect(HRX::Archive.parse(<<END).last_comment).to be == "contents\n"
|
204
|
+
<===>
|
205
|
+
contents
|
206
|
+
END
|
207
|
+
end
|
208
|
+
|
209
|
+
it "parses a file that only contains a comment with boundary-like sequences" do
|
210
|
+
expect(HRX::Archive.parse(<<HRX).last_comment).to be == <<CONTENTS
|
211
|
+
<===>
|
212
|
+
<==>
|
213
|
+
inline <===>
|
214
|
+
<====>
|
215
|
+
HRX
|
216
|
+
<==>
|
217
|
+
inline <===>
|
218
|
+
<====>
|
219
|
+
CONTENTS
|
220
|
+
end
|
221
|
+
|
222
|
+
context "with a file and a trailing comment" do
|
223
|
+
subject {HRX::Archive.parse(<<END)}
|
224
|
+
<===> file
|
225
|
+
contents
|
226
|
+
|
227
|
+
<===>
|
228
|
+
comment
|
229
|
+
END
|
230
|
+
|
231
|
+
it "parses one entry" do
|
232
|
+
expect(subject.entries.length).to be == 1
|
233
|
+
end
|
234
|
+
|
235
|
+
it "parses the filename" do
|
236
|
+
expect(subject.entries.first.path).to be == "file"
|
237
|
+
end
|
238
|
+
|
239
|
+
it "parses the contents" do
|
240
|
+
expect(subject.entries.first.content).to be == "contents\n"
|
241
|
+
end
|
242
|
+
|
243
|
+
it "parses the trailing comment" do
|
244
|
+
expect(subject.last_comment).to be == "comment\n"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
context "with a single directory" do
|
249
|
+
subject {HRX::Archive.parse("<===> dir/\n")}
|
250
|
+
|
251
|
+
it "parses one entry" do
|
252
|
+
expect(subject.entries.length).to be == 1
|
253
|
+
end
|
254
|
+
|
255
|
+
it "parses a directory" do
|
256
|
+
expect(subject.entries.first).to be_a(HRX::Directory)
|
257
|
+
end
|
258
|
+
|
259
|
+
it "parses the filename" do
|
260
|
+
expect(subject.entries.first.path).to be == "dir/"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
it "serializes in source order" do
|
265
|
+
hrx = HRX::Archive.parse(<<END)
|
266
|
+
<===> foo
|
267
|
+
<===> dir/bar
|
268
|
+
<===> baz
|
269
|
+
<===> dir/qux
|
270
|
+
END
|
271
|
+
|
272
|
+
expect(hrx.to_hrx).to be == <<END
|
273
|
+
<===> foo
|
274
|
+
<===> dir/bar
|
275
|
+
<===> baz
|
276
|
+
<===> dir/qux
|
277
|
+
END
|
278
|
+
end
|
279
|
+
|
280
|
+
it "serializes with the source boundary" do
|
281
|
+
hrx = HRX::Archive.parse("<=> file\n")
|
282
|
+
expect(hrx.to_hrx).to be == "<=> file\n"
|
283
|
+
end
|
284
|
+
|
285
|
+
let(:constructor) {lambda {|path| HRX::Archive.parse("<===> #{path}\n")}}
|
286
|
+
include_examples "validates paths"
|
287
|
+
|
288
|
+
context "forbids an HRX file that" do
|
289
|
+
# Specifies that the given HRX archive, with the given human-readable
|
290
|
+
# description, can't be parsed.
|
291
|
+
def self.that(description, text, message)
|
292
|
+
it description do
|
293
|
+
expect {HRX::Archive.parse(text)}.to raise_error(HRX::ParseError, message)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
that "doesn't start with a boundary", "file\n", /Expected boundary/
|
298
|
+
that "starts with an unclosed boundary", "<== file\n", /Expected boundary/
|
299
|
+
that "starts with an unopened boundary", "==> file\n", /Expected boundary/
|
300
|
+
that "starts with a malformed boundary", "<> file\n", /Expected boundary/
|
301
|
+
that "has a directory with contents", "<===> dir/\ncontents", /Expected boundary/
|
302
|
+
|
303
|
+
that "has duplicate files", "<=> file\n<=> file\n", /"file" defined twice/
|
304
|
+
that "has duplicate directories", "<=> dir/\n<=> dir/\n", %r{"dir/" defined twice}
|
305
|
+
that "has file with the same name as a directory", "<=> foo/\n<=> foo\n", /"foo" defined twice/
|
306
|
+
that "has file with the same name as an earlier implicit directory", "<=> foo/bar\n<=> foo\n", /"foo" defined twice/
|
307
|
+
that "has file with the same name as a later implicit directory", "<=> foo\n<=> foo/bar\n", /"foo" defined twice/
|
308
|
+
|
309
|
+
context "has a boundary that" do
|
310
|
+
that "isn't followed by a space", "<=>file\n", /Expected space/
|
311
|
+
that "isn't followed by a path", "<=> \n", /Expected a path/
|
312
|
+
that "has a file without a newline", "<=> file", /Expected newline/
|
313
|
+
end
|
314
|
+
|
315
|
+
context "has a middle boundary that" do
|
316
|
+
that "isn't followed by a space", "<=> file 1\n<=>file 2\n", /Expected space/
|
317
|
+
that "isn't followed by a path", "<=> file 1\n<=> \n", /Expected a path/
|
318
|
+
that "has a file without a newline", "<=> file 1\n<=> file", /Expected newline/
|
319
|
+
end
|
320
|
+
|
321
|
+
context "has multiple comments that" do
|
322
|
+
that "come before a file", <<END, /Expected space/
|
323
|
+
<=>
|
324
|
+
comment 1
|
325
|
+
<=>
|
326
|
+
comment 2
|
327
|
+
<=> file
|
328
|
+
END
|
329
|
+
|
330
|
+
that "come after a file", <<END, /Expected space/
|
331
|
+
<=> file
|
332
|
+
<=>
|
333
|
+
comment 1
|
334
|
+
<=>
|
335
|
+
comment 2
|
336
|
+
END
|
337
|
+
|
338
|
+
that "appear on their own", <<END, /Expected space/
|
339
|
+
<=>
|
340
|
+
comment 1
|
341
|
+
<=>
|
342
|
+
comment 2
|
343
|
+
END
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,95 @@
|
|
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 'stringio'
|
16
|
+
|
17
|
+
module Helpers
|
18
|
+
# Runs a block with warnings disabled.
|
19
|
+
def silence_warnings
|
20
|
+
old_stderr = $stderr
|
21
|
+
$stderr = StringIO.new
|
22
|
+
yield
|
23
|
+
ensure
|
24
|
+
$stderr = old_stderr if old_stderr
|
25
|
+
end
|
26
|
+
|
27
|
+
# Runs a block with `encoding` as Encoding.default_external.
|
28
|
+
def with_external_encoding(encoding)
|
29
|
+
old_encoding = Encoding.default_external
|
30
|
+
silence_warnings {Encoding.default_external = "iso-8859-1"}
|
31
|
+
ensure
|
32
|
+
silence_warnings {Encoding.default_external = old_encoding if old_encoding}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
RSpec.configure do |config|
|
37
|
+
config.include Helpers
|
38
|
+
|
39
|
+
config.expect_with :rspec do |expectations|
|
40
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
41
|
+
# and `failure_message` of custom matchers include text for helper methods
|
42
|
+
# defined using `chain`, e.g.:
|
43
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
44
|
+
# # => "be bigger than 2 and smaller than 4"
|
45
|
+
# ...rather than:
|
46
|
+
# # => "be bigger than 2"
|
47
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
48
|
+
end
|
49
|
+
|
50
|
+
config.mock_with :rspec do |mocks|
|
51
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
52
|
+
# a real object. This is generally recommended, and will default to
|
53
|
+
# `true` in RSpec 4.
|
54
|
+
mocks.verify_partial_doubles = true
|
55
|
+
end
|
56
|
+
|
57
|
+
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
58
|
+
# have no way to turn it off -- the option exists only for backwards
|
59
|
+
# compatibility in RSpec 3). It causes shared context metadata to be
|
60
|
+
# inherited by the metadata hash of host groups and examples, rather than
|
61
|
+
# triggering implicit auto-inclusion in groups with matching metadata.
|
62
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
63
|
+
|
64
|
+
# Allows RSpec to persist some state between runs in order to support
|
65
|
+
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
66
|
+
# you configure your source control system to ignore this file.
|
67
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
68
|
+
|
69
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
70
|
+
# recommended. For more details, see:
|
71
|
+
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
72
|
+
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
73
|
+
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
74
|
+
config.disable_monkey_patching!
|
75
|
+
|
76
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
77
|
+
# be too noisy due to issues in dependencies.
|
78
|
+
config.warnings = true
|
79
|
+
|
80
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
81
|
+
# file, and it's useful to allow more verbose output when running an
|
82
|
+
# individual spec file.
|
83
|
+
if config.files_to_run.one?
|
84
|
+
# Use the documentation formatter for detailed output,
|
85
|
+
# unless a formatter has already been configured
|
86
|
+
# (e.g. via a command-line flag).
|
87
|
+
config.default_formatter = "doc"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
91
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
92
|
+
# test failures related to randomization by passing the same `--seed` value
|
93
|
+
# as the one that triggered the failure.
|
94
|
+
Kernel.srand config.seed
|
95
|
+
end
|
@@ -0,0 +1,49 @@
|
|
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
|
+
RSpec.shared_examples "validates paths" do
|
17
|
+
# Specifies that the given `path`, described by `description`, is allowed.
|
18
|
+
def self.allows_a_path_that(description, path)
|
19
|
+
it "allows a path that #{description}" do
|
20
|
+
expect { constructor[path] }.not_to raise_error
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Specifies that the given `path`, described by `description`, is not allowed.
|
25
|
+
def self.forbids_a_path_that(description, path)
|
26
|
+
it "forbids a path that #{description}" do
|
27
|
+
expect { constructor[path] }.to raise_error(HRX::ParseError)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
allows_a_path_that "contains one component", "foo"
|
32
|
+
allows_a_path_that "contains multiple components", "foo/bar/baz"
|
33
|
+
allows_a_path_that "contains three dots", "..."
|
34
|
+
allows_a_path_that "starts with a dot", ".foo"
|
35
|
+
allows_a_path_that "contains non-alphanumeric characters", '~`!@#$%^&*()_-+= {}[]|;"\'<,>.?'
|
36
|
+
allows_a_path_that "contains non-ASCII characters", "☃"
|
37
|
+
|
38
|
+
forbids_a_path_that "is empty", ""
|
39
|
+
forbids_a_path_that 'is "."', "."
|
40
|
+
forbids_a_path_that 'is ".."', ".."
|
41
|
+
forbids_a_path_that "is only a separator", "/"
|
42
|
+
forbids_a_path_that "begins with a separator", "/foo"
|
43
|
+
forbids_a_path_that "contains multiple separators in a row", "foo//bar"
|
44
|
+
forbids_a_path_that "contains an invalid component", "foo/../bar"
|
45
|
+
|
46
|
+
[*0x00..0x09, *0x0B..0x1F, 0x3A, 0x5C, 0x7F].each do |c|
|
47
|
+
forbids_a_path_that "contains U+00#{c.to_s(16).rjust(2, "0")}", "fo#{c.chr}o"
|
48
|
+
end
|
49
|
+
end
|