-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
loader.cr
235 lines (193 loc) · 7.65 KB
/
loader.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
class Time::Location
@@location_cache = {} of String => NamedTuple(time: Time, location: Location)
# `InvalidTZDataError` is raised if a zoneinfo file contains invalid
# time zone data.
#
# Details on the exact cause can be found in the error message.
class InvalidTZDataError < Exception
def self.initialize(message : String? = "Malformed time zone information", cause : Exception? = nil)
super(message, cause)
end
end
# :nodoc:
def self.load?(name : String, sources : Enumerable(String)) : Time::Location?
if source = find_zoneinfo_file(name, sources)
load_from_dir_or_zip(name, source)
end
end
# :nodoc:
def self.load(name : String, sources : Enumerable(String)) : Time::Location?
if source = find_zoneinfo_file(name, sources)
load_from_dir_or_zip(name, source) || raise InvalidLocationNameError.new(name, source)
end
end
# :nodoc:
def self.load_from_dir_or_zip(name : String, source : String) : Time::Location?
if source.ends_with?(".zip")
open_file_cached(name, source) do |file|
read_zip_file(name, file) do |io|
read_zoneinfo(name, io)
end
end
else
path = File.join(source, name)
open_file_cached(name, path) do |file|
read_zoneinfo(name, file)
end
end
end
private def self.open_file_cached(name : String, path : String)
return nil unless File.exists?(path)
mtime = File.info(path).modification_time
if (cache = @@location_cache[name]?) && cache[:time] == mtime
return cache[:location]
else
File.open(path) do |file|
location = yield file
if location
@@location_cache[name] = {time: mtime, location: location}
return location
end
end
end
end
# :nodoc:
def self.find_zoneinfo_file(name : String, sources : Enumerable(String)) : String?
sources.each do |source|
if source.ends_with?(".zip")
path = source
else
path = File.join(source, name)
end
return source if File.exists?(path) && File.file?(path) && File.readable?(path)
end
end
# Parse "zoneinfo" time zone file.
# This is the standard file format used by most operating systems.
# See https://data.iana.org/time-zones/tz-link.html, https://github.com/eggert/tz, tzfile(5)
# :nodoc:
def self.read_zoneinfo(location_name : String, io : IO) : Time::Location
raise InvalidTZDataError.new unless io.read_string(4) == "TZif"
# 1-byte version, then 15 bytes of padding
version = io.read_byte
raise InvalidTZDataError.new unless version.in?(0_u8, '2'.ord, '3'.ord)
io.skip(15)
# six big-endian 32-bit integers:
# number of UTC/local indicators
# number of standard/wall indicators
# number of leap seconds
# number of transition times
# number of local time zones
# number of characters of time zone abbrev strings
num_utc_local = read_int32(io)
num_std_wall = read_int32(io)
num_leap_seconds = read_int32(io)
num_transitions = read_int32(io)
num_local_time_zones = read_int32(io)
abbrev_length = read_int32(io)
transitionsdata = read_buffer(io, num_transitions * 4)
# Time zone indices for transition times.
transition_indexes = Bytes.new(num_transitions)
io.read_fully(transition_indexes)
zonedata = read_buffer(io, num_local_time_zones * 6)
abbreviations = read_buffer(io, abbrev_length)
leap_second_time_pairs = Bytes.new(num_leap_seconds)
io.read_fully(leap_second_time_pairs)
isstddata = Bytes.new(num_std_wall)
io.read_fully(isstddata)
isutcdata = Bytes.new(num_utc_local)
io.read_fully(isutcdata)
# If version == 2 or 3, the entire file repeats, this time using
# 8-byte ints for txtimes and leap seconds.
# We won't need those until 2106.
zones = Array(Zone).new(num_local_time_zones) do
offset = read_int32(zonedata)
is_dst = zonedata.read_byte != 0_u8
name_idx = zonedata.read_byte
raise InvalidTZDataError.new unless name_idx && name_idx < abbreviations.size
abbreviations.pos = name_idx
name = abbreviations.gets(Char::ZERO, chomp: true)
raise InvalidTZDataError.new unless name
Zone.new(name, offset, is_dst)
end
transitions = Array(ZoneTransition).new(num_transitions) do |transition_id|
time = read_int32(transitionsdata).to_i64
zone_idx = transition_indexes[transition_id]
raise InvalidTZDataError.new unless zone_idx < zones.size
isstd = !isstddata[transition_id]?.in?(nil, 0_u8)
isutc = !isstddata[transition_id]?.in?(nil, 0_u8)
ZoneTransition.new(time, zone_idx, isstd, isutc)
end
new(location_name, zones, transitions)
rescue exc : IO::Error
raise InvalidTZDataError.new(cause: exc)
end
private def self.read_int32(io : IO)
io.read_bytes(Int32, IO::ByteFormat::BigEndian)
end
private def self.read_buffer(io : IO, size : Int)
buffer = Bytes.new(size)
io.read_fully(buffer)
IO::Memory.new(buffer)
end
# :nodoc:
CENTRAL_DIRECTORY_HEADER_SIGNATURE = 0x02014b50
# :nodoc:
END_OF_CENTRAL_DIRECTORY_HEADER_SIGNATURE = 0x06054b50
# :nodoc:
ZIP_TAIL_SIZE = 22
# :nodoc:
LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50
# :nodoc:
COMPRESSION_METHOD_UNCOMPRESSED = 0_i16
# This method loads an entry from an uncompressed zip file.
# See http://www.onicos.com/staff/iz/formats/zip.html for ZIP format layout
private def self.read_zip_file(name : String, file : File)
file.seek -ZIP_TAIL_SIZE, IO::Seek::End
if file.read_bytes(Int32, IO::ByteFormat::LittleEndian) != END_OF_CENTRAL_DIRECTORY_HEADER_SIGNATURE
raise InvalidTZDataError.new("Corrupt ZIP file #{file.path}")
end
file.skip 6
num_entries = file.read_bytes(Int16, IO::ByteFormat::LittleEndian)
file.skip 4
file.pos = file.read_bytes(Int32, IO::ByteFormat::LittleEndian)
num_entries.times do
break if file.read_bytes(Int32, IO::ByteFormat::LittleEndian) != CENTRAL_DIRECTORY_HEADER_SIGNATURE
file.skip 6
compression_method = file.read_bytes(Int16, IO::ByteFormat::LittleEndian)
file.skip 12
uncompressed_size = file.read_bytes(Int32, IO::ByteFormat::LittleEndian)
filename_length = file.read_bytes(Int16, IO::ByteFormat::LittleEndian)
extra_field_length = file.read_bytes(Int16, IO::ByteFormat::LittleEndian)
file_comment_length = file.read_bytes(Int16, IO::ByteFormat::LittleEndian)
file.skip 8
local_file_header_pos = file.read_bytes(Int32, IO::ByteFormat::LittleEndian)
filename = file.read_string(filename_length)
unless filename == name
file.skip extra_field_length + file_comment_length
next
end
unless compression_method == COMPRESSION_METHOD_UNCOMPRESSED
raise InvalidTZDataError.new("Unsupported compression in ZIP file: #{file.path}")
end
file.pos = local_file_header_pos
unless file.read_bytes(Int32, IO::ByteFormat::LittleEndian) == LOCAL_FILE_HEADER_SIGNATURE
raise InvalidTZDataError.new("Invalid ZIP file: #{file.path}")
end
file.skip 4
unless file.read_bytes(Int16, IO::ByteFormat::LittleEndian) == COMPRESSION_METHOD_UNCOMPRESSED
raise InvalidTZDataError.new("Invalid ZIP file: #{file.path}")
end
file.skip 16
unless file.read_bytes(Int16, IO::ByteFormat::LittleEndian) == filename_length
raise InvalidTZDataError.new("Invalid ZIP file: #{file.path}")
end
extra_field_length = file.read_bytes(Int16, IO::ByteFormat::LittleEndian)
unless file.gets(filename_length) == name
raise InvalidTZDataError.new("Invalid ZIP file: #{file.path}")
end
file.skip extra_field_length
return yield file
end
end
end