safe_yaml 0.1 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +48 -0
- data/CHANGES.md +154 -0
- data/Gemfile +3 -1
- data/LICENSE.txt +22 -0
- data/README.md +191 -0
- data/Rakefile +22 -2
- data/bin/safe_yaml +75 -0
- data/bundle_install_all_ruby_versions.sh +11 -0
- data/lib/safe_yaml.rb +90 -6
- data/lib/safe_yaml/deep.rb +34 -0
- data/lib/safe_yaml/libyaml_checker.rb +36 -0
- data/lib/safe_yaml/load.rb +181 -0
- data/lib/safe_yaml/parse/date.rb +37 -0
- data/lib/safe_yaml/parse/hexadecimal.rb +12 -0
- data/lib/safe_yaml/parse/sexagesimal.rb +26 -0
- data/lib/safe_yaml/psych_handler.rb +99 -0
- data/lib/safe_yaml/psych_resolver.rb +52 -0
- data/lib/safe_yaml/resolver.rb +94 -0
- data/lib/safe_yaml/safe_to_ruby_visitor.rb +29 -0
- data/lib/safe_yaml/store.rb +39 -0
- data/lib/safe_yaml/syck_hack.rb +36 -0
- data/lib/safe_yaml/syck_node_monkeypatch.rb +43 -0
- data/lib/safe_yaml/syck_resolver.rb +38 -0
- data/lib/safe_yaml/transform.rb +41 -0
- data/lib/safe_yaml/transform/to_boolean.rb +21 -0
- data/lib/safe_yaml/transform/to_date.rb +13 -0
- data/lib/safe_yaml/transform/to_float.rb +33 -0
- data/lib/safe_yaml/transform/to_integer.rb +26 -0
- data/lib/safe_yaml/transform/to_nil.rb +18 -0
- data/lib/safe_yaml/transform/to_symbol.rb +17 -0
- data/lib/safe_yaml/transform/transformation_map.rb +47 -0
- data/lib/{version.rb → safe_yaml/version.rb} +1 -1
- data/run_specs_all_ruby_versions.sh +38 -0
- data/safe_yaml.gemspec +11 -8
- data/spec/exploit.1.9.2.yaml +2 -0
- data/spec/exploit.1.9.3.yaml +2 -0
- data/spec/issue48.txt +20 -0
- data/spec/issue49.yml +0 -0
- data/spec/libyaml_checker_spec.rb +69 -0
- data/spec/psych_resolver_spec.rb +10 -0
- data/spec/resolver_specs.rb +278 -0
- data/spec/safe_yaml_spec.rb +697 -23
- data/spec/spec_helper.rb +37 -2
- data/spec/store_spec.rb +57 -0
- data/spec/support/exploitable_back_door.rb +13 -7
- data/spec/syck_resolver_spec.rb +10 -0
- data/spec/transform/base64_spec.rb +11 -0
- data/spec/transform/to_date_spec.rb +60 -0
- data/spec/transform/to_float_spec.rb +42 -0
- data/spec/transform/to_integer_spec.rb +64 -0
- data/spec/transform/to_symbol_spec.rb +51 -0
- data/spec/yaml_spec.rb +15 -0
- metadata +78 -24
- data/Gemfile.lock +0 -28
- data/lib/handler.rb +0 -86
- data/spec/handler_spec.rb +0 -108
data/bin/safe_yaml
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
|
5
|
+
require 'optparse'
|
6
|
+
require 'safe_yaml/load'
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
option_parser = OptionParser.new do |opts|
|
10
|
+
opts.banner = "Usage: safe_yaml [options]"
|
11
|
+
|
12
|
+
opts.on("-f", "--file=<path>", "Parse the given YAML file, dump the result to STDOUT") do |file|
|
13
|
+
options[:file] = file
|
14
|
+
end
|
15
|
+
|
16
|
+
opts.on("--libyaml-check", "Check for libyaml vulnerability CVE-2014-2525 on your system") do
|
17
|
+
options[:libyaml_check] = true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
option_parser.parse!
|
22
|
+
|
23
|
+
def report_libyaml_ok
|
24
|
+
puts "\e[32mGood news! You definitely have either a patched or up-to-date libyaml version :)\e[39m"
|
25
|
+
end
|
26
|
+
|
27
|
+
def check_for_overflow_bug
|
28
|
+
YAML.load("--- !#{'%20' * 100}")
|
29
|
+
report_libyaml_ok
|
30
|
+
end
|
31
|
+
|
32
|
+
def perform_libyaml_check(force=false)
|
33
|
+
unless SafeYAML::LibyamlChecker.libyaml_version_ok?
|
34
|
+
warn <<-EOM.gsub(/^ +/, ' ')
|
35
|
+
|
36
|
+
\e[33mSafeYAML Warning\e[39m
|
37
|
+
\e[33m----------------\e[39m
|
38
|
+
|
39
|
+
\e[31mYou may have an outdated version of libyaml (#{SafeYAML::LibyamlChecker::LIBYAML_VERSION}) installed on your system.\e[39m
|
40
|
+
|
41
|
+
Prior to 0.1.6, libyaml is vulnerable to a heap overflow exploit from malicious YAML payloads.
|
42
|
+
|
43
|
+
For more info, see:
|
44
|
+
https://www.ruby-lang.org/en/news/2014/03/29/heap-overflow-in-yaml-uri-escape-parsing-cve-2014-2525/
|
45
|
+
EOM
|
46
|
+
end
|
47
|
+
|
48
|
+
puts <<-EOM.gsub(/^ +/, ' ')
|
49
|
+
|
50
|
+
Hit Enter to check if your version of libyaml is vulnerable. This will run a test \e[31mwhich may crash\e[39m
|
51
|
+
\e[31mthe current process\e[39m. If it does, your system is vulnerable and you should do something about it.
|
52
|
+
|
53
|
+
Type "nm" and hit Enter if you don't want to run the check.
|
54
|
+
|
55
|
+
See the project wiki for more info:
|
56
|
+
|
57
|
+
https://github.com/dtao/safe_yaml/wiki/The-libyaml-vulnerability
|
58
|
+
EOM
|
59
|
+
|
60
|
+
if STDIN.readline.chomp("\n") != 'nm'
|
61
|
+
check_for_overflow_bug
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if options[:libyaml_check]
|
66
|
+
perform_libyaml_check(options[:force_libyaml_check])
|
67
|
+
|
68
|
+
elsif options[:file]
|
69
|
+
yaml = File.read(options[:file])
|
70
|
+
result = SafeYAML.load(yaml)
|
71
|
+
puts result.inspect
|
72
|
+
|
73
|
+
else
|
74
|
+
puts option_parser.help
|
75
|
+
end
|
data/lib/safe_yaml.rb
CHANGED
@@ -1,10 +1,94 @@
|
|
1
|
-
require "
|
2
|
-
require "handler"
|
1
|
+
require "safe_yaml/load"
|
3
2
|
|
4
3
|
module YAML
|
5
|
-
def self.
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
def self.load_with_options(yaml, *original_arguments)
|
5
|
+
filename, options = filename_and_options_from_arguments(original_arguments)
|
6
|
+
safe_mode = safe_mode_from_options("load", options)
|
7
|
+
arguments = [yaml]
|
8
|
+
|
9
|
+
if safe_mode == :safe
|
10
|
+
arguments << filename if SafeYAML::YAML_ENGINE == "psych"
|
11
|
+
arguments << options_for_safe_load(options)
|
12
|
+
safe_load(*arguments)
|
13
|
+
else
|
14
|
+
arguments << filename if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
|
15
|
+
unsafe_load(*arguments)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.load_file_with_options(file, options={})
|
20
|
+
safe_mode = safe_mode_from_options("load_file", options)
|
21
|
+
if safe_mode == :safe
|
22
|
+
safe_load_file(file, options_for_safe_load(options))
|
23
|
+
else
|
24
|
+
unsafe_load_file(file)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.safe_load(*args)
|
29
|
+
SafeYAML.load(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.safe_load_file(*args)
|
33
|
+
SafeYAML.load_file(*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
|
37
|
+
def self.unsafe_load_file(filename)
|
38
|
+
# https://github.com/tenderlove/psych/blob/v1.3.2/lib/psych.rb#L296-298
|
39
|
+
File.open(filename, 'r:bom|utf-8') { |f| self.unsafe_load(f, filename) }
|
40
|
+
end
|
41
|
+
|
42
|
+
else
|
43
|
+
def self.unsafe_load_file(filename)
|
44
|
+
# https://github.com/tenderlove/psych/blob/v1.2.2/lib/psych.rb#L231-233
|
45
|
+
self.unsafe_load File.open(filename)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class << self
|
50
|
+
alias_method :unsafe_load, :load
|
51
|
+
alias_method :load, :load_with_options
|
52
|
+
alias_method :load_file, :load_file_with_options
|
53
|
+
|
54
|
+
private
|
55
|
+
def filename_and_options_from_arguments(arguments)
|
56
|
+
if arguments.count == 1
|
57
|
+
if arguments.first.is_a?(String)
|
58
|
+
return arguments.first, {}
|
59
|
+
else
|
60
|
+
return nil, arguments.first || {}
|
61
|
+
end
|
62
|
+
|
63
|
+
else
|
64
|
+
return arguments.first, arguments.last || {}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def safe_mode_from_options(method, options={})
|
69
|
+
if options[:safe].nil?
|
70
|
+
safe_mode = SafeYAML::OPTIONS[:default_mode] || :safe
|
71
|
+
|
72
|
+
if SafeYAML::OPTIONS[:default_mode].nil? && !SafeYAML::OPTIONS[:suppress_warnings]
|
73
|
+
|
74
|
+
Kernel.warn <<-EOWARNING.gsub(/^\s+/, '')
|
75
|
+
Called '#{method}' without the :safe option -- defaulting to #{safe_mode} mode.
|
76
|
+
You can avoid this warning in the future by setting the SafeYAML::OPTIONS[:default_mode] option (to :safe or :unsafe).
|
77
|
+
EOWARNING
|
78
|
+
|
79
|
+
SafeYAML::OPTIONS[:suppress_warnings] = true
|
80
|
+
end
|
81
|
+
|
82
|
+
return safe_mode
|
83
|
+
end
|
84
|
+
|
85
|
+
options[:safe] ? :safe : :unsafe
|
86
|
+
end
|
87
|
+
|
88
|
+
def options_for_safe_load(base_options)
|
89
|
+
options = base_options.dup
|
90
|
+
options.delete(:safe)
|
91
|
+
options
|
92
|
+
end
|
9
93
|
end
|
10
94
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module SafeYAML
|
2
|
+
class Deep
|
3
|
+
def self.freeze(object)
|
4
|
+
object.each do |*entry|
|
5
|
+
value = entry.last
|
6
|
+
case value
|
7
|
+
when String, Regexp
|
8
|
+
value.freeze
|
9
|
+
when Enumerable
|
10
|
+
Deep.freeze(value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
return object.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.copy(object)
|
18
|
+
duplicate = object.dup rescue object
|
19
|
+
|
20
|
+
case object
|
21
|
+
when Array
|
22
|
+
(0...duplicate.count).each do |i|
|
23
|
+
duplicate[i] = Deep.copy(duplicate[i])
|
24
|
+
end
|
25
|
+
when Hash
|
26
|
+
duplicate.keys.each do |key|
|
27
|
+
duplicate[key] = Deep.copy(duplicate[key])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
duplicate
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
module SafeYAML
|
4
|
+
class LibyamlChecker
|
5
|
+
LIBYAML_VERSION = Psych::LIBYAML_VERSION rescue nil
|
6
|
+
|
7
|
+
# Do proper version comparison (e.g. so 0.1.10 is >= 0.1.6)
|
8
|
+
SAFE_LIBYAML_VERSION = Gem::Version.new("0.1.6")
|
9
|
+
|
10
|
+
KNOWN_PATCHED_LIBYAML_VERSIONS = Set.new([
|
11
|
+
# http://people.canonical.com/~ubuntu-security/cve/2014/CVE-2014-2525.html
|
12
|
+
"0.1.4-2ubuntu0.12.04.3",
|
13
|
+
"0.1.4-2ubuntu0.12.10.3",
|
14
|
+
"0.1.4-2ubuntu0.13.10.3",
|
15
|
+
"0.1.4-3ubuntu3",
|
16
|
+
|
17
|
+
# https://security-tracker.debian.org/tracker/CVE-2014-2525
|
18
|
+
"0.1.3-1+deb6u4",
|
19
|
+
"0.1.4-2+deb7u4",
|
20
|
+
"0.1.4-3.2"
|
21
|
+
]).freeze
|
22
|
+
|
23
|
+
def self.libyaml_version_ok?
|
24
|
+
return true if YAML_ENGINE != "psych" || defined?(JRUBY_VERSION)
|
25
|
+
return true if Gem::Version.new(LIBYAML_VERSION || "0") >= SAFE_LIBYAML_VERSION
|
26
|
+
return libyaml_patched?
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.libyaml_patched?
|
30
|
+
return false if (`which dpkg` rescue '').empty?
|
31
|
+
libyaml_version = `dpkg -s libyaml-0-2`.match(/^Version: (.*)$/)
|
32
|
+
return false if libyaml_version.nil?
|
33
|
+
KNOWN_PATCHED_LIBYAML_VERSIONS.include?(libyaml_version[1])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require "set"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
# This needs to be defined up front in case any internal classes need to base
|
5
|
+
# their behavior off of this.
|
6
|
+
module SafeYAML
|
7
|
+
YAML_ENGINE = defined?(YAML::ENGINE) ? YAML::ENGINE.yamler : (defined?(Psych) && YAML == Psych ? "psych" : "syck")
|
8
|
+
end
|
9
|
+
|
10
|
+
require "safe_yaml/libyaml_checker"
|
11
|
+
require "safe_yaml/deep"
|
12
|
+
require "safe_yaml/parse/hexadecimal"
|
13
|
+
require "safe_yaml/parse/sexagesimal"
|
14
|
+
require "safe_yaml/parse/date"
|
15
|
+
require "safe_yaml/transform/transformation_map"
|
16
|
+
require "safe_yaml/transform/to_boolean"
|
17
|
+
require "safe_yaml/transform/to_date"
|
18
|
+
require "safe_yaml/transform/to_float"
|
19
|
+
require "safe_yaml/transform/to_integer"
|
20
|
+
require "safe_yaml/transform/to_nil"
|
21
|
+
require "safe_yaml/transform/to_symbol"
|
22
|
+
require "safe_yaml/transform"
|
23
|
+
require "safe_yaml/resolver"
|
24
|
+
require "safe_yaml/syck_hack" if SafeYAML::YAML_ENGINE == "syck" && defined?(JRUBY_VERSION)
|
25
|
+
|
26
|
+
module SafeYAML
|
27
|
+
MULTI_ARGUMENT_YAML_LOAD = YAML.method(:load).arity != 1
|
28
|
+
|
29
|
+
DEFAULT_OPTIONS = Deep.freeze({
|
30
|
+
:default_mode => nil,
|
31
|
+
:suppress_warnings => false,
|
32
|
+
:deserialize_symbols => false,
|
33
|
+
:whitelisted_tags => [],
|
34
|
+
:custom_initializers => {},
|
35
|
+
:raise_on_unknown_tag => false
|
36
|
+
})
|
37
|
+
|
38
|
+
OPTIONS = Deep.copy(DEFAULT_OPTIONS)
|
39
|
+
|
40
|
+
PREDEFINED_TAGS = {}
|
41
|
+
|
42
|
+
if YAML_ENGINE == "syck"
|
43
|
+
YAML.tagged_classes.each do |tag, klass|
|
44
|
+
PREDEFINED_TAGS[klass] = tag
|
45
|
+
end
|
46
|
+
|
47
|
+
else
|
48
|
+
# Special tags appear to be hard-coded in Psych:
|
49
|
+
# https://github.com/tenderlove/psych/blob/v1.3.4/lib/psych/visitors/to_ruby.rb
|
50
|
+
# Fortunately, there aren't many that SafeYAML doesn't already support.
|
51
|
+
PREDEFINED_TAGS.merge!({
|
52
|
+
Exception => "!ruby/exception",
|
53
|
+
Range => "!ruby/range",
|
54
|
+
Regexp => "!ruby/regexp",
|
55
|
+
})
|
56
|
+
end
|
57
|
+
|
58
|
+
Deep.freeze(PREDEFINED_TAGS)
|
59
|
+
|
60
|
+
module_function
|
61
|
+
|
62
|
+
def restore_defaults!
|
63
|
+
OPTIONS.clear.merge!(Deep.copy(DEFAULT_OPTIONS))
|
64
|
+
end
|
65
|
+
|
66
|
+
def tag_safety_check!(tag, options)
|
67
|
+
return if tag.nil? || tag == "!"
|
68
|
+
if options[:raise_on_unknown_tag] && !options[:whitelisted_tags].include?(tag) && !tag_is_explicitly_trusted?(tag)
|
69
|
+
raise "Unknown YAML tag '#{tag}'"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def whitelist!(*classes)
|
74
|
+
classes.each do |klass|
|
75
|
+
whitelist_class!(klass)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def whitelist_class!(klass)
|
80
|
+
raise "#{klass} not a Class" unless klass.is_a?(::Class)
|
81
|
+
|
82
|
+
klass_name = klass.name
|
83
|
+
raise "#{klass} cannot be anonymous" if klass_name.nil? || klass_name.empty?
|
84
|
+
|
85
|
+
# Whitelist any built-in YAML tags supplied by Syck or Psych.
|
86
|
+
predefined_tag = PREDEFINED_TAGS[klass]
|
87
|
+
if predefined_tag
|
88
|
+
OPTIONS[:whitelisted_tags] << predefined_tag
|
89
|
+
return
|
90
|
+
end
|
91
|
+
|
92
|
+
# Exception is exceptional (har har).
|
93
|
+
tag_class = klass < Exception ? "exception" : "object"
|
94
|
+
|
95
|
+
tag_prefix = case YAML_ENGINE
|
96
|
+
when "psych" then "!ruby/#{tag_class}"
|
97
|
+
when "syck" then "tag:ruby.yaml.org,2002:#{tag_class}"
|
98
|
+
else raise "unknown YAML_ENGINE #{YAML_ENGINE}"
|
99
|
+
end
|
100
|
+
OPTIONS[:whitelisted_tags] << "#{tag_prefix}:#{klass_name}"
|
101
|
+
end
|
102
|
+
|
103
|
+
if YAML_ENGINE == "psych"
|
104
|
+
def tag_is_explicitly_trusted?(tag)
|
105
|
+
false
|
106
|
+
end
|
107
|
+
|
108
|
+
else
|
109
|
+
TRUSTED_TAGS = Set.new([
|
110
|
+
"tag:yaml.org,2002:binary",
|
111
|
+
"tag:yaml.org,2002:bool#no",
|
112
|
+
"tag:yaml.org,2002:bool#yes",
|
113
|
+
"tag:yaml.org,2002:float",
|
114
|
+
"tag:yaml.org,2002:float#fix",
|
115
|
+
"tag:yaml.org,2002:int",
|
116
|
+
"tag:yaml.org,2002:map",
|
117
|
+
"tag:yaml.org,2002:null",
|
118
|
+
"tag:yaml.org,2002:seq",
|
119
|
+
"tag:yaml.org,2002:str",
|
120
|
+
"tag:yaml.org,2002:timestamp",
|
121
|
+
"tag:yaml.org,2002:timestamp#ymd"
|
122
|
+
]).freeze
|
123
|
+
|
124
|
+
def tag_is_explicitly_trusted?(tag)
|
125
|
+
TRUSTED_TAGS.include?(tag)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
if SafeYAML::YAML_ENGINE == "psych"
|
130
|
+
require "safe_yaml/psych_handler"
|
131
|
+
require "safe_yaml/psych_resolver"
|
132
|
+
require "safe_yaml/safe_to_ruby_visitor"
|
133
|
+
|
134
|
+
def self.load(yaml, filename=nil, options={})
|
135
|
+
# If the user hasn't whitelisted any tags, we can go with this implementation which is
|
136
|
+
# significantly faster.
|
137
|
+
if (options && options[:whitelisted_tags] || SafeYAML::OPTIONS[:whitelisted_tags]).empty?
|
138
|
+
safe_handler = SafeYAML::PsychHandler.new(options) do |result|
|
139
|
+
return result
|
140
|
+
end
|
141
|
+
arguments_for_parse = [yaml]
|
142
|
+
arguments_for_parse << filename if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
|
143
|
+
Psych::Parser.new(safe_handler).parse(*arguments_for_parse)
|
144
|
+
return safe_handler.result
|
145
|
+
|
146
|
+
else
|
147
|
+
safe_resolver = SafeYAML::PsychResolver.new(options)
|
148
|
+
tree = SafeYAML::MULTI_ARGUMENT_YAML_LOAD ?
|
149
|
+
Psych.parse(yaml, filename) :
|
150
|
+
Psych.parse(yaml)
|
151
|
+
return safe_resolver.resolve_node(tree)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.load_file(filename, options={})
|
156
|
+
if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
|
157
|
+
File.open(filename, 'r:bom|utf-8') { |f| self.load(f, filename, options) }
|
158
|
+
|
159
|
+
else
|
160
|
+
# Ruby pukes on 1.9.2 if we try to open an empty file w/ 'r:bom|utf-8';
|
161
|
+
# so we'll not specify those flags here. This mirrors the behavior for
|
162
|
+
# unsafe_load_file so it's probably preferable anyway.
|
163
|
+
self.load File.open(filename), nil, options
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
else
|
168
|
+
require "safe_yaml/syck_resolver"
|
169
|
+
require "safe_yaml/syck_node_monkeypatch"
|
170
|
+
|
171
|
+
def self.load(yaml, options={})
|
172
|
+
resolver = SafeYAML::SyckResolver.new(SafeYAML::OPTIONS.merge(options || {}))
|
173
|
+
tree = YAML.parse(yaml)
|
174
|
+
return resolver.resolve_node(tree)
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.load_file(filename, options={})
|
178
|
+
File.open(filename) { |f| self.load(f, options) }
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module SafeYAML
|
4
|
+
class Parse
|
5
|
+
class Date
|
6
|
+
# This one's easy enough :)
|
7
|
+
DATE_MATCHER = /\A(\d{4})-(\d{2})-(\d{2})\Z/.freeze
|
8
|
+
|
9
|
+
# This unbelievable little gem is taken basically straight from the YAML spec, but made
|
10
|
+
# slightly more readable (to my poor eyes at least) to me:
|
11
|
+
# http://yaml.org/type/timestamp.html
|
12
|
+
TIME_MATCHER = /\A\d{4}-\d{1,2}-\d{1,2}(?:[Tt]|\s+)\d{1,2}:\d{2}:\d{2}(?:\.\d*)?\s*(?:Z|[-+]\d{1,2}(?::?\d{2})?)?\Z/.freeze
|
13
|
+
|
14
|
+
SECONDS_PER_DAY = 60 * 60 * 24
|
15
|
+
MICROSECONDS_PER_SECOND = 1000000
|
16
|
+
|
17
|
+
# So this is weird. In Ruby 1.8.7, the DateTime#sec_fraction method returned fractional
|
18
|
+
# seconds in units of DAYS for some reason. In 1.9.2, they changed the units -- much more
|
19
|
+
# reasonably -- to seconds.
|
20
|
+
SEC_FRACTION_MULTIPLIER = RUBY_VERSION == "1.8.7" ? (SECONDS_PER_DAY * MICROSECONDS_PER_SECOND) : MICROSECONDS_PER_SECOND
|
21
|
+
|
22
|
+
# The DateTime class has a #to_time method in Ruby 1.9+;
|
23
|
+
# Before that we'll just need to convert DateTime to Time ourselves.
|
24
|
+
TO_TIME_AVAILABLE = DateTime.instance_methods.include?(:to_time)
|
25
|
+
|
26
|
+
def self.value(value)
|
27
|
+
d = DateTime.parse(value)
|
28
|
+
|
29
|
+
return d.to_time if TO_TIME_AVAILABLE
|
30
|
+
|
31
|
+
usec = d.sec_fraction * SEC_FRACTION_MULTIPLIER
|
32
|
+
time = Time.utc(d.year, d.month, d.day, d.hour, d.min, d.sec, usec) - (d.offset * SECONDS_PER_DAY)
|
33
|
+
time.getlocal
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|