Summary
Psych::ScalarScanner resolves YAML 1.1 base-60 integers with a positional weight that assumes exactly three components, and applies the leading - to only the first component. Two independent bugs result: 1. Any two-component sexagesimal (12:30) is 60× too large. 2. Any negative sexagesimal with a non-zero trailing component (-12:30:00) has the wrong magnitude, independent of (1). A consequence of (1) is that 12:30 and 12:30:00 two distinct documents load to the same Ruby value.
Environment
- ruby 3.4.10
- psych 5.2.2 (bundled)
Reproduction
ruby require 'yaml' Psych.safe_load('a: 12:30')['a'] # 45000, expected 750
Psych.safe_load('a: 1:30')['a'] #5400, expected 90
Psych.safe_load('a: 100:00')['a'] #360000, expected 6000
Psych.safe_load('a: 12:30.5')['a'] #45030.0, expected 750.5
Psych.safe_load('a: -12:30')['a'] #-41400, expected -750
Psych.safe_load('a: -12:30:00')['a'] #-41400, expected -45000
Expected results
Per the YAML 1.1 int type, the sexagesimal form is [-+]?[1-9][0-9_]*(:[0-5]?[0-9])+, and the spec's canonical example fixes the weighting: sexagesimal: 190:20:30 -> 685230 i.e. 190*60^2 + 20*60^1 + 30*60^0. Components are weighted from the right, so the rightmost component always has weight 60^0. Therefore 12:30 = 12*60 + 30 = 750. Psych parses 190:20:30 correctly (685230), which confirms right-alignment is intended the three-component case is right only because it is the case the code hard-codes.
Root cause
lib/psych/scalar_scanner.rb:
ruby elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}$/) i = 0 string.split(':').each_with_index do |n,e| i += (n.to_i * 60 ** (e - 2).abs) end i
60 ** (e - 2).abs weights by distance from index 2, i.e. it assumes the string always has three components. With two components it yields 60^2 and 60^1 instead of 60^1 and 60^0. n.to_i also carries the sign only on the first element, so subsequent components are added as positive magnitudes: -12:30:00 becomes -43200 + 1800 + 0. Two smaller spec deviations in the same regex: {1,2} caps the form at three components where the spec allows (...)+, and the leading component is [0-9] where the spec says [1-9] (so 0:30 is resolved as an integer when it should stay a String).
Summary
Psych::ScalarScannerresolves YAML 1.1 base-60 integers with a positional weight that assumes exactly three components, and applies the leading-to only the first component. Two independent bugs result: 1. Any two-component sexagesimal (12:30) is 60× too large. 2. Any negative sexagesimal with a non-zero trailing component (-12:30:00) has the wrong magnitude, independent of (1). A consequence of (1) is that12:30and12:30:00two distinct documents load to the same Ruby value.Environment
Reproduction
Expected results
Per the YAML 1.1 int type, the sexagesimal form is
[-+]?[1-9][0-9_]*(:[0-5]?[0-9])+, and the spec's canonical example fixes the weighting: sexagesimal: 190:20:30 -> 685230 i.e.190*60^2 + 20*60^1 + 30*60^0. Components are weighted from the right, so the rightmost component always has weight60^0. Therefore12:30=12*60 + 30= 750. Psych parses190:20:30correctly (685230), which confirms right-alignment is intended the three-component case is right only because it is the case the code hard-codes.Root cause
lib/psych/scalar_scanner.rb:ruby elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}$/) i = 0 string.split(':').each_with_index do |n,e| i += (n.to_i * 60 ** (e - 2).abs) end i60 ** (e - 2).absweights by distance from index 2, i.e. it assumes the string always has three components. With two components it yields60^2and60^1instead of60^1and60^0.n.to_ialso carries the sign only on the first element, so subsequent components are added as positive magnitudes:-12:30:00becomes-43200 + 1800 + 0. Two smaller spec deviations in the same regex:{1,2}caps the form at three components where the spec allows(...)+, and the leading component is[0-9]where the spec says[1-9](so0:30is resolved as an integer when it should stay a String).