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

openSUSE Build Service is sponsored by