Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6969163
Update toml-test
KitaitiMakoto Oct 29, 2025
38e4a31
Update toml-test to v1.6.0
KitaitiMakoto Oct 29, 2025
7143376
Fix a bug of process_json
KitaitiMakoto Oct 29, 2025
5bf8aeb
Increase possible error for invalid TOML
KitaitiMakoto Oct 29, 2025
214d8ae
Don't TOML valid only for 1.1
KitaitiMakoto Oct 29, 2025
c59ee28
Raise error when duplicate key declaration after implicite key genera…
KitaitiMakoto Oct 29, 2025
3b01645
Raise error for invalid date time
KitaitiMakoto Oct 29, 2025
fb9a872
Prevent double comma
KitaitiMakoto Nov 1, 2025
8c3592d
Restrict date time format on scanning
KitaitiMakoto Nov 1, 2025
15e931d
Prevent comma-only array
KitaitiMakoto Nov 1, 2025
b566f78
[skip ci]Update generated parser
KitaitiMakoto Nov 1, 2025
b595e86
Prevent redefinition of dotted table
KitaitiMakoto Nov 1, 2025
6132cb1
Prevent unclosed string basic
KitaitiMakoto Nov 1, 2025
b410e00
Check of dup keys for inline tables
KitaitiMakoto Nov 2, 2025
a4461e0
Use patched toml-test
KitaitiMakoto Nov 2, 2025
caaece2
Assign array to variables for readability
KitaitiMakoto Nov 3, 2025
b168a3b
Pass matched string of date times to parser
KitaitiMakoto Nov 3, 2025
42a835d
Allow date-like key for tables
KitaitiMakoto Nov 3, 2025
43d8df8
Prevent dot at end of table keys
KitaitiMakoto Nov 3, 2025
648be54
Prevent overwriting key
KitaitiMakoto Nov 3, 2025
501d480
Update generated parser
KitaitiMakoto Nov 3, 2025
3c4f00e
Don't use unsupported notation for older Rubies
KitaitiMakoto Nov 3, 2025
16cbb54
Update generated parser
KitaitiMakoto Nov 3, 2025
bd7d7ad
Skip test which causes stack overflow for TruffleRuby
KitaitiMakoto Nov 3, 2025
4f49008
Use literaral comma
KitaitiMakoto Nov 3, 2025
8db4d41
Update generated parser
KitaitiMakoto Nov 3, 2025
bf481e0
Use skip method for skipped tests
KitaitiMakoto Nov 3, 2025
a97ffb7
Update lib/tomlrb/parser.y
KitaitiMakoto Nov 3, 2025
87fef24
Update generated parser
KitaitiMakoto Nov 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
534 changes: 315 additions & 219 deletions lib/tomlrb/generated_parser.rb

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion lib/tomlrb/handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,21 @@ def push(o)

def push_inline(inline_arrays)
merged_inline = {}
keys = Keys.new

inline_arrays.each do |inline_array|
current = merged_inline
value = inline_array.pop
keys.add_table_key inline_array, value.is_a?(Array)

inline_array.each_with_index do |inline_key, inline_index|
inline_key = inline_key.to_sym if @symbolize_keys
last_key = inline_index == inline_array.size - 1

if last_key
if current[inline_key].nil?
keys.add_pair_key [inline_key], []

current[inline_key] = value
else
raise Key::KeyConflict, "Inline key #{inline_key} is already used"
Expand Down Expand Up @@ -119,10 +124,14 @@ def validate_value(value)
private

def assign_key_path(current, key, key_emptied)
existed = current.key?(key)
raise ParseError, "Cannot overwrite value with key #{key}" if existed && !current[key].is_a?(Hash)
if key_emptied
raise ParseError, "Cannot overwrite value with key #{key}" unless current.is_a?(Hash)

