diff --git a/config/config.exs b/config/config.exs index bd813c5..b81979c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,6 +1,6 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. -use Mix.Config +import Config config :logger, utc_log: true config :tzdata, :autoupdate, :enabled diff --git a/lib/tzdata/data_loader.ex b/lib/tzdata/data_loader.ex index 192dd54..6d00893 100644 --- a/lib/tzdata/data_loader.ex +++ b/lib/tzdata/data_loader.ex @@ -21,7 +21,7 @@ defmodule Tzdata.DataLoader do extract(target_filename, new_dir_name) release_version = release_version_for_dir(new_dir_name) Logger.debug("Tzdata data downloaded. Release version #{release_version}.") - {:ok, content_length, release_version, new_dir_name, last_modified} + {:ok, content_length, release_version, new_dir_name, last_modified |> List.to_string()} end defp extract(filename, target_dir) do @@ -90,14 +90,14 @@ defmodule Tzdata.DataLoader do end defp content_length_from_headers(headers) do - case value_from_headers(headers, "Content-Length") do - {:ok, content_length} -> {:ok, content_length |> String.to_integer()} + case value_from_headers(headers, 'content-length') do + {:ok, content_length} -> {:ok, content_length |> List.to_integer()} {:error, reason} -> {:error, reason} end end defp last_modified_from_headers(headers) do - value_from_headers(headers, "Last-Modified") + value_from_headers(headers, 'last-modified') end defp value_from_headers(headers, key) do diff --git a/lib/tzdata/http_client/httpc.ex b/lib/tzdata/http_client/httpc.ex new file mode 100644 index 0000000..9a5fdf4 --- /dev/null +++ b/lib/tzdata/http_client/httpc.ex @@ -0,0 +1,59 @@ +defmodule Tzdata.HttpClient.Httpc do + require Logger + + @behaviour Tzdata.HTTPClient + + @impl true + def get(url, _headers, _options) when is_binary(url) do + String.to_charlist(url) |> get([], []) + end + + def get(url, _headers, _options) when is_list(url) do + request = {url, []} + + {:ok, {{_, response, _}, headers, body}} = :httpc.request(:get, request, http_options(), []) + + {:ok, {response, headers, :erlang.list_to_binary(body)}} + end + + @impl true + def head(url, _headers, _options) when is_binary(url) do + String.to_charlist(url) |> head([],[]) + end + + def head(url, _headers, _options) when is_list(url) do + request = {url, []} + + {:ok, {{_, response, _}, headers, []}} = :httpc.request(:head, request, http_options(), []) + + {:ok, {response, headers}} + end + + defp http_options() do + [{:ssl, ssl_options()}] + end + + defp ssl_options() do + local_storage = CAStore.file_path() |> String.to_charlist() + + [{:verify, :verify_peer}, + {:cacertfile, local_storage}, + {:depth, 2}, + {:customize_hostname_check, [ + {:match_fun, :public_key.pkix_verify_hostname_match_fun(:https)} + ]} + ] + end + + ## + # uses cacert file maintained elsewhere on the system + ## + defp custom_cacert(cacert) when is_binary(cacert) do + cacert |> String.to_charlist() + end + + defp custom_cacert(cacert) when is_list(cacert) do + cacert + end + +end diff --git a/lib/tzdata/period_builder.ex b/lib/tzdata/period_builder.ex index 94f2a90..3dc0be3 100644 --- a/lib/tzdata/period_builder.ex +++ b/lib/tzdata/period_builder.ex @@ -97,16 +97,17 @@ defmodule Tzdata.PeriodBuilder do def h_calc_next_zone_line(_btz_data, period, _, zone_line_tl, _) when zone_line_tl == [] do case period do nil -> [] - _ -> [ period ] + _ -> [period] end end # If there is a zone line tail, we recursively add to the list of periods with that zone line tail def h_calc_next_zone_line(btz_data, period, until_utc, zone_line_tl, letter) do tail = calc_periods(btz_data, zone_line_tl, until_utc, hd(zone_line_tl).rules, letter) + case period do nil -> tail - _ -> [ period | tail ] + _ -> [period | tail] end end @@ -194,8 +195,12 @@ defmodule Tzdata.PeriodBuilder do letter ) do until_utc = datetime_to_utc(Map.get(zone_line, :until), utc_off, std_off) - tail = calc_periods(btz_data, zone_line_tl, until_utc, Map.get(hd(zone_line_tl), :rules), letter) - if from == until_utc do # empty period may happen when 'until' of zone line coincides with end of rule + + tail = + calc_periods(btz_data, zone_line_tl, until_utc, Map.get(hd(zone_line_tl), :rules), letter) + + # empty period may happen when 'until' of zone line coincides with end of rule + if from == until_utc do tail else from_standard_time = standard_time_from_utc(from, utc_off) @@ -211,7 +216,7 @@ defmodule Tzdata.PeriodBuilder do zone_abbr: TzUtil.period_abbrevation(zone_line.format, std_off, letter) } - [ period | tail ] + [period | tail] end end @@ -286,9 +291,14 @@ defmodule Tzdata.PeriodBuilder do until_utc = datetime_to_utc(TzUtil.time_for_rule(rule, year), utc_off, std_off) # truncate end of period to within time range of zone line - until_before_lower_limit = is_integer(lower_limit) && is_integer(until_utc) && lower_limit > until_utc + until_before_lower_limit = + is_integer(lower_limit) && is_integer(until_utc) && lower_limit > until_utc + until_utc = if until_before_lower_limit, do: lower_limit, else: until_utc - last_included_rule = is_integer(upper_limit) && is_integer(until_utc) && upper_limit <= until_utc + + last_included_rule = + is_integer(upper_limit) && is_integer(until_utc) && upper_limit <= until_utc + until_utc = if last_included_rule, do: upper_limit, else: until_utc # derive standard and wall time for 'until' until_standard_time = standard_time_from_utc(until_utc, utc_off) @@ -313,38 +323,41 @@ defmodule Tzdata.PeriodBuilder do # If we've hit the upper time boundary of this zone line, we do not need to examine any more # rules for this rule set OR there are no more years to consider for this rule set - if last_included_rule || no_more_years && no_more_rules do + if last_included_rule || (no_more_years && no_more_rules) do h_calc_next_zone_line(btz_data, period, until_utc, zone_line_tl, letter) else - tail = cond do - # If there are no more rules for the year, continue with the next year - no_more_rules -> - calc_rule_periods( - btz_data, - [zone_line | zone_line_tl], - until_utc, - utc_off, - rule.save, - years |> tl, - zone_rules, - rule.letter - ) - # Else continue with those rules - true -> - calc_periods_for_year( - btz_data, - [zone_line | zone_line_tl], - until_utc, - utc_off, - rule.save, - years, - zone_rules, - rules_tail, - rule.letter, - lower_limit - ) - end - if period == nil, do: tail, else: [ period | tail ] + tail = + cond do + # If there are no more rules for the year, continue with the next year + no_more_rules -> + calc_rule_periods( + btz_data, + [zone_line | zone_line_tl], + until_utc, + utc_off, + rule.save, + years |> tl, + zone_rules, + rule.letter + ) + + # Else continue with those rules + true -> + calc_periods_for_year( + btz_data, + [zone_line | zone_line_tl], + until_utc, + utc_off, + rule.save, + years, + zone_rules, + rules_tail, + rule.letter, + lower_limit + ) + end + + if period == nil, do: tail, else: [period | tail] end end @@ -353,9 +366,9 @@ defmodule Tzdata.PeriodBuilder do def sort_rules_by_time(rules, year) do # n.b., we can have many rules per month - such as time changes for religious festivals rules - |> Enum.map(&({&1, TzUtil.tz_day_to_date(year, &1.in, &1.on)})) + |> Enum.map(&{&1, TzUtil.tz_day_to_date(year, &1.in, &1.on)}) |> Enum.sort(&(elem(&1, 1) < elem(&2, 1))) - |> Enum.map(&(elem(&1, 0))) + |> Enum.map(&elem(&1, 0)) end @doc """ diff --git a/mix.exs b/mix.exs index 00999b9..b93dbd8 100644 --- a/mix.exs +++ b/mix.exs @@ -19,7 +19,7 @@ defmodule Tzdata.Mixfile do def application do [ - extra_applications: [:logger], + extra_applications: [:logger, :inets, :public_key], env: env(), mod: {Tzdata.App, []} ] @@ -27,8 +27,8 @@ defmodule Tzdata.Mixfile do defp deps do [ - {:hackney, "~> 1.17"}, - {:ex_doc, "~> 0.21", only: :dev, runtime: false} + {:ex_doc, "~> 0.21", only: :dev, runtime: false}, + {:castore, "~> 0.1.17"} ] end @@ -44,7 +44,7 @@ defmodule Tzdata.Mixfile do [ autoupdate: :enabled, data_dir: nil, - http_client: Tzdata.HTTPClient.Hackney + http_client: Tzdata.HTTPClient.Httpc ] end diff --git a/mix.lock b/mix.lock index d134a8d..da321b1 100644 --- a/mix.lock +++ b/mix.lock @@ -1,15 +1,9 @@ %{ - "certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, - "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, - "hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "castore": {:hex, :castore, "0.1.17", "ba672681de4e51ed8ec1f74ed624d104c0db72742ea1a5e74edbc770c815182f", [:mix], [], "hexpm", "d9844227ed52d26e7519224525cb6868650c272d4a3d327ce3ca5570c12163f9"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"}, + "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, } diff --git a/test/tz_period_builder_test.exs b/test/tz_period_builder_test.exs index 962e5c2..ac122e3 100644 --- a/test/tz_period_builder_test.exs +++ b/test/tz_period_builder_test.exs @@ -33,7 +33,9 @@ defmodule Tzdata.PeriodBuilderTest do def convert(utc) do case utc do - atom when is_atom(utc) -> atom + atom when is_atom(utc) -> + atom + utc -> :calendar.gregorian_seconds_to_datetime(utc) |> NaiveDateTime.from_erl!() @@ -41,29 +43,38 @@ defmodule Tzdata.PeriodBuilderTest do end def test_for_overlaps(map, location) do - result = calc_periods(map, location) - |> Enum.reduce_while(nil, fn period, last -> - %{from: %{utc: from_utc}, until: %{utc: until_utc}, zone_abbr: zone_abbr} = period - # preconditions - assert from_utc != :max # period can't start at :max - assert until_utc != :min # period can't finish at :min - assert from_utc == :min || until_utc == :max || from_utc < until_utc, - "#{location}: #{convert(from_utc)}UTC >= #{convert(until_utc)}UTC" # 'from' must precede 'until' time - case last do - nil -> {:cont, {:ok, period}} - {:ok, last} -> - # check if this period overlaps with prior period - if last.until.utc != from_utc do - {:halt, {:error, - "Location #{location}: #{convert(last.from.utc)}UTC..#{convert(last.until.utc)}UTC #{last.zone_abbr}" - <> "... is non-sequential with ..." - <> "#{convert(from_utc)}UTC..#{convert(until_utc)}UTC #{zone_abbr}" - }} - else + result = + calc_periods(map, location) + |> Enum.reduce_while(nil, fn period, last -> + %{from: %{utc: from_utc}, until: %{utc: until_utc}, zone_abbr: zone_abbr} = period + # preconditions + # period can't start at :max + assert from_utc != :max + # period can't finish at :min + assert until_utc != :min + + assert from_utc == :min || until_utc == :max || from_utc < until_utc, + # 'from' must precede 'until' time + "#{location}: #{convert(from_utc)}UTC >= #{convert(until_utc)}UTC" + + case last do + nil -> {:cont, {:ok, period}} - end - end - end) + + {:ok, last} -> + # check if this period overlaps with prior period + if last.until.utc != from_utc do + {:halt, + {:error, + "Location #{location}: #{convert(last.from.utc)}UTC..#{convert(last.until.utc)}UTC #{last.zone_abbr}" <> + "... is non-sequential with ..." <> + "#{convert(from_utc)}UTC..#{convert(until_utc)}UTC #{zone_abbr}"}} + else + {:cont, {:ok, period}} + end + end + end) + assert {:ok, _last} = result end @@ -72,7 +83,10 @@ defmodule Tzdata.PeriodBuilderTest do {:ok, map} = Tzdata.BasicDataMap.from_single_file_in_dir(@fixtures_dir, "rule_overlap") {:ok, %{map: map}} end - test "will handle coincidence of a rule time change with a subsequent time change", %{map: map} do + + test "will handle coincidence of a rule time change with a subsequent time change", %{ + map: map + } do [ "America/Whitehorse", "America/Santiago", @@ -83,13 +97,13 @@ defmodule Tzdata.PeriodBuilderTest do "Africa/Cairo", "America/Argentina/Buenos_Aires" ] - |> Enum.each(&(test_for_overlaps(map, &1))) + |> Enum.each(&test_for_overlaps(map, &1)) end end test "source data has no time period overlaps", %{map: map} do map.zone_list - |> Enum.each(&(test_for_overlaps(map, &1))) + |> Enum.each(&test_for_overlaps(map, &1)) end test "can calculate for zones with one line", %{map: map} do