class TZInfo::DataSources::ZoneinfoReader

Experimental RBS support (using type sampling data from the type_fusion project).

# sig/tzinfo/data_sources/zoneinfo_reader.rbs

class TZInfo::DataSources::ZoneinfoReader
  def make_signed_int64: (Integer high, Integer low) -> Integer
end

:nodoc:
Reads compiled zoneinfo TZif (0, 2 or 3) files.

def apply_rules_with_transitions(file, transitions, offsets, rules)

Raises:
  • (InvalidZoneinfoFile) - if the previous offset of the first
  • (InvalidZoneinfoFile) - if the first offset does not match the

Parameters:
  • rules (Object) -- a {TimezoneOffset} specifying a constant offset or
  • offsets (Array) -- the offsets used by the defined
  • transitions (Array) -- the defined transitions.
  • file (IO) -- the file being processed.
def apply_rules_with_transitions(file, transitions, offsets, rules)
  last_defined = transitions[-1]
  if rules.kind_of?(TimezoneOffset)
    transitions[-1] = validate_and_fix_last_defined_transition_offset(file, last_defined, rules)
  else
    last_year = last_defined.local_end_at.to_time.year
    if last_year <= GENERATE_UP_TO
      rules = replace_with_existing_offsets(offsets, rules)
      generated = rules.transitions(last_year).find_all do |t|
        t.timestamp_value > last_defined.timestamp_value && !offset_matches_rule?(last_defined.offset, t.offset)
      end
      generated += (last_year + 1).upto(GENERATE_UP_TO).flat_map {|y| rules.transitions(y) }
      unless generated.empty?
        transitions[-1] = validate_and_fix_last_defined_transition_offset(file, last_defined, generated[0].previous_offset)
        transitions.concat(generated)
      end
    end
  end
end

def apply_rules_without_transitions(file, first_offset, rules)

Raises:
  • (InvalidZoneinfoFile) - if the first offset does not match the

Returns:
  • (Object) - either a {TimezoneOffset} or an `Array` of

Parameters:
  • rules (Object) -- a {TimezoneOffset} specifying a constant offset or
  • first_offset (TimezoneOffset) -- the first offset included in the
  • file (IO) -- the file being processed.
def apply_rules_without_transitions(file, first_offset, rules)
  if rules.kind_of?(TimezoneOffset)
    unless offset_matches_rule?(first_offset, rules)
      raise InvalidZoneinfoFile, "Constant offset POSIX-style TZ string does not match constant offset in file '#{file.path}'."
    end
    rules
  else
    transitions = 1970.upto(GENERATE_UP_TO).flat_map {|y| rules.transitions(y) }
    first_transition = transitions[0]
    unless offset_matches_rule?(first_offset, first_transition.previous_offset)
      # Not transitioning from the designated first offset.
      if offset_matches_rule?(first_offset, first_transition.offset)
        # Skip an unnecessary transition to the first offset.
        transitions.shift
      else
        # The initial offset doesn't match the ongoing rules. Replace the
        # previous offset of the first transition.
        transitions[0] = TimezoneTransition.new(first_transition.offset, first_offset, first_transition.timestamp_value)
      end
    end
    transitions
  end
end

def check_read(file, bytes)

Raises:
  • (InvalidZoneinfoFile) - if the number of bytes available didn't

Returns:
  • (String) - the bytes that were read.

Parameters:
  • bytes (Integer) -- the number of bytes to read.
  • file (IO) -- the file to read from.
def check_read(file, bytes)
  result = file.read(bytes)
  unless result && result.length == bytes
    raise InvalidZoneinfoFile, "Expected #{bytes} bytes reading '#{file.path}', but got #{result ? result.length : 0} bytes"
  end
  result
end

def derive_offsets(transitions, offsets)

Returns:
  • (Integer) - the index of the offset to be used prior to the first

Parameters:
  • offsets (Array) -- an `Array` of offset hashes.
  • transitions (Array) -- an `Array` of transition hashes.