current[key] = @stack.pop
value = @stack.pop
raise ParseError, "Cannot overwrite value with key #{key}" if current[key].is_a?(Hash) && !value.is_a?(Hash)
current[key] = value
return current
end
current[key] ||= {}
Expand Down Expand Up @@ -175,6 +184,7 @@ def find_or_create_first_table_key(current, key, declared, is_array_of_tables)
raise Key::KeyConflict, "Key #{key} is already used"
end
k = existed || Key.new(key, :table, declared)
k.declared = k.declared? || declared
current[key] = k
k
end
Expand Down Expand Up @@ -206,6 +216,7 @@ class Key
class KeyConflict < ParseError; end

attr_reader :key, :type
attr_writer :declared

def initialize(key, type, declared = false)
@key = key
Expand Down
3 changes: 2 additions & 1 deletion lib/tomlrb/local_date.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class LocalDate
def_delegators :@time, :year, :month, :day

def initialize(year, month, day)
@time = Time.new(year, month, day, 0, 0, 0, '-00:00')
@time = Time.utc(year, month, day, 0, 0, 0)
raise ArgumentError, "Invalid Local Date: #{year}-#{month}-#{day}" unless day.to_i == @time.day && month.to_i == @time.month && year.to_i == @time.year
end

# @param offset see {LocalDateTime#to_time}
Expand Down
4 changes: 3 additions & 1 deletion lib/tomlrb/local_date_time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ class LocalDateTime
def_delegators :@time, :year, :month, :day, :hour, :min, :sec, :usec, :nsec

def initialize(year, month, day, hour, min, sec) # rubocop:disable Metrics/ParameterLists
@time = Time.new(year, month, day, hour, min, sec, '-00:00')
@time = Time.utc(year, month, day, hour, min, sec)
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

@sec = sec
end

Expand Down
3 changes: 2 additions & 1 deletion lib/tomlrb/local_time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class LocalTime
def_delegators :@time, :hour, :min, :sec, :usec, :nsec

def initialize(hour, min, sec)
@time = Time.new(0, 1, 1, hour, min, sec, '-00:00')
@time = Time.utc(0, 1, 1, hour, min, sec)
raise ArgumentError, "Invalid Local Time: #{hour}-#{min}-#{sec}" unless min.to_i == @time.min && hour.to_i == @time.hour
@sec = sec
end

