Commit bf0e6eb3022bf2fde5623a0a3d151c07f5e82945

Authored by Jay Berkenbilt
1 parent bfbeec54

Add QUtil methods for dealing with PDF timestamp strings

ChangeLog
  1 +2021-02-09 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Add methods to QUtil for working with PDF timestamp strings:
  4 + pdf_time_to_qpdf_time, qpdf_time_to_pdf_time,
  5 + get_current_qpdf_time.
  6 +
1 7 2021-02-07 Jay Berkenbilt <ejb@ql.org>
2 8  
3 9 * Add new functions QUtil::pipe_file and QUtil::file_provider for
... ...
include/qpdf/QUtil.hh
... ... @@ -158,7 +158,6 @@ namespace QUtil
158 158 QPDF_DLL
159 159 void setLineBuf(FILE*);
160 160  
161   -
162 161 // May modify argv0
163 162 QPDF_DLL
164 163 char* getWhoami(char* argv0);
... ... @@ -172,6 +171,51 @@ namespace QUtil
172 171 QPDF_DLL
173 172 time_t get_current_time();
174 173  
  174 + // Portable structure representing a point in time with second
  175 + // granularity and time zone offset
  176 + struct QPDFTime
  177 + {
  178 + QPDFTime() = default;
  179 + QPDFTime(QPDFTime const&) = default;
  180 + QPDFTime& operator=(QPDFTime const&) = default;
  181 + QPDFTime(int year, int month, int day, int hour,
  182 + int minute, int second, int tz_delta) :
  183 + year(year),
  184 + month(month),
  185 + day(day),
  186 + hour(hour),
  187 + minute(minute),
  188 + second(second),
  189 + tz_delta(tz_delta)
  190 + {
  191 + }
  192 + int year; // actual year, no 1900 stuff
  193 + int month; // 1--12
  194 + int day; // 1--31
  195 + int hour;
  196 + int minute;
  197 + int second;
  198 + int tz_delta; // minutes before UTC
  199 + };
  200 +
  201 + QPDF_DLL
  202 + QPDFTime get_current_qpdf_time();
  203 +
  204 + // Convert a QPDFTime structure to a PDF timestamp string, which
  205 + // is "D:yyyymmddhhmmss<z>" where <z> is either "Z" for UTC or
  206 + // "-hh'mm'" or "+hh'mm'" for timezone offset. Examples:
  207 + // "D:20210207161528-05'00'", "D:20210207211528Z". See
  208 + // get_current_qpdf_time and the QPDFTime structure above.
  209 + QPDF_DLL
  210 + std::string qpdf_time_to_pdf_time(QPDFTime const&);
  211 +
  212 + // Convert a PDF timestamp string to a QPDFTime. If syntactically
  213 + // valid, return true and fill in qtm. If not valid, return false,
  214 + // and do not modify qtm. If qtm is null, just check the validity
  215 + // of the string.
  216 + QPDF_DLL
  217 + bool pdf_time_to_qpdf_time(std::string const&, QPDFTime* qtm = nullptr);
  218 +
175 219 // Return a string containing the byte representation of the UTF-8
176 220 // encoding for the unicode value passed in.
177 221 QPDF_DLL
... ...
libqpdf/QUtil.cc
... ... @@ -23,6 +23,7 @@
23 23 #include <fcntl.h>
24 24 #include <memory>
25 25 #include <locale>
  26 +#include <regex>
