ucl 0.1.4 → 0.2.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.
data/test/test_ucl.rb CHANGED
@@ -129,6 +129,18 @@ class TestUCL < Minitest::Test
129
129
  UCL.parse("# header\nx = 1 # inline\ny = 2"))
130
130
  end
131
131
 
132
+ # Characterisation, not endorsement: in libucl a trailing comment suppresses
133
+ # suffix parsing, so a value carrying a unit or a multiplier comes back as a
134
+ # string. Plain numbers are unaffected (see above). The README documents this
135
+ # as a trap; if a future libucl fixes it, this test is the canary that says
136
+ # the documentation needs updating.
137
+ def test_trailing_comment_suppresses_suffix_parsing
138
+ assert_equal({ 't' => 30.0 }, UCL.parse('t = 30s'))
139
+ assert_equal({ 't' => '30s' }, UCL.parse('t = 30s # note'))
140
+ assert_equal({ 'n' => 10_485_760 }, UCL.parse('n = 10mb'))
141
+ assert_equal({ 'n' => '10mb' }, UCL.parse('n = 10mb # note'))
142
+ end
143
+
132
144
  # ---- duplicate keys become an explicit array ----------------------------
133
145
 
134
146
  def test_duplicate_keys_make_array
@@ -303,6 +315,109 @@ class TestUCL < Minitest::Test
303
315
  assert_raises(TypeError) { UCL.parse(42) }
304
316
  end
305
317
 
318
+ # ---- nesting limit ------------------------------------------------------
319
+
320
+ # The conversion to Ruby objects and libucl's own tree handling both recurse
321
+ # once per nesting level, so the depth is capped instead of being left to
322
+ # exhaust the C stack. The threads below are what make these meaningful: a
323
+ # thread gets a 1 MiB machine stack, which is the small stack the limit has
324
+ # to protect.
325
+
326
+ def test_deeply_nested_array_raises_ucl_error
327
+ err = Thread.new { assert_raises(UCL::Error) { UCL.parse(deep_array) } }.value
328
+ assert_match(/nesting/, err.message)
329
+ end
330
+
331
+ def test_deeply_nested_object_raises_ucl_error
332
+ src = ('k {' * 20_000) + ('}' * 20_000)
333
+ Thread.new { assert_raises(UCL::Error) { UCL.parse(src) } }.join
334
+ end
335
+
336
+ def test_deeply_nested_file_raises_ucl_error
337
+ with_conf(deep_array) do |path|
338
+ Thread.new { assert_raises(UCL::Error) { UCL.load_file(path) } }.join
339
+ end
340
+ end
341
+
342
+ def test_nesting_just_below_the_limit_is_accepted
343
+ src = 'a = ' + ('[' * 999) + (']' * 999)
344
+ assert_kind_of Array, UCL.parse(src)['a']
345
+ end
346
+
347
+ def test_parser_still_works_after_a_rejected_document
348
+ Thread.new { assert_raises(UCL::Error) { UCL.parse(deep_array) } }.join
349
+ assert_equal({ 'x' => 1 }, UCL.parse('x = 1'))
350
+ end
351
+
352
+ def test_rejected_document_does_not_leak_the_parser
353
+ # Regression: the conversion used to escape with the parser and the whole
354
+ # parsed tree still allocated, losing ~2.7 MiB per rejected 40 KiB input.
355
+ before = process_rss_kib
356
+ skip 'cannot read RSS on this platform' if before.nil?
357
+ 100.times do
358
+ UCL.parse(deep_array)
359
+ rescue UCL::Error
360
+ # expected; what is being measured is what the failure path leaves behind
361
+ end
362
+ GC.start
363
+ growth = process_rss_kib - before
364
+ # Pre-fix this grew by ~280 MiB; the margin is deliberately generous so
365
+ # that ordinary heap growth cannot make the test flaky.
366
+ assert_operator growth, :<, 20 * 1024,
367
+ "leaked #{growth} KiB over 100 rejected documents"
368
+ end
369
+
370
+ # ---- macro safety -------------------------------------------------------
371
+
372
+ def test_parse_processes_the_include_macro
373
+ with_conf("secret = value\n") do |path|
374
+ assert_equal({ 'secret' => 'value' }, UCL.parse(%(.include "#{path}")))
375
+ end
376
+ end
377
+
378
+ def test_safe_parse_does_not_read_the_included_file
379
+ with_conf("secret = value\n") do |path|
380
+ # libucl versions differ in how they treat a macro line when macros are
381
+ # disabled (ignored, or rejected as an invalid key); either is fine, as
382
+ # long as the file is not read.
383
+ begin
384
+ refute_includes UCL.safe_parse(%(.include "#{path}")), 'secret'
385
+ rescue UCL::Error => e
386
+ refute_empty e.message
387
+ end
388
+ end
389
+ end
390
+
391
+ def test_safe_parse_parses_ordinary_input
392
+ assert_equal({ 'a' => 1, 'b' => [2, 3] }, UCL.safe_parse("a = 1\nb = [2,3]"))
393
+ end
394
+
395
+ def test_safe_parse_honours_explicit_flags
396
+ assert_equal({ a: 1 },
397
+ UCL.safe_parse('A = 1', UCL::KEY_SYMBOL | UCL::KEY_LOWERCASE))
398
+ end
399
+
400
+ def test_safe_parse_uses_the_default_flags
401
+ UCL.flags = UCL::KEY_SYMBOL
402
+ assert_equal({ a: 1 }, UCL.safe_parse('a = 1'))
403
+ end
404
+
405
+ # ---- subclasses ---------------------------------------------------------
406
+
407
+ def test_subclass_uses_its_own_default_flags
408
+ klass = Class.new(UCL)
409
+ klass.flags = UCL::KEY_SYMBOL
410
+ assert_equal({ 'name' => 'value' }, UCL.parse('name = value'))
411
+ assert_equal({ name: 'value' }, klass.parse('name = value'))
412
+ end
413
+
414
+ def test_subclass_without_flags_falls_back_to_ucl
415
+ UCL.flags = UCL::KEY_SYMBOL
416
+ klass = Class.new(UCL)
417
+ assert_equal UCL::KEY_SYMBOL, klass.flags
418
+ assert_equal({ name: 'value' }, klass.parse('name = value'))
419
+ end
420
+
306
421
  # ---- constants ----------------------------------------------------------