Expand Down
88 changes: 61 additions & 27 deletions lib/tomlrb/parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,19 @@ rule
| NEWLINE
;
table
: table_start table_continued NEWLINE
| table_start table_continued EOS
: table_start table_identifier table_end newlines
| table_start table_identifier table_end EOS
| table_start table_end newlines
| table_start table_end EOS
;
table_start
: '[' '[' { @handler.start_(:array_of_tables) }
| '[' { @handler.start_(:table) }
;
table_continued
: ']' ']' { array = @handler.end_(:array_of_tables); @handler.set_context(array, is_array_of_tables: true) }
| ']' { array = @handler.end_(:table); @handler.set_context(array) }
| table_identifier table_next
;
table_next
table_end
: ']' ']' { array = @handler.end_(:array_of_tables); @handler.set_context(array, is_array_of_tables: true) }
| ']' { array = @handler.end_(:table); @handler.set_context(array) }
| '.' table_continued
;
table_identifier
: table_identifier '.' table_identifier_component { @handler.push(val[2]) }
Expand All @@ -47,6 +44,8 @@ rule
| NON_DEC_INTEGER
| FLOAT_KEYWORD
| BOOLEAN
| DATETIME { result = val[0][0] }
| LOCAL_TIME { result = val[0][0] }
;
inline_table
: inline_table_start inline_table_end
Expand Down Expand Up @@ -123,34 +122,66 @@ rule
| NON_DEC_INTEGER
| FLOAT_KEYWORD
| BOOLEAN
| DATETIME { result = val[0][0] }
| LOCAL_TIME { result = val[0][0] }
;
array
: start_array array_continued
: start_array array_first_value array_values comma end_array
| start_array array_first_value array_values end_array
| start_array array_first_value comma end_array
| start_array array_first_value end_array
| start_array end_array
;
array_continued
: ']' { array = @handler.end_(:array); @handler.push(array.compact) }
| value array_next
| NEWLINE array_continued
array_first_value
: newlines non_nil_value
| non_nil_value
;
array_next
: ']' { array = @handler.end_(:array); @handler.push(array.compact) }
| ',' array_continued
| NEWLINE array_continued
array_values
: array_values array_value
| array_value
;
array_value
: comma newlines non_nil_value
| comma non_nil_value
;
start_array
: '[' { @handler.start_(:array) }
;
end_array
: newlines ']' { array = @handler.end_(:array); @handler.push(array.compact) }
| ']' { array = @handler.end_(:array); @handler.push(array.compact) }
;
comma
: newlines ','
| ','
;
newlines
: newlines NEWLINE
| NEWLINE
;
value
: scalar { @handler.push(val[0]) }
| array
| inline_table
;
non_nil_value
: non_nil_scalar { @handler.push(val[0]) }
| array
| inline_table
;
scalar
: string
| literal
;
non_nil_scalar
: string
| non_nil_literal
;
literal
| FLOAT { result = val[0].to_f }
| non_nil_literal
;
non_nil_literal
: FLOAT { result = val[0].to_f }
| FLOAT_KEYWORD {
v = val[0]
result = if v.end_with?('nan')
Expand All @@ -170,23 +201,26 @@ rule
}
| BOOLEAN { result = val[0] == 'true' ? true : false }
| DATETIME {
v = val[0]
result = if v[6].nil?
if v[4].nil?
LocalDate.new(v[0], v[1], v[2])
_str, year, month, day, hour, min, sec, offset = val[0]
result = if offset.nil?
if hour.nil?
LocalDate.new(year, month, day)
else
LocalDateTime.new(v[0], v[1], v[2], v[3] || 0, v[4] || 0, v[5].to_f)
LocalDateTime.new(year, month, day, hour, min || 0, sec.to_f)
end
else
# Patch for 24:00:00 which Ruby parses
if v[3].to_i == 24 && v[4].to_i == 0 && v[5].to_i == 0
v[3] = (v[3].to_i + 1).to_s
if hour.to_i == 24 && min.to_i == 0 && sec.to_i == 0
hour = (hour.to_i + 1).to_s
end

Time.new(v[0], v[1], v[2], v[3] || 0, v[4] || 0, v[5].to_f, v[6])
time = Time.new(year, month, day, hour || 0, min || 0, sec.to_f, offset)
# Should be out of parser.y?
raise ArgumentError, "Invalid Offset Date-Time: #{year}-#{month}-#{day}T#{hour}:#{min}:#{sec}#{offset}" 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
time
end
}
| LOCAL_TIME { result = LocalTime.new(*val[0]) }
| LOCAL_TIME { result = LocalTime.new(*val[0][1..-1]) }
;
string
: STRING_MULTI { result = StringUtils.replace_escaped_chars(StringUtils.multiline_replacements(val[0])) }
Expand Down
10 changes: 5 additions & 5 deletions lib/tomlrb/scanner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ class Scanner
NEWLINE =
/(?:[ \t]*(?:\r?\n)[ \t]*)+/.freeze
STRING_BASIC =
/(")(?:\\?[^\u0000-\u0008\u000A-\u001F\u007F])*?\1/.freeze
/(")(?:\\?[^\u0000-\u0008\u000A-\u001F\u007F\\]|(?:\\[^\u0000-\u0008\u000A-\u001F\u007F]))*?\1/.freeze
STRING_MULTI =
/"{3}([^\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]*?(?<!\\)"{3,5})/m.freeze
STRING_LITERAL =
/(')(?:\\?[^\u0000-\u0008\u000A-\u001F\u007F])*?\1/.freeze
STRING_LITERAL_MULTI =
/'{3}([^\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]*?'{3,5})/m.freeze
DATETIME =
/(-?\d{4})-(\d{2})-(\d{2})(?:(?:t|\s)(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?))?(z|[-+]\d{2}:\d{2})?/i.freeze
/(-?\d{4})-([01]\d)-([0-3]\d)(?:(?:t|\s)([0-2]\d):([0-5]\d):([0-6]\d(?:\.\d+)?))?(z|[-+][01]\d:\d{2})?/i.freeze
LOCAL_TIME =
/(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)/.freeze
/([0-2]\d):([0-5]\d):([0-6]\d(?:\.\d+)?)/.freeze
FLOAT =
/[+-]?(?:(?:\d|[1-9](?:_?\d)*)\.\d(?:_?\d)*|\d+(?=[eE]))(?:[eE][+-]?[0-9]+(_[0-9])*[0-9]*)?(?!\w)/.freeze
FLOAT_KEYWORD =
Expand Down Expand Up @@ -73,12 +73,12 @@ def next_token

def process_datetime
offset = @ss[7].gsub(/[zZ]/, '+00:00') if @ss[7]
args = [@ss[1], @ss[2], @ss[3], @ss[4], @ss[5], @ss[6], offset]
args = [@ss[0], @ss[1], @ss[2], @ss[3], @ss[4], @ss[5], @ss[6], offset]
[:DATETIME, args]
end

def process_local_time
args = [@ss[1], @ss[2], @ss[3].to_f]
args = [@ss[0], @ss[1], @ss[2], @ss[3].to_f]
[:LOCAL_TIME, args]
end

Expand Down
23 changes: 21 additions & 2 deletions test/test_compliance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
]

