123456789_123456789_123456789_123456789_123456789_

Class: TZInfo::DataSources::ZoneinfoReader

Relationships & Source Files
Inherits: Object
Defined in: lib/tzinfo/data_sources/zoneinfo_reader.rb

Overview

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

Constant Summary

Class Method Summary

Instance Method Summary

Constructor Details

.new(posix_tz_parser, string_deduper) ⇒ ZoneinfoReader

Initializes a new ZoneinfoReader.

Parameters:

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 25

def initialize(posix_tz_parser, string_deduper)
  @posix_tz_parser = posix_tz_parser
  @string_deduper = string_deduper
end

Instance Method Details

#apply_rules_with_transitions(file, transitions, offsets, rules) (private)

Apply the rules from the TZ string when there were defined transitions. Checks for a matching offset with the last transition. Redefines the last transition if required and if the rules don't specific a constant offset, generates transitions until 100 years into the future (at the time of loading zoneinfo_reader.rb).

Parameters:

Raises:

  • (InvalidZoneinfoFile)

    if the first offset does not match the rules.

  • (InvalidZoneinfoFile)

    if the previous offset of the first generated transition does not match the offset of the last defined transition.

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 311

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

#apply_rules_without_transitions(file, first_offset, rules) ⇒ Object (private)

Apply the rules from the TZ string when there were no defined transitions. Checks for a matching offset. Returns the rules-based constant offset or generates transitions from 1970 until 100 years into the future (at the time of loading zoneinfo_reader.rb).

Parameters:

Returns:

Raises:

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 199

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

#check_read(file, bytes) ⇒ String (private)

Reads the given number of bytes from the given file and checks that the correct number of bytes could be read.

Parameters:

  • file (IO)

    the file to read from.

  • bytes (Integer)

    the number of bytes to read.

Returns:

  • (String)

    the bytes that were read.

Raises:

  • (InvalidZoneinfoFile)

    if the number of bytes available didn't match the number requested.

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 76

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

#derive_offsets(transitions, offsets) ⇒ Integer (private)

Zoneinfo files don't include the offset from standard time (std_offset) for DST periods. Derive the base offset (base_utc_offset) where DST is observed from either the previous or next non-DST period.

Parameters:

  • transitions (Array<Hash>)

    an Array of transition hashes.

  • offsets (Array<Hash>)

    an Array of offset hashes.

Returns:

  • (Integer)

    the index of the offset to be used prior to the first transition.

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 94

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

#find_existing_offset(offsets, offset) ⇒ TimezoneOffset (private)

Finds an offset that is equivalent to the one specified in the given Array. Matching is performed with TimezoneOffset#==.

Parameters:

Returns:

  • (TimezoneOffset)

    the matching offset from offsets or nil if not found.

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 233

def find_existing_offset(offsets, offset)
  offsets.find {|o| o == offset }
end

#make_signed_int32(long) ⇒ Integer (private)

Translates an unsigned 32-bit integer (as returned by unpack) to signed 32-bit.

Parameters:

  • long (Integer)

    an unsigned 32-bit integer.

Returns:

  • (Integer)

    long translated to signed 32-bit.

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 52

def make_signed_int32(long)
  long >= 0x80000000 ? long - 0x100000000 : long
end

#make_signed_int64(high, low) ⇒ Integer (private)

Translates a pair of unsigned 32-bit integers (as returned by unpack, most significant first) to a signed 64-bit integer.

Parameters:

  • high (Integer)

    the most significant 32-bits.

  • low (Integer)

    the least significant 32-bits.

Returns:

  • (Integer)

    high and low combined and translated to signed 64-bit.

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 63

def make_signed_int64(high, low)
  unsigned = (high << 32) | low
  unsigned >= 0x8000000000000000 ? unsigned - 0x10000000000000000 : unsigned
end

#offset_matches_rule?(offset, rule_offset) ⇒ Boolean (private)

Determines if the offset from a transition matches the offset from a rule. This is a looser match than equality, not requiring that the base_utc_offset and std_offset both match (which have to be derived for transitions, but are known for rules.

Parameters:

Returns:

  • (Boolean)

    whether the offsets match.

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 179

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

#parse(file) ⇒ Object (private)

Parses a zoneinfo file and returns either a ::TZInfo::TimezoneOffset that is constantly observed or an Array of ::TZInfo::TimezoneTransitions.

Parameters:

  • file (IO)

    the file to be read.

Returns:

Raises:

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 343

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

#read(file_path) ⇒ Object

Reads a zoneinfo structure from the given path. Returns either a ::TZInfo::TimezoneOffset that is constantly observed or an Array ::TZInfo::TimezoneTransitions.

Parameters:

  • file_path (String)

    the path of a zoneinfo file.

Returns:

Raises:

  • (SecurityError)

    if safe mode is enabled and file_path is tainted.

  • (InvalidZoneinfoFile)

    if file_path` does not refer to a valid zoneinfo file.

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 41

def read(file_path)
  File.open(file_path, 'rb') { |file| parse(file) }
end

#replace_with_existing_offsets(offsets, annual_rules) ⇒ AnnualRules (private)

Returns a new ::TZInfo::AnnualRules instance with standard and daylight savings offsets replaced with equivalents from an array. This reduces the memory requirement for loaded time zones by reusing offsets for rule-generated transitions.

Parameters:

Returns:

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 250

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

#validate_and_fix_last_defined_transition_offset(file, last_defined, first_rule_offset) ⇒ TimezoneTransition (private)

Validates the offset indicated to be observed by the rules before the first generated transition against the offset of the last defined transition.

Fix the last defined transition if it differ on just base/std offsets (which are derived). Raise an error if the observed UTC offset or abbreviations differ.

Parameters:

  • file (IO)

    the file being processed.

  • last_defined (TimezoneTransition)

    the last defined transition in the file.

  • first_rule_offset (TimezoneOffset)

    the offset the rules indicate is observed prior to the first rules generated transition.

Returns:

  • (TimezoneTransition)

    the last defined transition (either the original instance or a replacement).

Raises:

[ GitHub ]

  
# File 'lib/tzinfo/data_sources/zoneinfo_reader.rb', line 278

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