26 27 #ifndef QPDF_NO_WCHAR_T
27 28 # include <cwchar>
28 29 #endif
... ... @@ -823,6 +824,108 @@ QUtil::get_current_time()
823 824 #endif
824 825 }
825 826  
  827 +QUtil::QPDFTime
  828 +QUtil::get_current_qpdf_time()
  829 +{
  830 +#ifdef _WIN32
  831 + SYSTEMTIME ltime;
  832 + GetLocalTime(&ltime);
  833 + TIME_ZONE_INFORMATION tzinfo;
  834 + GetTimeZoneInformation(&tzinfo);
  835 + return QPDFTime(static_cast<int>(ltime.wYear),
  836 + static_cast<int>(ltime.wMonth),
  837 + static_cast<int>(ltime.wDay),
  838 + static_cast<int>(ltime.wHour),
  839 + static_cast<int>(ltime.wMinute),
  840 + static_cast<int>(ltime.wSecond),
  841 + static_cast<int>(tzinfo.Bias));
  842 +#else
  843 + struct tm ltime;
  844 + time_t now = time(0);
  845 + tzset();
  846 + localtime_r(&now, &ltime);
  847 + return QPDFTime(static_cast<int>(ltime.tm_year + 1900),
  848 + static_cast<int>(ltime.tm_mon + 1),
  849 + static_cast<int>(ltime.tm_mday),
  850 + static_cast<int>(ltime.tm_hour),
  851 + static_cast<int>(ltime.tm_min),
  852 + static_cast<int>(ltime.tm_sec),
  853 + static_cast<int>(timezone / 60));
  854 +#endif
  855 +}
  856 +
  857 +std::string
  858 +QUtil::qpdf_time_to_pdf_time(QPDFTime const& qtm)
  859 +{
  860 + std::string tz_offset;
  861 + int t = qtm.tz_delta;
  862 + if (t == 0)
  863 + {
  864 + tz_offset = "Z";
  865 + }
  866 + else
  867 + {
  868 + if (t < 0)
  869 + {
  870 + t = -t;
  871 + tz_offset += "+";
  872 + }
  873 + else
  874 + {
  875 + tz_offset += "-";
  876 + }
  877 + tz_offset +=
  878 + QUtil::int_to_string(t / 60, 2) + "'" +
  879 + QUtil::int_to_string(t % 60, 2) + "'";
  880 + }
  881 + return ("D:" +
  882 + QUtil::int_to_string(qtm.year, 4) +
  883 + QUtil::int_to_string(qtm.month, 2) +
  884 + QUtil::int_to_string(qtm.day, 2) +
  885 + QUtil::int_to_string(qtm.hour, 2) +
  886 + QUtil::int_to_string(qtm.minute, 2) +
  887 + QUtil::int_to_string(qtm.second, 2) +
  888 + tz_offset);
  889 +}
  890 +
  891 +bool
  892 +QUtil::pdf_time_to_qpdf_time(std::string const& str, QPDFTime* qtm)
  893 +{
  894 + static std::regex pdf_date("^D:([0-9]{4})([0-9]{2})([0-9]{2})"
  895 + "([0-9]{2})([0-9]{2})([0-9]{2})"
  896 + "(?:(Z)|([\\+\\-])([0-9]{2})'([0-9]{2})')$");
  897 + std::smatch m;
  898 + if (! std::regex_match(str, m, pdf_date))
  899 + {
  900 + return false;
  901 + }
  902 + int tz_delta = 0;
  903 + auto to_i = [](std::string const& s) {
  904 + return QUtil::string_to_int(s.c_str());
  905 + };
  906 +
  907 + if (m[7] == "")
  908 + {
  909 + tz_delta = ((to_i(m[9]) * 60) +
  910 + to_i(m[10]));
  911 + if (m[8] == "+")
  912 + {
  913 + tz_delta = -tz_delta;
  914 + }
  915 + }
  916 + if (qtm)
  917 + {
  918 + *qtm = QPDFTime(to_i(m[1]),
  919 + to_i(m[2]),
  920 + to_i(m[3]),
  921 + to_i(m[4]),
  922 + to_i(m[5]),
  923 + to_i(m[6]),
  924 + tz_delta);
  925 + }
  926 + return true;
  927 +}
  928 +
826 929 std::string
827 930 QUtil::toUTF8(unsigned long uval)
828 931 {
... ...
libtests/qtest/qutil/qutil.out
... ... @@ -105,3 +105,7 @@ rename file
105 105 create file
106 106 rename over existing
107 107 delete file
  108 +---- timestamp
  109 +D:20210209144925-05'00'
  110 +D:20210210011925+05'30'
  111 +D:20210209191925Z
... ...
libtests/qutil.cc
... ... @@ -581,6 +581,27 @@ void rename_delete_test()
581 581 assert_no_file("old\xcf\x80.~tmp");
582 582 }
583 583  
  584 +void timestamp_test()
  585 +{
  586 + auto check = [](QUtil::QPDFTime const& t) {
  587 + std::string pdf = QUtil::qpdf_time_to_pdf_time(t);
  588 + std::cout << pdf << std::endl;
  589 + QUtil::QPDFTime t2;
  590 + assert(QUtil::pdf_time_to_qpdf_time(pdf, &t2));
  591 + assert(QUtil::qpdf_time_to_pdf_time(t2) == pdf);
  592 + };
  593 + check(QUtil::QPDFTime(2021, 2, 9, 14, 49, 25, 300));
  594 + check(QUtil::QPDFTime(2021, 2, 10, 1, 19, 25, -330));
  595 + check(QUtil::QPDFTime(2021, 2, 9, 19, 19, 25, 0));
  596 + assert(! QUtil::pdf_time_to_qpdf_time("potato"));
  597 + // Round trip on the current time without actually printing it.
  598 + // Manual testing was done to ensure that we are actually getting
  599 + // back the current time in various timezones.
  600 + assert(QUtil::pdf_time_to_qpdf_time(
  601 + QUtil::qpdf_time_to_pdf_time(
  602 + QUtil::get_current_qpdf_time())));
  603 +}
  604 +
584 605 int main(int argc, char* argv[])
585 606 {
586 607 try
... ... @@ -611,6 +632,8 @@ int main(int argc, char* argv[])
611 632 hex_encode_decode_test();
612 633 std::cout << "---- rename/delete" << std::endl;
613 634 rename_delete_test();
  635 + std::cout << "---- timestamp" << std::endl;
  636 + timestamp_test();
614 637 }
615 638 catch (std::exception& e)
616 639 {
... ...
manual/qpdf-manual.xml
... ... @@ -4943,6 +4943,14 @@ print &quot;\n&quot;;
4943 4943 </listitem>
4944 4944 <listitem>
4945 4945 <para>
  4946 + Add <function>QUtil::get_current_qpdf_time</function>,
  4947 + <function>QUtil::pdf_time_to_qpdf_time</function>, and
  4948 + <function>QUtil::qpdf_time_to_pdf_time</function> for
  4949 + working with PDF timestamp strings.
  4950 + </para>
  4951 + </listitem>
  4952 + <listitem>
  4953 + <para>
4946 4954 Add <function>warn</function> to
4947 4955 <classname>QPDF</classname>'s public API.
4948 4956 </para>
... ...