describe Tomlrb::Parser do
toml_test_list_file = File.join(__dir__, '../toml-test/tests/files-toml-1.0.0')
toml_test_list = File.readlines(toml_test_list_file, chomp: true)
toml_test_list_base = Pathname.new("toml-test/tests")

tests_dirs = [
File.join(__dir__, '../toml-spec-tests'),
File.join(__dir__, '../toml-test/tests')
Expand All @@ -35,6 +39,16 @@
local_path = toml_path.relative_path_from(Pathname.new(File.join(__dir__, '..')))

it "parses #{local_path}" do
if ENV['CI'] == 'true' && RUBY_ENGINE == 'truffleruby' &&
local_path.to_path == 'toml-spec-tests/values/qa-table-inline-nested-1000.toml'
skip 'Skipping #{local_path} on TruffleRuby CI due to stack size limitations'
end

if tests_dir == File.join(__dir__, '../toml-test/tests')
path_in_list = local_path.relative_path_from(toml_test_list_base)
skip 'Skipping test for Toml 1.1' unless toml_test_list.include?(path_in_list.to_path)
end

actual = Tomlrb.load_file(toml_path.to_path)
json_path = toml_path.sub_ext('.json')
yaml_path = toml_path.sub_ext('.yaml')
Expand All @@ -52,8 +66,13 @@
toml_path = toml_path.expand_path
local_path = toml_path.relative_path_from(Pathname.new(File.join(__dir__, '..')))

if tests_dir == File.join(__dir__, '../toml-test/tests')
path_in_list = local_path.relative_path_from(toml_test_list_base)
next unless toml_test_list.include?(path_in_list.to_path)
end

it "raises an error on parsing #{local_path}" do
_{ Tomlrb.load_file(toml_path.to_path) }.must_raise Tomlrb::ParseError, RangeError, ArgumentError
_{ Tomlrb.load_file(toml_path.to_path) }.must_raise Tomlrb::ParseError, RangeError, ArgumentError, IndexError, TypeError
end
end
end
Expand Down Expand Up @@ -119,7 +138,7 @@ def process_json_leaf(node)
def process_json(node)
case node
when Hash
if node['type']
if node.keys == ['type', 'value']
process_json_leaf(node)
else
node.each_with_object({}) {|(key, value), table|
Expand Down
6 changes: 6 additions & 0 deletions test/test_local_date_time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ def time_knows_zone_names?
_(d.to_s).must_equal '1979-05-27'
end

it 'raises error for invalid date' do
assert_raises ArgumentError do
Tomlrb::LocalDate.new('2100', '02', '29')
end
end

describe '#to_time' do
subject { Tomlrb::LocalDate.new('1979', '05', '27') }

Expand Down
2 changes: 1 addition & 1 deletion toml-test