def derive_offsets(transitions, offsets)
  # The first non-DST offset (if there is one) is the offset observed
  # before the first transition. Fall back to the first DST offset if
  # there are no non-DST offsets.
  first_non_dst_offset_index = offsets.index {|o| !o[:is_dst] }
  first_offset_index = first_non_dst_offset_index || 0
  return first_offset_index if transitions.empty?
  # Determine the base_utc_offset of the next non-dst offset at each transition.
  base_utc_offset_from_next = nil
  transitions.reverse_each do |transition|
    offset = offsets[transition[:offset]]
    if offset[:is_dst]
      transition[:base_utc_offset_from_next] = base_utc_offset_from_next if base_utc_offset_from_next
    else
      base_utc_offset_from_next = offset[:observed_utc_offset]
    end
  end
  base_utc_offset_from_previous = first_non_dst_offset_index ? offsets[first_non_dst_offset_index][:observed_utc_offset] : nil
  defined_offsets = {}
  transitions.each do |transition|
    offset_index = transition[:offset]
    offset = offsets[offset_index]
    observed_utc_offset = offset[:observed_utc_offset]
    if offset[:is_dst]
      base_utc_offset_from_next = transition[:base_utc_offset_from_next]
      difference_to_previous = (observed_utc_offset - (base_utc_offset_from_previous || observed_utc_offset)).abs
      difference_to_next = (observed_utc_offset - (base_utc_offset_from_next || observed_utc_offset)).abs
      base_utc_offset = if difference_to_previous == 3600
        base_utc_offset_from_previous
      elsif difference_to_next == 3600
        base_utc_offset_from_next
      elsif difference_to_previous > 0 && difference_to_next > 0
        difference_to_previous < difference_to_next ? base_utc_offset_from_previous : base_utc_offset_from_next
      elsif difference_to_previous > 0
        base_utc_offset_from_previous
      elsif difference_to_next > 0
        base_utc_offset_from_next
      else
        # No difference, assume a 1 hour offset from standard time.
        observed_utc_offset - 3600
      end
      if !offset[:base_utc_offset]
        offset[:base_utc_offset] = base_utc_offset
        defined_offsets[offset] = offset_index
      elsif offset[:base_utc_offset] != base_utc_offset
        # An earlier transition has already derived a different
        # base_utc_offset. Define a new offset or reuse an existing identically
        # defined offset.
        new_offset = offset.dup
        new_offset[:base_utc_offset] = base_utc_offset
        offset_index = defined_offsets[new_offset]
        unless offset_index
          offsets << new_offset
          offset_index = offsets.length - 1
          defined_offsets[new_offset] = offset_index
        end
        transition[:offset] = offset_index
      end
    else
      base_utc_offset_from_previous = observed_utc_offset
    end
  end
  first_offset_index
end

def find_existing_offset(offsets, offset)

Returns:
  • (TimezoneOffset) - the matching offset from `offsets` or `nil`

Parameters:
  • offset (TimezoneOffset) -- the offset to search for.
  • offsets (Array) -- an `Array` to search.
def find_existing_offset(offsets, offset)
  offsets.find {|o| o == offset }
end

def initialize(posix_tz_parser, string_deduper)

Parameters:
  • string_deduper (StringDeduper) -- a {StringDeduper} instance to use
  • posix_tz_parser (PosixTimeZoneParser) -- a {PosixTimeZoneParser}
def initialize(posix_tz_parser, string_deduper)
  @posix_tz_parser = posix_tz_parser
  @string_deduper = string_deduper
end

def make_signed_int32(long)

Returns:
  • (Integer) - {long} translated to signed 32-bit.

Parameters:
  • long (Integer) -- an unsigned 32-bit integer.
def make_signed_int32(long)
  long >= 0x80000000 ? long - 0x100000000 : long
end

def make_signed_int64(high, low)

Experimental RBS support (using type sampling data from the type_fusion project).

def make_signed_int64: (Integer high, Integer low) -> Integer

This signature was generated using 2 samples from 1 application.

Returns:
  • (Integer) - {high} and {low} combined and translated to signed

Parameters:
  • low (Integer) -- the least significant 32-bits.
  • high (Integer) -- the most significant 32-bits.
def make_signed_int64(high, low)
  unsigned = (high << 32) | low
  unsigned >= 0x8000000000000000 ? unsigned - 0x10000000000000000 : unsigned
end

def offset_matches_rule?(offset, rule_offset)

