tomlrb 2.0.4 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79334a64c6114d7a83fd6446645fa3f83f5995fc92bb6e51c12579ea20846c8a
4
- data.tar.gz: 007e015700fe6a9e739ba29d3840125c1ab859ed9039329843444cf5f4a735a1
3
+ metadata.gz: e6628b6e0662be0698c44c3dab186115a3424bfd9fe94dde4cb7c6e308d079ce
4
+ data.tar.gz: 81f84bf3dd98784ce68b7b6b7945461e381edb7234e84c86778f7d9eef2d8fab
5
5
  SHA512:
6
- metadata.gz: 6b5090126db023bb2182a4dfd724d56c8f6cf9db33f775de2dedb14aaf4dca5844a4b00211bda3c8cecf68296e778dbe18be4e68486f112113f4dd2a4720df05
7
- data.tar.gz: 5816f7c67269a251e71c37a4202f549b0356dcbfd5746ead8203638be1bbc9984a5847b167c225582ada7025f62d51bde71fce5742fa20d6f3762a1d7393730f
6
+ metadata.gz: 5bcefe0976b4840098240fd353784c886d58c1e1711a3130a8946751e85a670e170dceecd38ddb2557f06dd2d21ad1e96927abe0f52d1b17c855db6b1576ea29
7
+ data.tar.gz: bcdfd0a6f4638667e06d4228681b9cc764f849a16fb111125e3423910a00576f52e50c6c8749e0250b530d28ca4983e8e1eb0c9c2b513bf99da70123f6b0eaab
data/CHANGELOG.md ADDED
@@ -0,0 +1,103 @@
1
+ ### 3.0.0 - 2026-10-15
2
+
3
+ Breaking changes:
4
+
5
+ * Require Ruby 3.3 or later
6
+ * Read documents as TOML 1.1 by default. Every valid TOML 1.0 document parses unchanged; 1.1 additionally accepts `\e` and `\xHH` escapes, times without seconds, and inline tables that span lines, contain comments or end with a comma. `version: "1.0"` keeps rejecting those
7
+ * Fractional seconds are exact `Rational`s instead of `Float`s: `Time`, `LocalDateTime` and `LocalTime` values carry `56.123`, not `56.122999…`
8
+ * Remove `Tomlrb::Scanner`, `Tomlrb::Handler`, `Tomlrb::StringUtils`, `Tomlrb::GeneratedParser`, `Tomlrb::Keys` and `Tomlrb::Key`; `Tomlrb::Parser.new` now takes the document string. Key redefinitions raise `Tomlrb::ParseError` as before
9
+ * `require "tomlrb"` no longer loads `time` and `stringio`
10
+
11
+ Features:
12
+
13
+ * TOML 1.1 support, selected with `version: "1.1"` (default) or `"1.0"`
14
+
15
+ Security:
16
+
17
+ * Nesting depth of arrays and inline tables is limited only by memory: they are parsed with an explicit stack
18
+
19
+ Performance:
20
+
21
+ * Replace the Racc grammar with a hand-written single-pass parser; Racc is no longer used. Parsing executes 2.2-2.5x fewer instructions than perfect_toml 0.9.1 and 12-14x fewer than 2.0.4 (2.6-2.9x and 14-16x with YJIT)
22
+ * Multi-line strings are scanned by searching for the closing quotes, a backslash or a control character: a 1 MB `"""` or `'''` string parses 25x faster, peak memory no longer grows by about 48 MB per MB of string, and quote-dense content (`ab"cd""ef`) no longer costs a step per quote
23
+
24
+ Conformance (every TOML 1.0 and 1.1 case of toml-test 2.2.0 passes):
25
+
26
+ * An escaped backslash right before a closing `"""` (`"""x\\"""`) ends the string
27
+ * Reject a carriage return not followed by a line feed in multi-line strings, and trim a CRLF (not just LF) right after an opening `"""` or `'''`
28
+ * Parse bare keys that start with `true`, `false`, `inf` or `nan` (`trueval`, `inf-x`) or look like dates and numbers (`1979-05-27a`, `0x1f`, `[[1e5]]`), and reject keys containing `:`
29
+ * Reject empty values in inline tables (`{a = }`), inline tables that are not assigned to a key, and dotted keys that extend an inline table
30
+ * Reject spaced array-of-tables brackets (`[ [a]]`, `[[a] ]`) on indented and CRLF lines too, and stop rejecting multi-line arrays whose line starts with `[ [`
31
+ * Separate date and time only with `T`, `t` or a space, accept an offset only after a time, and reject negative years and days that do not exist (`1979-06-31T00:00:00-00:00`)
32
+ * Reject floats with leading zeros (`01e5`) and accept underscores in exponents (`1e1_0`)
33
+
34
+ Other:
35
+
36
+ * Parse errors report the line and column (in characters)
37
+ * `benchmarks/matrix.rb` measures time, allocations and peak memory over a set of generated workloads; `benchmarks/bench.rb` accepts several files
38
+ * `LocalDate`, `LocalDateTime` and `LocalTime` validate their fields arithmetically and build their `Time` on first use
39
+ * Ship RBS type signatures in `sig/`
40
+
41
+ ### 2.0.5 - 2026-10-15
42
+
43
+ * Fix exponential backtracking (ReDoS) when scanning unterminated basic and literal strings ([GHSA-gq8g-3x53-v8fh](https://github.com/fbernier/tomlrb/security/advisories/GHSA-gq8g-3x53-v8fh))
44
+ * Fix `SystemStackError` and quadratic scan time on long runs of spaces/tabs ([GHSA-mc56-vpwf-3m26](https://github.com/fbernier/tomlrb/security/advisories/GHSA-mc56-vpwf-3m26))
45
+ * Literal strings no longer treat `\'` as an escaped quote, per the TOML spec
46
+ * Add SECURITY.md: report vulnerabilities through GitHub private vulnerability reporting
47
+ * Declare racc as a runtime dependency: Ruby 3.3 made it a bundled gem, so under Bundler `require "tomlrb"` failed unless the app's Gemfile pulled racc in
48
+ * Stop shipping the unused `parser.y` grammar source in the gem
49
+
50
+ Both vulnerabilities were reported by [@tsigouris007](https://github.com/tsigouris007), advised by [@kpatsakis](https://github.com/kpatsakis).
51
+
52
+ ### 2.0.4 - 2025-12-18
53
+
54
+ * Update Racc to v1.8.1
55
+ * Code cleanup
56
+ * Fix conformance to TOML v1.0.0
57
+
58
+ ### 2.0.3 - 2022-05-28
59
+
60
+ * Enable symbolize_keys: true for inline tables
61
+
62
+ ### 2.0.2 - 2022-05-25
63
+
64
+ * Conform to TOML v1.0.0
65
+
66
+ ### 2.0.1 - 2020-12-20
67
+
68
+ * Fix error with table nested under array-of-tables
69
+ * Some performance improvements
70
+
71
+ ### 2.0.0 - 2020-11-26
72
+
73
+ * Conform to TOML v1.0.0-rc3
74
+
75
+ #### Breanking Changes ####
76
+
77
+ TOML v0.5.0 introduced new value types: Local Date-Time, Local Date and Local Time which represent time without time zone information. Tomlrb also introduced corresponding classes starting from v2.0.0. By this change, some table values such as `2020-11-24T20:32:18`, `2020-11-24` or `20:32:18` are not treated as strings but as new the classes `Tomlrb::LocalDateTime`, `Tomlrb::LocalDate` and `Tomlrb::LocalTime` respectively. You can get the string values by calling `#to_s` method on any of those classes. Additionally, You can also get `Time` objects with the `#to_time` method. See [API documentation](https://www.rubydoc.info/gems/tomlrb) for the methods' details.
78
+
79
+ ### 1.3.0 - 2020-03-19
80
+
81
+ * Fix error with falsy table values
82
+
83
+ ### 1.2.9 - 2019-11-22
84
+
85
+ * Fixes and cleanups for ruby 2.7
86
+
87
+ ### 1.2.8 - 2018-12-18
88
+
89
+ * Reduce gem size by excluding tests (tas50)
90
+ * Make integer and float parsing closer to the spec (sgarciac)
91
+
92
+ ### 1.2.7 - 2018-07-12
93
+
94
+ * Datetime should be UTC when no offset or timezone are specified
95
+
96
+ ### 1.2.6 - 2017-10-23
97
+
98
+ * Fix issue where an unclosed table could make the parsing loop infinitely.
99
+ * Proper string escaping
100
+
101
+ ### 1.1.3 - 2015-11-24
102
+
103
+ * Bare integers can be used as keys as per the spec
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # Tomlrb
2
+
3
+ [![CI](https://github.com/fbernier/tomlrb/actions/workflows/ci.yml/badge.svg)](https://github.com/fbernier/tomlrb/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/tomlrb)](https://rubygems.org/gems/tomlrb)
5
+
6
+ A fast, pure-Ruby [TOML](https://toml.io) parser with no runtime dependencies, for [TOML 1.1](https://toml.io/en/v1.1.0) and [1.0](https://toml.io/en/v1.0.0). It passes every case of the official [toml-test](https://github.com/toml-lang/toml-test) suite for both versions.
7
+
8
+ Requires Ruby 3.3 or later. Runs on CRuby (with or without YJIT), JRuby and TruffleRuby.
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ bundle add tomlrb
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```ruby
19
+ require "tomlrb"
20
+
21
+ Tomlrb.parse(%(title = "TOML"\n[owner]\nname = "Tom"))
22
+ # => {"title" => "TOML", "owner" => {"name" => "Tom"}}
23
+
24
+ Tomlrb.load_file("config.toml", symbolize_keys: true)
25
+ # => {title: "TOML", owner: {name: "Tom"}}
26
+ ```
27
+
28
+ `Tomlrb.parse` also accepts an IO. Invalid documents raise `Tomlrb::ParseError` with the line and column; impossible dates and times raise `ArgumentError`.
29
+
30
+ Documents are read as TOML 1.1, which accepts every valid TOML 1.0 document unchanged. Pass `version: "1.0"` to also reject what 1.1 added (`\e` and `\xHH` escapes, times without seconds, inline tables spanning lines or ending with a comma), for example when a file must stay readable by a 1.0-only parser:
31
+
32
+ ```ruby
33
+ Tomlrb.parse("t = 07:32") # => {"t" => #<Tomlrb::LocalTime: 07:32:00>}
34
+ Tomlrb.parse("t = 07:32", version: "1.0") # raises Tomlrb::ParseError
35
+ ```
36
+
37
+ TOML values map to Ruby as follows:
38
+
39
+ | TOML | Ruby |
40
+ |---|---|
41
+ | string | `String` |
42
+ | integer | `Integer` |
43
+ | float | `Float` (`inf`, `nan` included) |
44
+ | boolean | `true`, `false` |
45
+ | offset date-time | `Time` |
46
+ | local date-time | `Tomlrb::LocalDateTime` |
47
+ | local date | `Tomlrb::LocalDate` |
48
+ | local time | `Tomlrb::LocalTime` |
49
+ | array | `Array` |
50
+ | table, inline table | `Hash` |
51
+
52
+ Fractional seconds are exact (`Rational`), and the local types convert with `#to_time`.
53
+
54
+ Type signatures for the public API are in [`sig/`](sig/tomlrb.rbs).
55
+
56
+ ## Benchmark
57
+
58
+ `ruby benchmarks/bench.rb [file...]` compares against [toml-rb](https://github.com/emancu/toml-rb) and [perfect_toml](https://github.com/mame/perfect_toml) when they are installed. Each iteration parses every given file (`benchmarks/example.toml` by default, a 4.6 KB TOML 1.0 document using every value type and table form, which all three parsers accept).
59
+
60
+ `ruby [--yjit] benchmarks/matrix.rb` runs one parser over generated workloads, one per input shape (plain, escaped and 1 MB strings, plain and quote-dense multi-line strings, signed and unsigned numbers, floats, inline tables, date-times, many small documents), and reports time, Ruby allocations, peak RSS growth and a digest of the results per workload. `--lib DIR` loads tomlrb from another checkout, so equal digests show that two versions parse alike; `--parser perfect_toml` measures perfect_toml instead. Inputs keep the known encoding code range a string from `File.read` has; `--unknown-coderange` also times the UTF-8 validation of a string whose code range Ruby has not computed.
61
+
62
+ CPU instructions per parse (fewer is better), measured with `perf stat` on CRuby 3.3 without YJIT. With YJIT tomlrb's lead grows: 2.6-2.9x fewer instructions than perfect_toml and 14-16x fewer than 2.0.4. The crates.io corpus is the first 300 `Cargo.toml` files of a local Cargo registry (`~/.cargo/registry/src/*/*/Cargo.toml`, sorted by path) that all three parsers accept.
63
+
64
+ | Input | tomlrb | perfect_toml 0.9.1 | tomlrb 2.0.4 |
65
+ |---|---|---|---|
66
+ | `benchmarks/example.toml` (4.6 KB) | 3.0 M | 6.6 M | 34.1 M |
67
+ | 300 crates.io `Cargo.toml` files (622 KB) | 243 M | 559 M | 3,100 M |
68
+ | rust-lang/rust `Cargo.lock` at `56df459` (158 KB) | 48 M | 121 M | 687 M |
69
+
70
+ `ruby benchmarks/profile.rb [file]` records a [Vernier](https://github.com/jhawthorn/vernier) profile.
71
+
72
+ ## Development
73
+
74
+ ```sh
75
+ bin/setup # fetch the toml-test submodule and install gems
76
+ bundle exec rake # tests, Standard (lint), RBS validation
77
+ bundle exec rake toml_test # the official toml-test runner (needs Go)
78
+ bin/console # IRB with tomlrb loaded
79
+ ```
80
+
81
+ Releases are published from GitHub Actions with [trusted publishing](https://guides.rubygems.org/trusted-publishing/): bump `lib/tomlrb/version.rb`, add the CHANGELOG entry, then run the `release` workflow.
82
+
83
+ ## Thanks
84
+
85
+ Thanks to [@jpbougie](https://github.com/jpbougie) for the crash course on the Chomsky hierarchy and general tips.
@@ -1,32 +1,43 @@
1
- # frozen-string-literal: true
2
-
3
- require 'forwardable'
1
+ # frozen_string_literal: true
4
2
 
5
3
  module Tomlrb
4
+ # A TOML local date (no time, no offset), e.g. `1979-05-27`.
6
5
  class LocalDate
7
- extend Forwardable
6
+ DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31].freeze
7
+
8
+ # @return [Boolean] whether year, month and day (Integers) name a real calendar day
9
+ def self.valid_date?(year, month, day)
10
+ return false unless month.between?(1, 12) && day >= 1
8
11
 
9
- def_delegators :@time, :year, :month, :day
12
+ day <= DAYS_IN_MONTH[month] ||
13
+ (month == 2 && day == 29 && year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))
14
+ end
10
15
 
16
+ attr_reader :year, :month, :day
17
+
18
+ # The Time behind #to_time is built on first use: parsing creates many of these
19
+ # and building a Time costs more than validating the fields.
11
20
  def initialize(year, month, day)
12
- @time = Time.utc(year, month, day, 0, 0, 0)
13
- raise ArgumentError, "Invalid Local Date: #{year}-#{month}-#{day}" unless day.to_i == @time.day && month.to_i == @time.month && year.to_i == @time.year
21
+ @year = year.to_i
22
+ @month = month.to_i
23
+ @day = day.to_i
24
+ raise ArgumentError, "Invalid Local Date: #{self}" unless LocalDate.valid_date?(@year, @month, @day)
14
25
  end
15
26
 
16
27
  # @param offset see {LocalDateTime#to_time}
17
28
  # @return [Time] 00:00:00 of the date
18
- def to_time(offset = '-00:00')
19
- return @time if offset == '-00:00'
20
- Time.new(year, month, day, 0, 0, 0, offset)
29
+ def to_time(offset = "-00:00")
30
+ return @time ||= Time.utc(@year, @month, @day, 0, 0, 0) if offset == "-00:00"
31
+
32
+ Time.new(@year, @month, @day, 0, 0, 0, offset)
21
33
  end
22
34
 
23
35
  def to_s
24
- @time.strftime('%F')
36
+ format("%04d-%02d-%02d", @year, @month, @day)
25
37
  end
26
38
 
27
39
  def ==(other)
28
- other.is_a?(self.class) &&
29
- to_time == other.to_time
40
+ other.is_a?(self.class) && @year == other.year && @month == other.month && @day == other.day
30
41
  end
31
42
 
32
43
  def inspect
@@ -1,44 +1,57 @@
1
- # frozen-string-literal: true
1
+ # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
3
+ require "forwardable"
4
+ require_relative "local_date"
5
+ require_relative "local_time"
4
6
 
5
7
  module Tomlrb
8
+ # A TOML local date-time (no offset), e.g. `1979-05-27T07:32:00.999999`.
6
9
  class LocalDateTime
7
10
  extend Forwardable
8
11
 
9
- def_delegators :@time, :year, :month, :day, :hour, :min, :sec, :usec, :nsec
12
+ attr_reader :year, :month, :day, :hour, :min
10
13
 
11
- def initialize(year, month, day, hour, min, sec) # rubocop:disable Metrics/ParameterLists
12
- @time = Time.utc(year, month, day, hour, min, sec)
13
- raise ArgumentError, "Invalid Local Date-Time: #{year}-#{month}-#{day}T#{hour}:#{min}:#{sec}" unless min.to_i == @time.min && hour.to_i == @time.hour && day.to_i == @time.day && month.to_i == @time.month && year.to_i == @time.year
14
+ def_delegators :to_time, :sec, :usec, :nsec
14
15
 
16
+ # Fields are validated here; the Time behind them is built on first use.
17
+ #
18
+ # @param sec [Integer, Rational, Float] seconds, fraction included
19
+ def initialize(year, month, day, hour, min, sec) # rubocop:disable Metrics/ParameterLists
20
+ @year = year.to_i
21
+ @month = month.to_i
22
+ @day = day.to_i
23
+ @hour = hour.to_i
24
+ @min = min.to_i
15
25
  @sec = sec
26
+ raise ArgumentError, "Invalid Local Date-Time: #{self}" unless
27
+ LocalDate.valid_date?(@year, @month, @day) && @hour < 24 && @min < 60 && sec < 60
16
28
  end
17
29
 
18
- # @param offset [String, Symbol, Numeric, nil] time zone offset.
19
- # * when +String+, must be '+HH:MM' format, '-HH:MM' format, 'UTC', 'A'..'I' or 'K'..'Z'. Arguments excluding '+-HH:MM' are supporeted at Ruby >= 2.7.0
20
- # * when +Symbol+, must be +:dst+(for summar time for local) or +:std+(for standard time).
21
- # * when +Numeric+, it is time zone offset in second.
22
- # * when +nil+, local time zone offset is used.
23
- # @return [Time]
24
- def to_time(offset = '-00:00')
25
- return @time if offset == '-00:00'
26
- Time.new(year, month, day, hour, min, @sec, offset)
30
+ # @param offset [String, Integer, nil] UTC offset as accepted by Time.new
31
+ # ("+09:00", "UTC", "Z", seconds east of UTC, or nil for the local zone)
32
+ # @return [Time] this date-time at the given offset (UTC by default)
33
+ def to_time(offset = "-00:00")
34
+ return @time ||= Time.utc(@year, @month, @day, @hour, @min, @sec) if offset == "-00:00"
35
+
36
+ Time.new(@year, @month, @day, @hour, @min, @sec, offset)
27
37
  end
28
38
 
29
39
  def to_s
30
- frac = (@sec - sec)
31
- frac_str = frac.zero? ? '' : frac.to_s[1..-1]
32
- @time.strftime('%FT%T') << frac_str
40
+ format("%04d-%02d-%02dT", @year, @month, @day) << LocalTime.format(@hour, @min, @sec)
33
41
  end
34
42
 
35
43
  def ==(other)
36
- other.is_a?(self.class) &&
37
- to_time == other.to_time
44
+ other.is_a?(self.class) && @year == other.year && @month == other.month && @day == other.day &&
45
+ @hour == other.hour && @min == other.min && @sec == other.seconds
38
46
  end
39
47
 
40
48
  def inspect
41
49
  "#<#{self.class}: #{self}>"
42
50
  end
51
+
52
+ protected
53
+
54
+ # Seconds with their fraction; #sec is the whole part only, like Time#sec.
55
+ def seconds = @sec
43
56
  end
44
57
  end
@@ -1,41 +1,61 @@
1
- # frozen-string-literal: true
1
+ # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
3
+ require "forwardable"
4
4
 
5
5
  module Tomlrb
6
+ # A TOML local time (no date, no offset), e.g. `07:32:00.999999`.
6
7
  class LocalTime
7
8
  extend Forwardable
8
9
 
9
- def_delegators :@time, :hour, :min, :sec, :usec, :nsec
10
+ attr_reader :hour, :min
10
11
 
12
+ def_delegators :time, :sec, :usec, :nsec
13
+
14
+ # @return [String] `hh:mm:ss`, plus `.` and the fraction digits if `sec` has any
15
+ def self.format(hour, min, sec)
16
+ whole = sec.floor
17
+ fraction = sec - whole
18
+ text = Kernel.format("%02d:%02d:%02d", hour, min, whole)
19
+ fraction.zero? ? text : text << Kernel.format("%.9f", fraction)[1..].sub(/0+\z/, "")
20
+ end
21
+
22
+ # Fields are validated here; the Time behind them is built on first use.
23
+ #
24
+ # @param sec [Integer, Rational, Float] seconds, fraction included
11
25
  def initialize(hour, min, sec)
12
- @time = Time.utc(0, 1, 1, hour, min, sec)
13
- raise ArgumentError, "Invalid Local Time: #{hour}-#{min}-#{sec}" unless min.to_i == @time.min && hour.to_i == @time.hour
26
+ @hour = hour.to_i
27
+ @min = min.to_i
14
28
  @sec = sec
29
+ raise ArgumentError, "Invalid Local Time: #{self}" unless @hour < 24 && @min < 60 && sec < 60
15
30
  end
16
31
 
17
- # @param year [Integer]
18
- # @param month [Integer]
19
- # @param day [Integer]
20
32
  # @param offset see {LocalDateTime#to_time}
21
- # @return [Time] the time of the date specified by params
22
- def to_time(year, month, day, offset = '-00:00')
23
- Time.new(year, month, day, hour, min, @sec, offset)
33
+ # @return [Time] this time on the given day
34
+ def to_time(year, month, day, offset = "-00:00")
35
+ Time.new(year, month, day, @hour, @min, @sec, offset)
24
36
  end
25
37
 
26
38
  def to_s
27
- frac = (@sec - sec)
28
- frac_str = frac.zero? ? '' : frac.to_s[1..-1]
29
- @time.strftime('%T') << frac_str
39
+ LocalTime.format(@hour, @min, @sec)
30
40
  end
31
41
 
32
42
  def ==(other)
33
- other.is_a?(self.class) &&
34
- @time == other.to_time(0, 1, 1)
43
+ other.is_a?(self.class) && @hour == other.hour && @min == other.min && @sec == other.seconds
35
44
  end
36
45
 
37
46
  def inspect
38
47
  "#<#{self.class}: #{self}>"
39
48
  end
49
+
50
+ protected
51
+
52
+ # Seconds with their fraction; #sec is the whole part only, like Time#sec.
53
+ def seconds = @sec
54
+
55
+ private
56
+
57
+ def time
58
+ @time ||= Time.utc(0, 1, 1, @hour, @min, @sec)
59
+ end
40
60
  end
41
61
  end