fix_1416_iptc_DateCreated (#1547)

* fix_1416_iptc_DateCreated

* Fix unit tests

* DateValue:read 2nd iteration on pre-condition

* test with ISO_8601 date format

* Use std::regex for ISO 8601 basic & extended date formats

* Use std::regex for ISO 8601 basic & extended time formats

* Add more tests & notes for DateValue & TimeValue

* Comment tests using local calendar times

* DateValue::write also adds padding to year field

Co-authored-by: Luis Díaz Más <piponazo@gmail.com>
main
Robin Mills 4 years ago committed by GitHub
parent fd8447129c
commit 13a2cf336d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -979,7 +979,6 @@ namespace Exiv2 {
//! Simple Date helper structure
struct EXIV2API Date {
Date() = default;
int year{0}; //!< Year
int month{0}; //!< Month
int day{0}; //!< Day
@ -1031,6 +1030,7 @@ namespace Exiv2 {
@return Number of characters written.
*/
long copy(byte* buf, ByteOrder byteOrder = invalidByteOrder) const override;
//! Return date struct containing date information
virtual const Date& getDate() const;
long count() const override;
@ -1150,31 +1150,6 @@ namespace Exiv2 {
//@}
private:
//! @name Manipulators
//@{
/*!
@brief Set time from \em buf if it conforms to \em format
(3 input items).
This function only sets the hour, minute and second parts of time_.
@param buf A 0 terminated C-string containing the time to parse.
@param format Format string for sscanf().
@return 0 if successful, else 1.
*/
int scanTime3(const char* buf, const char* format);
/*!
@brief Set time from \em buf if it conforms to \em format
(6 input items).
This function sets all parts of time_.
@param buf A 0 terminated C-string containing the time to parse.
@param format Format string for sscanf().
@return 0 if successful, else 1.
*/
int scanTime6(const char* buf, const char* format);
//@}
//! @name Accessors
//@{

@ -27,16 +27,17 @@
#include "unused.h"
// + standard includes
#include <iostream>
#include <iomanip>
#include <sstream>
#include <ctype.h>
#include <cassert>
#include <cstring>
#include <ctime>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <ctype.h>
#include <cstring>
#include <ctime>
#include <iomanip>
#include <regex>
#include <sstream>
// *****************************************************************************
// class member definitions
@ -906,52 +907,31 @@ namespace Exiv2 {
int DateValue::read(const byte* buf, long len, ByteOrder /*byteOrder*/)
{
// Hard coded to read Iptc style dates
if (len != 8) {
#ifndef SUPPRESS_WARNINGS
EXV_WARNING << Error(kerUnsupportedDateFormat) << "\n";
#endif
return 1;
}
// Make the buffer a 0 terminated C-string for sscanf
char b[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0 };
std::memcpy(b, reinterpret_cast<const char*>(buf), 8);
int scanned = sscanf(b, "%4d%2d%2d",
&date_.year, &date_.month, &date_.day);
if ( scanned != 3
|| date_.year < 0
|| date_.month < 1 || date_.month > 12
|| date_.day < 1 || date_.day > 31) {
#ifndef SUPPRESS_WARNINGS
EXV_WARNING << Error(kerUnsupportedDateFormat) << "\n";
#endif
return 1;
}
return 0;
const std::string str(reinterpret_cast<const char*>(buf), len);
return read(str);
}
int DateValue::read(const std::string& buf)
{
// Hard coded to read Iptc style dates
if (buf.length() < 8) {
#ifndef SUPPRESS_WARNINGS
EXV_WARNING << Error(kerUnsupportedDateFormat) << "\n";
#endif
return 1;
// ISO 8601 date formats:
// https://web.archive.org/web/20171020084445/https://www.loc.gov/standards/datetime/ISO_DIS%208601-1.pdf
static const std::regex reExtended(R"(^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]))");
static const std::regex reBasic(R"(^(\d{4})(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01]))");
std::smatch sm;
// Note: We use here regex_search instead of regex_match, because the string can be longer than expected and
// also contain the time
if (std::regex_search(buf, sm, reExtended) || std::regex_search(buf, sm, reBasic)) {
date_.year = std::stoi(sm[1].str());
date_.month = std::stoi(sm[2].str());
date_.day = std::stoi(sm[3].str());
return 0;
}
int scanned = sscanf(buf.c_str(), "%4d-%2d-%2d",
&date_.year, &date_.month, &date_.day);
if ( scanned != 3
|| date_.year < 0
|| date_.month < 1 || date_.month > 12
|| date_.day < 1 || date_.day > 31) {
#ifndef SUPPRESS_WARNINGS
EXV_WARNING << Error(kerUnsupportedDateFormat) << "\n";
#endif
return 1;
}
return 0;
}
void DateValue::setDate(const Date& src)
{
@ -962,9 +942,11 @@ namespace Exiv2 {
long DateValue::copy(byte* buf, ByteOrder /*byteOrder*/) const
{
// \note Here the date is copied in the Basic format YYYYMMDD, as the IPTC key Iptc.Application2.DateCreated
// wants it. Check https://exiv2.org/iptc.html
// sprintf wants to add the null terminator, so use oversized buffer
char temp[9];
int wrote = snprintf(temp, sizeof(temp), "%04d%02d%02d", date_.year, date_.month, date_.day);
assert(wrote == 8);
std::memcpy(buf, temp, wrote);
@ -993,8 +975,9 @@ namespace Exiv2 {
std::ostream& DateValue::write(std::ostream& os) const
{
// Write DateValue in ISO 8601 Extended format: YYYY-MM-DD
std::ios::fmtflags f( os.flags() );
os << date_.year << '-' << std::right
os << std::setw(4) << std::setfill('0') << date_.year << '-' << std::right
<< std::setw(2) << std::setfill('0') << date_.month << '-'
<< std::setw(2) << std::setfill('0') << date_.day;
os.flags(f);
@ -1044,83 +1027,50 @@ namespace Exiv2 {
int TimeValue::read(const byte* buf, long len, ByteOrder /*byteOrder*/)
{
// Make the buffer a 0 terminated C-string for scanTime[36]
char b[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
std::memcpy(b, reinterpret_cast<const char*>(buf), (len < 12 ? len : 11));
// Hard coded to read HHMMSS or Iptc style times
int rc = 1;
if (len == 6) {
// Try to read (non-standard) HHMMSS format
rc = scanTime3(b, "%2d%2d%2d");
}
if (len == 11) {
rc = scanTime6(b, "%2d%2d%2d%1c%2d%2d");
}
if (rc) {
rc = 1;
#ifndef SUPPRESS_WARNINGS
EXV_WARNING << Error(kerUnsupportedTimeFormat) << "\n";
#endif
}
return rc;
const std::string str(reinterpret_cast<const char*>(buf), len);
return read(str);
}
int TimeValue::read(const std::string& buf)
{
// Hard coded to read H:M:S or Iptc style times
int rc = 1;
if (buf.length() < 9) {
// Try to read (non-standard) H:M:S format
rc = scanTime3(buf.c_str(), "%d:%d:%d");
// ISO 8601 time formats:
// https://web.archive.org/web/20171020084445/https://www.loc.gov/standards/datetime/ISO_DIS%208601-1.pdf
// Not supported formats:
// 4.2.2.4 Representations with decimal fraction: 232050,5
static const std::regex re(R"(^(2[0-3]|[01][0-9]):?([0-5][0-9])?:?([0-5][0-9])?$)");
static const std::regex reExt(R"(^(2[0-3]|[01][0-9]):?([0-5][0-9]):?([0-5][0-9])(Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?)$)");
std::smatch sm;
if (std::regex_match(buf, sm, re) || std::regex_match(buf, sm, reExt)) {
time_.hour = sm.length(1) ? std::stoi(sm[1].str()) : 0;
time_.minute = sm.length(2) ? std::stoi(sm[2].str()) : 0;
time_.second = sm.length(3) ? std::stoi(sm[3].str()) : 0;
if (sm.size() > 4)
{
std::string str = sm[4].str();
const auto strSize = str.size();
auto posColon = str.find(':');
if (posColon == std::string::npos) {
// Extended format
time_.tzHour = std::stoi(str.substr(0,3));
if (strSize > 3) {
int minute = std::stoi(str.substr(3));
time_.tzMinute = time_.tzHour < 0 ? -minute : minute;
}
else {
rc = scanTime6(buf.c_str(), "%d:%d:%d%1c%d:%d");
} else {
// Basic format
time_.tzHour = std::stoi(str.substr(0, posColon));
int minute = std::stoi(str.substr(posColon+1));
time_.tzMinute = time_.tzHour < 0 ? -minute : minute;
}
}
return 0;
}
if (rc) {
rc = 1;
#ifndef SUPPRESS_WARNINGS
EXV_WARNING << Error(kerUnsupportedTimeFormat) << "\n";
#endif
}
return rc;
}
int TimeValue::scanTime3(const char* buf, const char* format)
{
int rc = 1;
Time t;
int scanned = sscanf(buf, format, &t.hour, &t.minute, &t.second);
if ( scanned == 3
&& t.hour >= 0 && t.hour < 24
&& t.minute >= 0 && t.minute < 60
&& t.second >= 0 && t.second < 60) {
time_ = t;
rc = 0;
}
return rc;
}
int TimeValue::scanTime6(const char* buf, const char* format)
{
int rc = 1;
Time t;
char plusMinus = 0;
int scanned = sscanf(buf, format, &t.hour, &t.minute, &t.second,
&plusMinus, &t.tzHour, &t.tzMinute);
if ( scanned == 6
&& t.hour >= 0 && t.hour < 24
&& t.minute >= 0 && t.minute < 60
&& t.second >= 0 && t.second < 60
&& t.tzHour >= 0 && t.tzHour < 24
&& t.tzMinute >= 0 && t.tzMinute < 60) {
time_ = t;
if (plusMinus == '-') {
time_.tzHour *= -1;
time_.tzMinute *= -1;
}
rc = 0;
}
return rc;
return 1;
}
void TimeValue::setTime( const Time& src )
@ -1130,6 +1080,8 @@ namespace Exiv2 {
long TimeValue::copy(byte* buf, ByteOrder /*byteOrder*/) const
{
// NOTE: Here the time is copied in the Basic format HHMMSS:HHMM, as the IPTC key Iptc.Application2.TimeCreated
// wants it. Check https://exiv2.org/iptc.html
char temp[12];
char plusMinus = '+';
if (time_.tzHour < 0 || time_.tzMinute < 0)
@ -1167,8 +1119,10 @@ namespace Exiv2 {
std::ostream& TimeValue::write(std::ostream& os) const
{
// Write TimeValue in ISO 8601 Extended format: hh:mm:ss±hh:mm
char plusMinus = '+';
if (time_.tzHour < 0 || time_.tzMinute < 0) plusMinus = '-';
if (time_.tzHour < 0 || time_.tzMinute < 0)
plusMinus = '-';
std::ios::fmtflags f( os.flags() );
os << std::right

@ -642,11 +642,11 @@ def addModTest(filename):
stdin = """
a Iptc.Application2.Headline The headline I am
a Iptc.Application2.Keywords Yet another keyword
m Iptc.Application2.DateCreated 2004-8-3
m Iptc.Application2.DateCreated 2004-08-03
a Iptc.Application2.Urgency 3
m Iptc.Application2.SuppCategory "bla bla ba"
a Iptc.Envelope.ModelVersion 2
a Iptc.Envelope.TimeSent 14:41:0-05:00
a Iptc.Envelope.TimeSent 14:41:00-05:00
a Iptc.Application2.RasterizedCaption 230 42 34 2 90 84 23 146
""".lstrip('\n').encode()
Executer('iptctest {tmp}', vars(), stdin=stdin)

@ -19,8 +19,13 @@
*/
#include "value.hpp"
#include <gtest/gtest.h>
#include <array>
#include <algorithm>
#include <sstream>
using namespace Exiv2;
TEST(ADateValue, isDefaultConstructed)
@ -31,7 +36,7 @@ TEST(ADateValue, isDefaultConstructed)
ASSERT_EQ(0, dateValue.getDate().day);
}
TEST(ADateValue, isConstructedWithArgs)
TEST(ADateValue, canBeConstructedWithValidDate)
{
const DateValue dateValue (2018, 4, 2);
ASSERT_EQ(2018, dateValue.getDate().year);
@ -39,6 +44,45 @@ TEST(ADateValue, isConstructedWithArgs)
ASSERT_EQ(2, dateValue.getDate().day);
}
/// \todo Probably we should avoid this ...
TEST(ADateValue, canBeConstructedWithInvalidDate)
{
const DateValue dateValue (2018, 13, 69);
ASSERT_EQ(2018, dateValue.getDate().year);
ASSERT_EQ(13, dateValue.getDate().month);
ASSERT_EQ(69, dateValue.getDate().day);
}
TEST(ADateValue, setsValidDateCorrectly)
{
DateValue dateValue;
DateValue::Date date;
date.year = 2018;
date.month = 4;
date.day = 2;
dateValue.setDate(date);
ASSERT_EQ(2018, dateValue.getDate().year);
ASSERT_EQ(4, dateValue.getDate().month);
ASSERT_EQ(2, dateValue.getDate().day);
}
/// \todo Probably we should avoid this ...
TEST(ADateValue, setsInvalidDateCorrectly)
{
DateValue dateValue;
DateValue::Date date;
date.year = 2018;
date.month = 13;
date.day = 69;
dateValue.setDate(date);
ASSERT_EQ(2018, dateValue.getDate().year);
ASSERT_EQ(13, dateValue.getDate().month);
ASSERT_EQ(69, dateValue.getDate().day);
}
TEST(ADateValue, readFromByteBufferWithExpectedSize)
{
@ -54,7 +98,7 @@ TEST(ADateValue, doNotReadFromByteBufferWithoutExpectedSize)
{
DateValue dateValue;
const byte date[8] = {0x32, 0x30, 0x31, 0x38, 0x30, 0x34, 0x30, 0x32 }; // 20180402
ASSERT_EQ(1, dateValue.read(date, 9));
ASSERT_EQ(1, dateValue.read(date, 6));
}
TEST(ADateValue, doNotReadFromByteBufferWithExpectedSizeButNotCorrectContent)
@ -65,7 +109,7 @@ TEST(ADateValue, doNotReadFromByteBufferWithExpectedSizeButNotCorrectContent)
}
TEST(ADateValue, readFromStringWithExpectedSize)
TEST(ADateValue, readFromStringWithExpectedSizeAndDashes)
{
DateValue dateValue;
const std::string date ("2018-04-02");
@ -75,29 +119,90 @@ TEST(ADateValue, readFromStringWithExpectedSize)
ASSERT_EQ(2, dateValue.getDate().day);
}
TEST(ADateValue, doNotReadFromStringWithoutExpectedSize)
TEST(ADateValue, readFromStringWithExpectedSizeWithoutDashes)
{
DateValue dateValue;
const std::string date ("20180402");
ASSERT_EQ(1, dateValue.read(date));
ASSERT_EQ(0, dateValue.read(date));
ASSERT_EQ(2018, dateValue.getDate().year);
ASSERT_EQ(4, dateValue.getDate().month);
ASSERT_EQ(2, dateValue.getDate().day);
}
TEST(ADateValue, readFromStringWithTime)
{
DateValue dateValue;
const std::string date ("2018-04-02T12:01:44.999999999");
ASSERT_EQ(0, dateValue.read(date));
ASSERT_EQ(2018, dateValue.getDate().year);
ASSERT_EQ(4, dateValue.getDate().month);
ASSERT_EQ(2, dateValue.getDate().day);
}
TEST(ADateValue, doNotReadFromStringWithoutExpectedSize)
{
DateValue dateValue;
ASSERT_EQ(1, dateValue.read("2018-04-0"));
ASSERT_EQ(1, dateValue.read("2018040"));
}
TEST(ADateValue, doNotReadFromStringWithExpectedSizeButNotCorrectContent)
{
DateValue dateValue;
const std::string date ("2018-aa-bb");
ASSERT_EQ(1, dateValue.read(date));
ASSERT_EQ(1, dateValue.read("2018-24-02"));
ASSERT_EQ(1, dateValue.read("2018-aa-bb"));
ASSERT_EQ(1, dateValue.read("2018aabb"));
}
TEST(ADateValue, writesRecentDateToExtendedFormat)
{
const DateValue dateValue (2021, 12, 1);
std::ostringstream stream;
dateValue.write(stream);
ASSERT_EQ("2021-12-01", stream.str());
}
TEST(ADateValue, writesVeryOldDateToExtendedFormat)
{
const DateValue dateValue (1, 1, 1);
std::ostringstream stream;
dateValue.write(stream);
ASSERT_EQ("0001-01-01", stream.str());
}
TEST(ADateValue, copyToByteBuffer)
TEST(ADateValue, copiesToByteBufferWithBasicFormat)
{
const DateValue dateValue (2018, 4, 2);
const byte expectedDate[8] = {0x32, 0x30, 0x31, 0x38, 0x30, 0x34, 0x30, 0x32 }; // 20180402
byte buffer[8];
ASSERT_EQ(8, dateValue.copy(buffer));
for (int i = 0; i < 8; ++i) {
ASSERT_EQ(expectedDate[i], buffer[i]);
}
const DateValue dateValue (2021, 12, 1);
std::array<byte, 8> buf;
buf.fill(0);
const byte expectedDate[10] = {'2', '0', '2', '1', '1', '2', '0', '1'};
ASSERT_EQ(8, dateValue.copy(buf.data()));
ASSERT_TRUE(std::equal(buf.begin(), buf.end(), expectedDate));
}
// I used https://www.epochconverter.com/ for knowing the expectations
/* These functions convert the time to the local calendar time. Find a way to do the conversions with UTC
TEST(ADateValue, toLong)
{
const DateValue dateValue (2021, 12, 1);
long val = dateValue.toLong();
ASSERT_EQ(1638313200, val);
}
TEST(ADateValue, toFloat)
{
const DateValue dateValue (2021, 12, 1);
long val = dateValue.toFloat();
ASSERT_FLOAT_EQ(1638313200.f, val);
}
TEST(ADateValue, toRational)
{
const DateValue dateValue (2021, 12, 1);
auto val = dateValue.toRational();
ASSERT_EQ(1638313200, val.first);
ASSERT_EQ(1, val.second);
}
*/

@ -44,7 +44,37 @@ TEST(ATimeValue, isConstructedWithArgs)
/// \todo add tests to check what happen with values out of valid ranges
TEST(ATimeValue, canBeReadFromStringHMS)
TEST(ATimeValue, canBeReadFromCompleteBasicFormatString)
{
TimeValue value;
const std::string hms("235502");
ASSERT_EQ(0, value.read(hms));
ASSERT_EQ(23, value.getTime().hour);
ASSERT_EQ(55, value.getTime().minute);
ASSERT_EQ(2, value.getTime().second);
}
TEST(ATimeValue, canBeReadFromReducedBasicFormatString_HHMM)
{
TimeValue value;
const std::string hms("2355");
ASSERT_EQ(0, value.read(hms));
ASSERT_EQ(23, value.getTime().hour);
ASSERT_EQ(55, value.getTime().minute);
ASSERT_EQ(0, value.getTime().second);
}
TEST(ATimeValue, canBeReadFromReducedBasicFormatString_HH)
{
TimeValue value;
const std::string hms("23");
ASSERT_EQ(0, value.read(hms));
ASSERT_EQ(23, value.getTime().hour);
ASSERT_EQ(0, value.getTime().minute);
ASSERT_EQ(0, value.getTime().second);
}
TEST(ATimeValue, canBeReadFromCompleteExtendedFormatString)
{
TimeValue value;
const std::string hms("23:55:02");
@ -52,45 +82,86 @@ TEST(ATimeValue, canBeReadFromStringHMS)
ASSERT_EQ(23, value.getTime().hour);
ASSERT_EQ(55, value.getTime().minute);
ASSERT_EQ(2, value.getTime().second);
ASSERT_EQ(0, value.getTime().tzHour);
}
TEST(ATimeValue, canBeReadFromReducedExtendedFormatString_HHMM)
{
TimeValue value;
const std::string hms("23:55");
ASSERT_EQ(0, value.read(hms));
ASSERT_EQ(23, value.getTime().hour);
ASSERT_EQ(55, value.getTime().minute);
ASSERT_EQ(0, value.getTime().second);
}
TEST(ATimeValue, canBeReadFromBasicStringWithTimeZoneDesignatorPositive)
{
TimeValue value;
std::string hms("152746+0100");
ASSERT_EQ(0, value.read(hms));
ASSERT_EQ(15, value.getTime().hour);
ASSERT_EQ(27, value.getTime().minute);
ASSERT_EQ(46, value.getTime().second);
ASSERT_EQ(1, value.getTime().tzHour);
ASSERT_EQ(0, value.getTime().tzMinute);
value = TimeValue();
hms = "152746+02";
ASSERT_EQ(0, value.read(hms));
ASSERT_EQ(15, value.getTime().hour);
ASSERT_EQ(27, value.getTime().minute);
ASSERT_EQ(46, value.getTime().second);
ASSERT_EQ(2, value.getTime().tzHour);
ASSERT_EQ(0, value.getTime().tzMinute);
}
TEST(ATimeValue, canBeReadFromWideString)
TEST(ATimeValue, canBeReadFromExtendedStringWithTimeZoneDesignatorPositive)
{
TimeValue value;
const std::string hms("23:55:02+04:04");
std::string hms("23:55:02+04:04");
ASSERT_EQ(0, value.read(hms));
ASSERT_EQ(23, value.getTime().hour);
ASSERT_EQ(55, value.getTime().minute);
ASSERT_EQ(2, value.getTime().second);
ASSERT_EQ(4, value.getTime().tzHour);
ASSERT_EQ(4, value.getTime().tzMinute);
value = TimeValue();
hms = "23:44:03+04";
ASSERT_EQ(0, value.read(hms));
ASSERT_EQ(23, value.getTime().hour);
ASSERT_EQ(44, value.getTime().minute);
ASSERT_EQ(3, value.getTime().second);
ASSERT_EQ(4, value.getTime().tzHour);
ASSERT_EQ(0, value.getTime().tzMinute);
}
TEST(ATimeValue, canBeReadFromWideStringNegative)
TEST(ATimeValue, canBeReadFromExtendedStringWithTimeZoneDesignatorNegative)
{
TimeValue value;
const std::string hms("23:55:02-04:04");
std::string hms("23:55:02-04:04");
ASSERT_EQ(0, value.read(hms));
ASSERT_EQ(23, value.getTime().hour);
ASSERT_EQ(55, value.getTime().minute);
ASSERT_EQ(2, value.getTime().second);
ASSERT_EQ(-4, value.getTime().tzHour);
ASSERT_EQ(-4, value.getTime().tzMinute);
value = TimeValue();
hms = "23:44:03-04";
ASSERT_EQ(0, value.read(hms));
ASSERT_EQ(23, value.getTime().hour);
ASSERT_EQ(44, value.getTime().minute);
ASSERT_EQ(3, value.getTime().second);
ASSERT_EQ(-4, value.getTime().tzHour);
ASSERT_EQ(0, value.getTime().tzMinute);
}
/// \todo check what we should do here.
TEST(ATimeValue, canBeReadFromWideStringOther)
TEST(ATimeValue, cannotBeReadFromStringWithTimeZoneDesignatorWithoutSymbol)
{
TimeValue value;
const std::string hms("23:55:02?04:04");
ASSERT_EQ(0, value.read(hms));
ASSERT_EQ(23, value.getTime().hour);
ASSERT_EQ(55, value.getTime().minute);
ASSERT_EQ(2, value.getTime().second);
ASSERT_EQ(4, value.getTime().tzHour);
ASSERT_EQ(4, value.getTime().tzMinute);
ASSERT_EQ(1, value.read(hms));
}
TEST(ATimeValue, cannotReadFromStringWithBadFormat)

Loading…
Cancel
Save