Returns:
  • (Boolean) - whether the offsets match.

Parameters:
  • rule_offset (TimezoneOffset) -- an offset from a rule.
  • offset (TimezoneOffset) -- an offset from a transition.
def offset_matches_rule?(offset, rule_offset)
  offset.observed_utc_offset == rule_offset.observed_utc_offset &&
    offset.dst? == rule_offset.dst? &&
    offset.abbreviation == rule_offset.abbreviation
end

def parse(file)

Raises:
  • (InvalidZoneinfoFile) - if the file is not a valid zoneinfo file.

Returns:
  • (Object) - either a {TimezoneOffset} or an `Array` of

Parameters:
  • file (IO) -- the file to be read.
def parse(file)
  magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
    check_read(file, 44).unpack('a4 a x15 NNNNNN')
  if magic != 'TZif'
    raise InvalidZoneinfoFile, "The file '#{file.path}' does not start with the expected header."
  end
  if version == '2' || version == '3'
    # Skip the first 32-bit section and read the header of the second 64-bit section
    file.seek(timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 + ttisstdcnt + ttisutccnt, IO::SEEK_CUR)
    prev_version = version
    magic, version, ttisutccnt, ttisstdcnt, leapcnt, timecnt, typecnt, charcnt =
      check_read(file, 44).unpack('a4 a x15 NNNNNN')
    unless magic == 'TZif' && (version == prev_version)
      raise InvalidZoneinfoFile, "The file '#{file.path}' contains an invalid 64-bit section header."
    end
    using_64bit = true
  elsif version != '3' && version != '2' && version != "\0"
    raise InvalidZoneinfoFile, "The file '#{file.path}' contains a version of the zoneinfo format that is not currently supported."
  else
    using_64bit = false
  end
  unless leapcnt == 0
    raise InvalidZoneinfoFile, "The file '#{file.path}' contains leap second data. TZInfo requires zoneinfo files that omit leap seconds."
  end
  transitions = if using_64bit
    timecnt.times.map do |i|
      high, low = check_read(file, 8).unpack('NN'.freeze)
      transition_time = make_signed_int64(high, low)
      {at: transition_time}
    end
  else
    timecnt.times.map do |i|
      transition_time = make_signed_int32(check_read(file, 4).unpack('N'.freeze)[0])
      {at: transition_time}
    end
  end
  check_read(file, timecnt).unpack('C*'.freeze).each_with_index do |localtime_type, i|
    raise InvalidZoneinfoFile, "Invalid offset referenced by transition in file '#{file.path}'." if localtime_type >= typecnt
    transitions[i][:offset] = localtime_type
  end
  offsets = typecnt.times.map do |i|
    gmtoff, isdst, abbrind = check_read(file, 6).unpack('NCC'.freeze)
    gmtoff = make_signed_int32(gmtoff)
    isdst = isdst == 1
    {observed_utc_offset: gmtoff, is_dst: isdst, abbr_index: abbrind}
  end
  abbrev = check_read(file, charcnt)
  if using_64bit
    # Skip to the POSIX-style TZ string.
    file.seek(ttisstdcnt + ttisutccnt, IO::SEEK_CUR) # + leapcnt * 8, but leapcnt is checked above and guaranteed to be 0.
    tz_string_start = check_read(file, 1)
    raise InvalidZoneinfoFile, "Expected newline starting POSIX-style TZ string in file '#{file.path}'." unless tz_string_start == "\n"
    tz_string = file.readline("\n").force_encoding(Encoding::UTF_8)
    raise InvalidZoneinfoFile, "Expected newline ending POSIX-style TZ string in file '#{file.path}'." unless tz_string.chomp!("\n")
    begin
      rules = @posix_tz_parser.parse(tz_string)
    rescue InvalidPosixTimeZone => e
      raise InvalidZoneinfoFile, "Failed to parse POSIX-style TZ string in file '#{file.path}': #{e}"
    end
  else
    rules = nil
  end
  # Derive the offsets from standard time (std_offset).
  first_offset_index = derive_offsets(transitions, offsets)
  offsets = offsets.map do |o|
    observed_utc_offset = o[:observed_utc_offset]
    base_utc_offset = o[:base_utc_offset]
    if base_utc_offset
      # DST offset with base_utc_offset derived by derive_offsets.
      std_offset = observed_utc_offset - base_utc_offset
    elsif o[:is_dst]
      # DST offset unreferenced by a transition (offset in use before the
      # first transition). No derived base UTC offset, so assume 1 hour
      # DST.
      base_utc_offset = observed_utc_offset - 3600
      std_offset = 3600
    else
      # Non-DST offset.
      base_utc_offset = observed_utc_offset
      std_offset = 0
    end
    abbrev_start = o[:abbr_index]
    raise InvalidZoneinfoFile, "Abbreviation index is out of range in file '#{file.path}'." unless abbrev_start < abbrev.length
    abbrev_end = abbrev.index("\0", abbrev_start)
    raise InvalidZoneinfoFile, "Missing abbreviation null terminator in file '#{file.path}'." unless abbrev_end
    abbr = @string_deduper.dedupe(RubyCoreSupport.untaint(abbrev[abbrev_start...abbrev_end].force_encoding(Encoding::UTF_8)))
    TimezoneOffset.new(base_utc_offset, std_offset, abbr)
  end
  first_offset = offsets[first_offset_index]
  if transitions.empty?
    if rules
      apply_rules_without_transitions(file, first_offset, rules)
    else
      first_offset
    end
  else
    previous_offset = first_offset
    previous_at = nil
    transitions = transitions.map do |t|
      offset = offsets[t[:offset]]
      at = t[:at]
      raise InvalidZoneinfoFile, "Transition at #{at} is not later than the previous transition at #{previous_at} in file '#{file.path}'." if previous_at && previous_at >= at
      tt = TimezoneTransition.new(offset, previous_offset, at)
      previous_offset = offset
      previous_at = at
      tt
    end
    apply_rules_with_transitions(file, transitions, offsets, rules) if rules
    transitions
  end
