diff --git a/ChangeLog b/ChangeLog index c81f88e0..a6f1c4b4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2021-02-09 Jay Berkenbilt + + * Add methods to QUtil for working with PDF timestamp strings: + pdf_time_to_qpdf_time, qpdf_time_to_pdf_time, + get_current_qpdf_time. + 2021-02-07 Jay Berkenbilt * Add new functions QUtil::pipe_file and QUtil::file_provider for diff --git a/include/qpdf/QUtil.hh b/include/qpdf/QUtil.hh index fe18c9b7..09a6c181 100644 --- a/include/qpdf/QUtil.hh +++ b/include/qpdf/QUtil.hh @@ -158,7 +158,6 @@ namespace QUtil QPDF_DLL void setLineBuf(FILE*); - // May modify argv0 QPDF_DLL char* getWhoami(char* argv0); @@ -172,6 +171,51 @@ namespace QUtil QPDF_DLL time_t get_current_time(); + // Portable structure representing a point in time with second + // granularity and time zone offset + struct QPDFTime + { + QPDFTime() = default; + QPDFTime(QPDFTime const&) = default; + QPDFTime& operator=(QPDFTime const&) = default; + QPDFTime(int year, int month, int day, int hour, + int minute, int second, int tz_delta) : + year(year), + month(month), + day(day), + hour(hour), + minute(minute), + second(second), + tz_delta(tz_delta) + { + } + int year; // actual year, no 1900 stuff + int month; // 1--12 + int day; // 1--31 + int hour; + int minute; + int second; + int tz_delta; // minutes before UTC + }; + + QPDF_DLL + QPDFTime get_current_qpdf_time(); + + // Convert a QPDFTime structure to a PDF timestamp string, which + // is "D:yyyymmddhhmmss" where is either "Z" for UTC or + // "-hh'mm'" or "+hh'mm'" for timezone offset. Examples: + // "D:20210207161528-05'00'", "D:20210207211528Z". See + // get_current_qpdf_time and the QPDFTime structure above. + QPDF_DLL + std::string qpdf_time_to_pdf_time(QPDFTime const&); + + // Convert a PDF timestamp string to a QPDFTime. If syntactically + // valid, return true and fill in qtm. If not valid, return false, + // and do not modify qtm. If qtm is null, just check the validity + // of the string. + QPDF_DLL + bool pdf_time_to_qpdf_time(std::string const&, QPDFTime* qtm = nullptr); + // Return a string containing the byte representation of the UTF-8 // encoding for the unicode value passed in. QPDF_DLL diff --git a/libqpdf/QUtil.cc b/libqpdf/QUtil.cc index dc847679..4df76fa8 100644 --- a/libqpdf/QUtil.cc +++ b/libqpdf/QUtil.cc @@ -23,6 +23,7 @@ #include #include #include +#include #ifndef QPDF_NO_WCHAR_T # include #endif @@ -823,6 +824,108 @@ QUtil::get_current_time() #endif } +QUtil::QPDFTime +QUtil::get_current_qpdf_time() +{ +#ifdef _WIN32 + SYSTEMTIME ltime; + GetLocalTime(<ime); + TIME_ZONE_INFORMATION tzinfo; + GetTimeZoneInformation(&tzinfo); + return QPDFTime(static_cast(ltime.wYear), + static_cast(ltime.wMonth), + static_cast(ltime.wDay), + static_cast(ltime.wHour), + static_cast(ltime.wMinute), + static_cast(ltime.wSecond), + static_cast(tzinfo.Bias)); +#else + struct tm ltime; + time_t now = time(0); + tzset(); + localtime_r(&now, <ime); + return QPDFTime(static_cast(ltime.tm_year + 1900), + static_cast(ltime.tm_mon + 1), + static_cast(ltime.tm_mday), + static_cast(ltime.tm_hour), + static_cast(ltime.tm_min), + static_cast(ltime.tm_sec), + static_cast(timezone / 60)); +#endif +} + +std::string +QUtil::qpdf_time_to_pdf_time(QPDFTime const& qtm) +{ + std::string tz_offset; + int t = qtm.tz_delta; + if (t == 0) + { + tz_offset = "Z"; + } + else + { + if (t < 0) + { + t = -t; + tz_offset += "+"; + } + else + { + tz_offset += "-"; + } + tz_offset += + QUtil::int_to_string(t / 60, 2) + "'" + + QUtil::int_to_string(t % 60, 2) + "'"; + } + return ("D:" + + QUtil::int_to_string(qtm.year, 4) + + QUtil::int_to_string(qtm.month, 2) + + QUtil::int_to_string(qtm.day, 2) + + QUtil::int_to_string(qtm.hour, 2) + + QUtil::int_to_string(qtm.minute, 2) + + QUtil::int_to_string(qtm.second, 2) + + tz_offset); +} + +bool +QUtil::pdf_time_to_qpdf_time(std::string const& str, QPDFTime* qtm) +{ + static std::regex pdf_date("^D:([0-9]{4})([0-9]{2})([0-9]{2})" + "([0-9]{2})([0-9]{2})([0-9]{2})" + "(?:(Z)|([\\+\\-])([0-9]{2})'([0-9]{2})')$"); + std::smatch m; + if (! std::regex_match(str, m, pdf_date)) + { + return false; + } + int tz_delta = 0; + auto to_i = [](std::string const& s) { + return QUtil::string_to_int(s.c_str()); + }; + + if (m[7] == "") + { + tz_delta = ((to_i(m[9]) * 60) + + to_i(m[10])); + if (m[8] == "+") + { + tz_delta = -tz_delta; + } + } + if (qtm) + { + *qtm = QPDFTime(to_i(m[1]), + to_i(m[2]), + to_i(m[3]), + to_i(m[4]), + to_i(m[5]), + to_i(m[6]), + tz_delta); + } + return true; +} + std::string QUtil::toUTF8(unsigned long uval) { diff --git a/libtests/qtest/qutil/qutil.out b/libtests/qtest/qutil/qutil.out index 5fe841ac..a5b79e7f 100644 --- a/libtests/qtest/qutil/qutil.out +++ b/libtests/qtest/qutil/qutil.out @@ -105,3 +105,7 @@ rename file create file rename over existing delete file +---- timestamp +D:20210209144925-05'00' +D:20210210011925+05'30' +D:20210209191925Z diff --git a/libtests/qutil.cc b/libtests/qutil.cc index 94454374..b67b7580 100644 --- a/libtests/qutil.cc +++ b/libtests/qutil.cc @@ -581,6 +581,27 @@ void rename_delete_test() assert_no_file("old\xcf\x80.~tmp"); } +void timestamp_test() +{ + auto check = [](QUtil::QPDFTime const& t) { + std::string pdf = QUtil::qpdf_time_to_pdf_time(t); + std::cout << pdf << std::endl; + QUtil::QPDFTime t2; + assert(QUtil::pdf_time_to_qpdf_time(pdf, &t2)); + assert(QUtil::qpdf_time_to_pdf_time(t2) == pdf); + }; + check(QUtil::QPDFTime(2021, 2, 9, 14, 49, 25, 300)); + check(QUtil::QPDFTime(2021, 2, 10, 1, 19, 25, -330)); + check(QUtil::QPDFTime(2021, 2, 9, 19, 19, 25, 0)); + assert(! QUtil::pdf_time_to_qpdf_time("potato")); + // Round trip on the current time without actually printing it. + // Manual testing was done to ensure that we are actually getting + // back the current time in various timezones. + assert(QUtil::pdf_time_to_qpdf_time( + QUtil::qpdf_time_to_pdf_time( + QUtil::get_current_qpdf_time()))); +} + int main(int argc, char* argv[]) { try @@ -611,6 +632,8 @@ int main(int argc, char* argv[]) hex_encode_decode_test(); std::cout << "---- rename/delete" << std::endl; rename_delete_test(); + std::cout << "---- timestamp" << std::endl; + timestamp_test(); } catch (std::exception& e) { diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml index 39f3d969..05526958 100644 --- a/manual/qpdf-manual.xml +++ b/manual/qpdf-manual.xml @@ -4941,6 +4941,14 @@ print "\n"; details. + + + Add QUtil::get_current_qpdf_time, + QUtil::pdf_time_to_qpdf_time, and + QUtil::qpdf_time_to_pdf_time for + working with PDF timestamp strings. + + Add warn to