307
422
 
308
423
  def test_constants_defined
@@ -313,6 +428,20 @@ class TestUCL < Minitest::Test
313
428
 
314
429
  private
315
430
 
431
+ # 20 000 levels: deeper than the 1000-level limit, and deep enough that the
432
+ # C stack would be gone without it.
433
+ def deep_array
434
+ 'a = ' + ('[' * 20_000) + (']' * 20_000)
435
+ end
436
+
437
+ # Resident set size in KiB, or nil where it cannot be read.
438
+ def process_rss_kib
439
+ out = `ps -o rss= -p #{$$} 2>/dev/null`
440
+ $?.success? && !out.strip.empty? ? out.to_i : nil
441
+ rescue StandardError
442
+ nil
443
+ end
444
+
316
445
  def with_conf(content)
317
446
  Tempfile.create(['ucl', '.conf']) do |f|
318
447
  f.write(content)
data/ucl.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'ucl'
3
- s.version = '0.1.4'
3
+ s.version = '0.2.0'
4
4
  s.summary = 'Universal Configuration Language (UCL) parser'
5
5
  s.description = <<~EOF
6
6
  Parse configuration files written in the Universal Configuration
@@ -14,14 +14,27 @@ Gem::Specification.new do |s|
14
14
  s.authors = [ "Stéphane D'Alu" ]
15
15
  s.email = [ 'sdalu@sdalu.com' ]
16
16
 
17
+ # The floor that has actually been exercised: 3.1 (Debian bookworm) and
18
+ # 3.4. Nothing here is known to need a newer Ruby.
19
+ s.required_ruby_version = '>= 3.1'
20
+
17
21
  s.extensions = [ 'ext/extconf.rb' ]