end

def read(file_path)

Raises:
  • (InvalidZoneinfoFile) - if `file_path`` does not refer to a valid
  • (SecurityError) - if safe mode is enabled and `file_path` is

Returns:
  • (Object) - either a {TimezoneOffset} or an `Array` of

Parameters:
  • file_path (String) -- the path of a zoneinfo file.
def read(file_path)
  File.open(file_path, 'rb') { |file| parse(file) }
end

def replace_with_existing_offsets(offsets, annual_rules)

Returns:
  • (AnnualRules) - either a new {AnnualRules} instance with either

Parameters:
  • annual_rules (AnnualRules) -- the {AnnualRules} instance to check.
  • offsets (Array) -- an `Array` to search for
def replace_with_existing_offsets(offsets, annual_rules)
  existing_std_offset = find_existing_offset(offsets, annual_rules.std_offset)
  existing_dst_offset = find_existing_offset(offsets, annual_rules.dst_offset)
  if existing_std_offset || existing_dst_offset
    AnnualRules.new(existing_std_offset || annual_rules.std_offset, existing_dst_offset || annual_rules.dst_offset,
      annual_rules.dst_start_rule, annual_rules.dst_end_rule)
  else
    annual_rules
  end
end

def validate_and_fix_last_defined_transition_offset(file, last_defined, first_rule_offset)

Raises:
  • (InvalidZoneinfoFile) - if the offset of {last_defined} and

Returns:
  • (TimezoneTransition) - the last defined transition (either the

Parameters:
  • first_rule_offset (TimezoneOffset) -- the offset the rules indicate
  • last_defined (TimezoneTransition) -- the last defined transition in
  • file (IO) -- the file being processed.
def validate_and_fix_last_defined_transition_offset(file, last_defined, first_rule_offset)
  offset_of_last_defined = last_defined.offset
  if offset_of_last_defined == first_rule_offset
    last_defined
  else
    if offset_matches_rule?(offset_of_last_defined, first_rule_offset)
      # The same overall offset, but differing in the base or std
      # offset (which are derived). Correct by using the rule.
      TimezoneTransition.new(first_rule_offset, last_defined.previous_offset, last_defined.timestamp_value)
    else
      raise InvalidZoneinfoFile, "The first offset indicated by the POSIX-style TZ string did not match the final defined offset in file '#{file.path}'."
    end
  end
end