File 0294-stdlib-Fix-zip-to-handle-invalid-dos-timestamps.patch of Package erlang
From 3d2fd90b170d96dfbb97c997a17e5a28ddc75f26 Mon Sep 17 00:00:00 2001
From: ruslandoga <ruslandoga+gh@icloud.com>
Date: Wed, 5 Mar 2025 02:39:16 +0300
Subject: [PATCH] stdlib: Fix zip to handle invalid dos timestamps
---
erts/preloaded/src/prim_zip.erl | 9 ++++-
lib/stdlib/src/zip.erl | 17 ++++++--
lib/stdlib/test/zip_SUITE.erl | 37 +++++++++++++++++-
.../test/zip_SUITE_data/bad_seconds.zip | Bin 0 -> 281 bytes
.../zip_SUITE_data/bad_seconds.zip.license | 7 ++++
5 files changed, 64 insertions(+), 6 deletions(-)
create mode 100644 lib/stdlib/test/zip_SUITE_data/bad_seconds.zip
create mode 100644 lib/stdlib/test/zip_SUITE_data/bad_seconds.zip.license
diff --git a/erts/preloaded/src/prim_zip.erl b/erts/preloaded/src/prim_zip.erl
index f1cfb4dafc..9fbfe54679 100644
--- a/erts/preloaded/src/prim_zip.erl
+++ b/erts/preloaded/src/prim_zip.erl
@@ -532,8 +532,15 @@ add_extra_info(FI, _) ->
dos_date_time_to_datetime(DosDate, DosTime) ->
<<Hour:5, Min:6, Sec:5>> = <<DosTime:16>>,
<<YearFrom1980:7, Month:4, Day:5>> = <<DosDate:16>>,
+
+ %% Note that we have a different solution here than in zip.erl
+ %% as it uses calendar and we don't have access to that here.
+ %%
+ %% The assumption is that prim_zip will never really care about
+ %% the timestamps and thus it does not really matter.
+
{{YearFrom1980+1980, Month, Day},
- {Hour, Min, Sec * 2}}.
+ {Hour, Min, min(Sec * 2, 59)}}.
cd_file_header_from_bin(<<VersionMadeBy:16/little,
VersionNeeded:16/little,
diff --git a/lib/stdlib/src/zip.erl b/lib/stdlib/src/zip.erl
index a8050cff98..e3bd27e815 100644
--- a/lib/stdlib/src/zip.erl
+++ b/lib/stdlib/src/zip.erl
@@ -2466,10 +2466,21 @@ file_header_ctime_to_datetime(FH) ->
%% bit 0 - 4 5 - 10 11 - 15 16 - 20 21 - 24 25 - 31
%% value second minute hour day (1 - 31) month (1 - 12) years from 1980
dos_date_time_to_datetime(DosDate, DosTime) ->
- <<Hour:5, Min:6, Sec:5>> = <<DosTime:16>>,
+ <<Hour:5, Min:6, DoubleSec:5>> = <<DosTime:16>>,
<<YearFrom1980:7, Month:4, Day:5>> = <<DosDate:16>>,
- {{YearFrom1980+1980, Month, Day},
- {Hour, Min, Sec * 2}}.
+
+ Datetime = {{YearFrom1980+1980, Month, Day},
+ {Hour, Min, DoubleSec * 2}},
+ if DoubleSec > 29 ->
+ %% If DoubleSec * 2 > 59, something is broken
+ %% with this archive, but unzip wraps the value
+ %% so we do the same by converting to greg seconds
+ %% and then back again.
+ calendar:gregorian_seconds_to_datetime(
+ calendar:datetime_to_gregorian_seconds(Datetime));
+ true ->
+ Datetime
+ end.
dos_date_time_from_datetime({{Year, Month, Day}, {Hour, Min, Sec}}) ->
YearFrom1980 = Year-1980,
diff --git a/lib/stdlib/test/zip_SUITE.erl b/lib/stdlib/test/zip_SUITE.erl
index 8fcfa6460a..61289ffca6 100644
--- a/lib/stdlib/test/zip_SUITE.erl
+++ b/lib/stdlib/test/zip_SUITE.erl
@@ -35,7 +35,7 @@
zip64_central_headers/0, unzip64_central_headers/0,
zip64_central_headers/1, unzip64_central_headers/1,
zip64_central_directory/1,
- basic_timestamp/1, extended_timestamp/1,
+ basic_timestamp/1, extended_timestamp/1, capped_timestamp/1,
uid_gid/1]).
-export([zip/5, unzip/3]).
@@ -97,7 +97,7 @@ un_z64(Mode) ->
end.
zip_testcases() ->
- [mode, basic_timestamp, extended_timestamp, uid_gid, sanitize_filenames].
+ [mode, basic_timestamp, extended_timestamp, capped_timestamp, uid_gid, sanitize_filenames].
zip64_testcases() ->
[unzip64_central_headers,
@@ -1558,6 +1558,39 @@ extended_timestamp(Config) ->
ok.
+% checks that the timestamps in the zip file are wrapped if > 59
+capped_timestamp(Config) ->
+
+ DataDir = get_value(data_dir, Config),
+ Archive = filename:join(DataDir, "bad_seconds.zip"),
+ PrivDir = get_value(pdir, Config),
+ ExtractDir = filename:join(PrivDir, "extract"),
+
+ {ok, [#zip_comment{},
+ #zip_file{ info = ZipFI = #file_info{ mtime = ZMtime }} ]} =
+ zip:list_dir(Archive),
+
+ ct:log("in zip : ~p",[ZipFI]),
+
+ %% zipinfo shows something different from what unzip
+ ct:log("zipinfo:~n~ts",[os:cmd("zipinfo -v "++Archive)]),
+
+ % and not {{2024, 12, 31}, {23, 59, 60}}
+ ?assertEqual({{2025, 1, 1}, {0, 0, 0}}, ZMtime),
+
+ ok = file:make_dir(ExtractDir),
+ ?assertMatch(
+ {ok, ["testfile.txt"]},
+ unzip(Config, Archive, [{cwd,ExtractDir}])),
+
+ {ok, UnzipFI } =
+ file:read_file_info(filename:join(ExtractDir, "testfile.txt"),[raw]),
+
+ ct:log("extract: ~p",[UnzipFI]),
+ UnzipMode = un_z64(get_value(unzip, Config)),
+ assert_timestamp(UnzipMode, UnzipFI, ZMtime),
+ ok.
+
assert_timestamp(unemzip, _FI, _ZMtime) ->
%% emzip does not support timestamps
ok;
diff --git a/lib/stdlib/test/zip_SUITE_data/bad_seconds.zip.license b/lib/stdlib/test/zip_SUITE_data/bad_seconds.zip.license
new file mode 100644
index 0000000000..0c957e5e3f
--- /dev/null
+++ b/lib/stdlib/test/zip_SUITE_data/bad_seconds.zip.license
@@ -0,0 +1,7 @@
+%CopyrightBegin%
+
+SPDX-License-Identifier: Apache-2.0
+
+Copyright 2025 ruslandoga <ruslandoga+gh@icloud.com>
+
+%CopyrightEnd%
--
2.43.0