18
- s.files = %w[ ucl.gemspec README.md LICENSE ] +
19
- Dir['ext/**/*.{c,h,rb}'] +
20
- Dir['test/**/*.rb']
21
22
 
22
- # Used at build time to download and compile libucl from source when no
23
- # system-wide installation is found (requires cmake and a C compiler).
24
- s.add_dependency 'mini_portile2', '~> 2.8'
23
+ # ext/libucl is a git submodule holding the whole upstream tree, most of
24
+ # which (tests, utils, the Lua and Python bindings, docs) has no business
25
+ # in the gem -- hence an explicit list rather than an ext/**/* glob. It
26
+ # names what the embedded build compiles, plus the licence texts that
27
+ # redistributing those sources requires; see LICENSE-DEPENDENCIES.md.
28
+ # `rake build` refuses to run when the submodule is not populated, since
29
+ # these globs would silently come back empty.
30
+ s.files = %w[ ucl.gemspec README.md LICENSE LICENSE-DEPENDENCIES.md ] +
31
+ Dir['ext/*.{c,h,rb}'] +
32
+ Dir['test/**/*.rb'] +
33
+ %w[ ext/libucl/COPYING ] +
34
+ Dir['ext/libucl/include/ucl.h'] +
35
+ Dir['ext/libucl/src/*.{c,h}'] +
36
+ Dir['ext/libucl/uthash/*.h'] +
37
+ Dir['ext/libucl/klib/*.h']
25
38
 
26
39
  s.add_development_dependency 'rake'
27
40
  s.add_development_dependency 'minitest', '~> 5.0'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ucl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stéphane D'Alu
@@ -9,20 +9,6 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: mini_portile2
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '2.8'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '2.8'
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: rake
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -77,8 +63,30 @@ extensions:
77
63
  extra_rdoc_files: []
78
64
  files:
79
65
  - LICENSE
66
+ - LICENSE-DEPENDENCIES.md
80
67
  - README.md
81
68
  - ext/extconf.rb
69
+ - ext/libucl/COPYING
70
+ - ext/libucl/include/ucl.h
71
+ - ext/libucl/klib/khash.h
72
+ - ext/libucl/klib/kvec.h
73
+ - ext/libucl/src/mum.h
74
+ - ext/libucl/src/tree.h
75
+ - ext/libucl/src/ucl_chartable.h
76
+ - ext/libucl/src/ucl_emitter.c
77
+ - ext/libucl/src/ucl_emitter_streamline.c
78
+ - ext/libucl/src/ucl_emitter_utils.c
79
+ - ext/libucl/src/ucl_hash.c
80
+ - ext/libucl/src/ucl_hash.h
81
+ - ext/libucl/src/ucl_internal.h
82
+ - ext/libucl/src/ucl_msgpack.c
83
+ - ext/libucl/src/ucl_parser.c
84
+ - ext/libucl/src/ucl_schema.c
85
+ - ext/libucl/src/ucl_sexp.c
86
+ - ext/libucl/src/ucl_util.c
87
+ - ext/libucl/uthash/uthash.h
88
+ - ext/libucl/uthash/utlist.h
89
+ - ext/libucl/uthash/utstring.h
82
90
  - ext/ucl.c
83
91
  - test/test_ucl.rb
84
92
  - ucl.gemspec
@@ -93,14 +101,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
93
101
  requirements:
94
102
  - - ">="
95
103
  - !ruby/object:Gem::Version
96
- version: '0'
104
+ version: '3.1'
97
105
  required_rubygems_version: !ruby/object:Gem::Requirement
98
106
  requirements:
99
107
  - - ">="
100
108
  - !ruby/object:Gem::Version
101
109
  version: '0'
102
110
  requirements: []
103
- rubygems_version: 4.0.9
111
+ rubygems_version: 4.0.16
104
112
  specification_version: 4
105
113
  summary: Universal Configuration Language (UCL) parser
106
114
  test_files: []