exiv2/src/exiv2.cpp
Dan Čermák 4b24e011dc Free potentially allocated regexes from Params::evalGrep
In Params::evalGrep regcomp allocates some internal space for the regex objects,
which must be freed manually via regfree(). This was however only done when the
call to regcomp() failed and not on success. As the regexes are needed later,
they have to be deallocated by the destructor.
2018-05-17 17:17:37 +02:00

1498 lines
55 KiB
C++

// ***************************************************************** -*- C++ -*-
/*
* Copyright (C) 2004-2017 Andreas Huggel <ahuggel@gmx.net>
*
* This program is part of the Exiv2 distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA.
*/
/*
Abstract: Command line program to display and manipulate image metadata.
File: exiv2.cpp
Author(s): Andreas Huggel (ahu) <ahuggel@gmx.net>
History: 10-Dec-03, ahu: created
*/
// *****************************************************************************
// included header files
#include "config.h"
#include "exiv2app.hpp"
#include "actions.hpp"
#include "utils.hpp"
#include "convert.hpp"
#include "i18n.h" // NLS support.
#include "xmp_exiv2.hpp"
#include <iostream>
#include <fstream>
#include <iomanip>
#include <cstring>
#include <cassert>
#include <cctype>
#if defined(EXV_HAVE_REGEX_H)
#include <regex.h>
#endif
// *****************************************************************************
// local declarations
namespace {
//! List of all command identifiers and corresponding strings
static const CmdIdAndString cmdIdAndString[] = {
{ add, "add" },
{ set, "set" },
{ del, "del" },
{ reg, "reg" },
{ invalidCmdId, "invalidCmd" } // End of list marker
};
// Return a command Id for a command string
CmdId commandId(const std::string& cmdString);
// Evaluate [-]HH[:MM[:SS]], returns true and sets time to the value
// in seconds if successful, else returns false.
bool parseTime(const std::string& ts, long& time);
/*!
@brief Parse the oparg string into a bitmap of common targets.
@param optarg Option arguments
@param action Action being processed
@return A bitmap of common targets or -1 in case of a parse error
*/
int parseCommonTargets(const std::string& optarg,
const std::string& action);
/*!
@brief Parse numbers separated by commas into container
@param previewNumbers Container for the numbers
@param optarg Option arguments
@param j Starting index into optarg
@return Number of characters processed
*/
int parsePreviewNumbers(Params::PreviewNumbers& previewNumbers,
const std::string& optarg,
int j);
/*!
@brief Parse metadata modification commands from multiple files
@param modifyCmds Reference to a structure to store the parsed commands
@param cmdFiles Container with the file names
*/
bool parseCmdFiles(ModifyCmds& modifyCmds,
const Params::CmdFiles& cmdFiles);
/*!
@brief Parse metadata modification commands from a container of commands
@param modifyCmds Reference to a structure to store the parsed commands
@param cmdLines Container with the commands
*/
bool parseCmdLines(ModifyCmds& modifyCmds,
const Params::CmdLines& cmdLines);
/*!
@brief Parse one line of the command file
@param modifyCmd Reference to a command structure to store the parsed
command
@param line Input line
@param num Line number (used for error output)
*/
bool parseLine(ModifyCmd& modifyCmd,
const std::string& line, int num);
/*!
@brief Parses a string containing backslash-escapes
@param input Input string, assumed to be UTF-8
*/
std::string parseEscapes(const std::string& input);
}
// *****************************************************************************
// Main
int main(int argc, char* const argv[])
{
#ifdef EXV_ENABLE_NLS
setlocale(LC_ALL, "");
bindtextdomain(EXV_PACKAGE, EXV_LOCALEDIR);
textdomain(EXV_PACKAGE);
#endif
// Handle command line arguments
Params& params = Params::instance();
if (params.getopt(argc, argv)) {
params.usage();
return 1;
}
if (params.help_) {
params.help();
return 0;
}
if (params.version_) {
params.version(params.verbose_);
return 0;
}
// Create the required action class
Action::TaskFactory& taskFactory = Action::TaskFactory::instance();
Action::Task::AutoPtr task
= taskFactory.create(Action::TaskType(params.action_));
assert(task.get());
// Process all files
int rc = 0;
int n = 1;
int s = static_cast<int>(params.files_.size());
int w = s > 9 ? s > 99 ? 3 : 2 : 1;
for (Params::Files::const_iterator i = params.files_.begin();
i != params.files_.end(); ++i) {
if (params.verbose_) {
std::cout << _("File") << " " << std::setw(w) << std::right << n++ << "/" << s << ": "
<< *i << std::endl;
}
int ret = task->run(*i);
if (rc == 0) rc = ret;
}
taskFactory.cleanup();
params.cleanup();
Exiv2::XmpParser::terminate();
// Return a positive one byte code for better consistency across platforms
return static_cast<unsigned int>(rc) % 256;
} // main
// *****************************************************************************
// class Params
Params* Params::instance_ = 0;
const Params::YodAdjust Params::emptyYodAdjust_[] = {
{ false, "-Y", 0 },
{ false, "-O", 0 },
{ false, "-D", 0 },
};
Params& Params::instance()
{
if (0 == instance_) {
instance_ = new Params;
}
return *instance_;
}
Params::~Params() {
#if defined(EXV_HAVE_REGEX_H)
for (size_t i=0; i<instance().greps_.size(); ++i) {
regfree(&instance().greps_.at(i));
}
#endif
}
void Params::cleanup()
{
delete instance_;
instance_ = 0;
}
void Params::version(bool verbose,std::ostream& os) const
{
bool b64 = sizeof(void*)==8;
const char* sBuild = b64 ? "(64 bit build)" : "(32 bit build)" ;
os << EXV_PACKAGE_STRING << " " << Exiv2::versionNumberHexString() << " " << sBuild << "\n";
if ( Params::instance().greps_.empty() ) {
os << _("Copyright (C) 2004-2017 Andreas Huggel.\n")
<< "\n"
<< _("This program is free software; you can redistribute it and/or\n"
"modify it under the terms of the GNU General Public License\n"
"as published by the Free Software Foundation; either version 2\n"
"of the License, or (at your option) any later version.\n")
<< "\n"
<< _("This program is distributed in the hope that it will be useful,\n"
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
"GNU General Public License for more details.\n")
<< "\n"
<< _("You should have received a copy of the GNU General Public\n"
"License along with this program; if not, write to the Free\n"
"Software Foundation, Inc., 51 Franklin Street, Fifth Floor,\n"
"Boston, MA 02110-1301 USA\n");
}
if ( verbose ) Exiv2::dumpLibraryInfo(os,Params::instance().greps_);
}
void Params::usage(std::ostream& os) const
{
os << _("Usage:") << " " << progname()
<< " " << _("[ options ] [ action ] file ...\n\n")
<< _("Manipulate the Exif metadata of images.\n");
}
std::string Params::printTarget(std::string before,int target,bool bPrint,std::ostream& out)
{
std::string t;
if ( target & Params::ctExif ) t+= 'e';
if ( target & Params::ctXmpSidecar ) t+= 'X';
if ( target & Params::ctXmpRaw ) t+= target & Params::ctXmpSidecar ? 'X' : 'R' ;
if ( target & Params::ctIptc ) t+= 'i';
if ( target & Params::ctIccProfile ) t+= 'C';
if ( target & Params::ctIptcRaw ) t+= 'I';
if ( target & Params::ctXmp ) t+= 'x';
if ( target & Params::ctComment ) t+= 'c';
if ( target & Params::ctThumb ) t+= 't';
if ( target & Params::ctPreview ) t+= 'p';
if ( target & Params::ctStdInOut ) t+= '-';
if ( bPrint ) out << before << " :" << t << std::endl;
return t;
}
void Params::help(std::ostream& os) const
{
usage(os);
os << _("\nActions:\n")
<< _(" ad | adjust Adjust Exif timestamps by the given time. This action\n"
" requires at least one of the -a, -Y, -O or -D options.\n")
<< _(" pr | print Print image metadata.\n")
<< _(" rm | delete Delete image metadata from the files.\n")
<< _(" in | insert Insert metadata from corresponding *.exv files.\n"
" Use option -S to change the suffix of the input files.\n")
<< _(" ex | extract Extract metadata to *.exv, *.xmp and thumbnail image files.\n")
<< _(" mv | rename Rename files and/or set file timestamps according to the\n"
" Exif create timestamp. The filename format can be set with\n"
" -r format, timestamp options are controlled with -t and -T.\n")
<< _(" mo | modify Apply commands to modify (add, set, delete) the Exif and\n"
" IPTC metadata of image files or set the JPEG comment.\n"
" Requires option -c, -m or -M.\n")
<< _(" fi | fixiso Copy ISO setting from the Nikon Makernote to the regular\n"
" Exif tag.\n")
<< _(" fc | fixcom Convert the UNICODE Exif user comment to UCS-2. Its current\n"
" character encoding can be specified with the -n option.\n")
<< _("\nOptions:\n")
<< _(" -h Display this help and exit.\n")
<< _(" -V Show the program version and exit.\n")
<< _(" -v Be verbose during the program run.\n")
<< _(" -q Silence warnings and error messages during the program run (quiet).\n")
<< _(" -Q lvl Set log-level to d(ebug), i(nfo), w(arning), e(rror) or m(ute).\n")
<< _(" -b Show large binary values.\n")
<< _(" -u Show unknown tags.\n")
<< _(" -g key Only output info for this key (grep).\n")
<< _(" -K key Only output info for this key (exact match).\n")
<< _(" -n enc Charset to use to decode UNICODE Exif user comments.\n")
<< _(" -k Preserve file timestamps (keep).\n")
<< _(" -t Also set the file timestamp in 'rename' action (overrides -k).\n")
<< _(" -T Only set the file timestamp in 'rename' action, do not rename\n"
" the file (overrides -k).\n")
<< _(" -f Do not prompt before overwriting existing files (force).\n")
<< _(" -F Do not prompt before renaming files (Force).\n")
<< _(" -a time Time adjustment in the format [-]HH[:MM[:SS]]. This option\n"
" is only used with the 'adjust' action.\n")
<< _(" -Y yrs Year adjustment with the 'adjust' action.\n")
<< _(" -O mon Month adjustment with the 'adjust' action.\n")
<< _(" -D day Day adjustment with the 'adjust' action.\n")
<< _(" -p mode Print mode for the 'print' action. Possible modes are:\n")
<< _(" s : print a summary of the Exif metadata (the default)\n")
<< _(" a : print Exif, IPTC and XMP metadata (shortcut for -Pkyct)\n")
<< _(" e : print Exif metadata (shortcut for -PEkycv)\n")
<< _(" t : interpreted (translated) Exif data (-PEkyct)\n")
<< _(" v : plain Exif data values (-PExgnycv)\n")
<< _(" h : hexdump of the Exif data (-PExgnycsh)\n")
<< _(" i : IPTC data values (-PIkyct)\n")
<< _(" x : XMP properties (-PXkyct)\n")
<< _(" c : JPEG comment\n")
<< _(" p : list available previews\n")
<< _(" C : print ICC profile embedded in image\n")
<< _(" R : recursive print structure of image\n")
<< _(" S : print structure of image\n")
<< _(" X : extract XMP from image\n")
<< _(" -P flgs Print flags for fine control of tag lists ('print' action):\n")
<< _(" E : include Exif tags in the list\n")
<< _(" I : IPTC datasets\n")
<< _(" X : XMP properties\n")
<< _(" x : print a column with the tag number\n")
<< _(" g : group name\n")
<< _(" k : key\n")
<< _(" l : tag label\n")
<< _(" n : tag name\n")
<< _(" y : type\n")
<< _(" c : number of components (count)\n")
<< _(" s : size in bytes\n")
<< _(" v : plain data value\n")
<< _(" t : interpreted (translated) data\n")
<< _(" h : hexdump of the data\n")
<< _(" -d tgt Delete target(s) for the 'delete' action. Possible targets are:\n")
<< _(" a : all supported metadata (the default)\n")
<< _(" e : Exif section\n")
<< _(" t : Exif thumbnail only\n")
<< _(" i : IPTC data\n")
<< _(" x : XMP packet\n")
<< _(" c : JPEG comment\n")
<< _(" -i tgt Insert target(s) for the 'insert' action. Possible targets are\n"
" the same as those for the -d option, plus a modifier:\n"
" X : Insert metadata from an XMP sidecar file <file>.xmp\n"
" Only JPEG thumbnails can be inserted, they need to be named\n"
" <file>-thumb.jpg\n")
<< _(" -e tgt Extract target(s) for the 'extract' action. Possible targets\n"
" are the same as those for the -d option, plus a target to extract\n"
" preview images and a modifier to generate an XMP sidecar file:\n"
" p[<n>[,<m> ...]] : Extract preview images.\n"
" X : Extract metadata to an XMP sidecar file <file>.xmp\n")
<< _(" -r fmt Filename format for the 'rename' action. The format string\n"
" follows strftime(3). The following keywords are supported:\n")
<< _(" :basename: - original filename without extension\n")
<< _(" :dirname: - name of the directory holding the original file\n")
<< _(" :parentname: - name of parent directory\n")
<< _(" Default filename format is ")
<< format_ << ".\n"
<< _(" -c txt JPEG comment string to set in the image.\n")
<< _(" -m file Command file for the modify action. The format for commands is\n"
" set|add|del <key> [[<type>] <value>].\n")
<< _(" -M cmd Command line for the modify action. The format for the\n"
" commands is the same as that of the lines of a command file.\n")
<< _(" -l dir Location (directory) for files to be inserted from or extracted to.\n")
<< _(" -S .suf Use suffix .suf for source files for insert command.\n\n");
} // Params::help
int Params::option(int opt, const std::string& optarg, int optopt)
{
int rc = 0;
switch (opt) {
case 'h': help_ = true; break;
case 'V': version_ = true; break;
case 'v': verbose_ = true; break;
case 'q': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::mute); break;
case 'Q': rc = setLogLevel(optarg); break;
case 'k': preserve_ = true; break;
case 'b': binary_ = false; break;
case 'u': unknown_ = false; break;
case 'f': force_ = true; fileExistsPolicy_ = overwritePolicy; break;
case 'F': force_ = true; fileExistsPolicy_ = renamePolicy; break;
case 'g': rc = evalGrep(optarg); break;
case 'K': rc = evalKey(optarg); printMode_ = pmList; break;
case 'n': charset_ = optarg; break;
case 'r': rc = evalRename(opt, optarg); break;
case 't': rc = evalRename(opt, optarg); break;
case 'T': rc = evalRename(opt, optarg); break;
case 'a': rc = evalAdjust(optarg); break;
case 'Y': rc = evalYodAdjust(yodYear, optarg); break;
case 'O': rc = evalYodAdjust(yodMonth, optarg); break;
case 'D': rc = evalYodAdjust(yodDay, optarg); break;
case 'p': rc = evalPrint(optarg); break;
case 'P': rc = evalPrintFlags(optarg); break;
case 'd': rc = evalDelete(optarg); break;
case 'e': rc = evalExtract(optarg); break;
case 'C': rc = evalExtract(optarg); break;
case 'i': rc = evalInsert(optarg); break;
case 'c': rc = evalModify(opt, optarg); break;
case 'm': rc = evalModify(opt, optarg); break;
case 'M': rc = evalModify(opt, optarg); break;
case 'l': directory_ = optarg; break;
case 'S': suffix_ = optarg; break;
case ':':
std::cerr << progname() << ": " << _("Option") << " -" << static_cast<char>(optopt)
<< " " << _("requires an argument\n");
rc = 1;
break;
case '?':
std::cerr << progname() << ": " << _("Unrecognized option") << " -"
<< static_cast<char>(optopt) << "\n";
rc = 1;
break;
default:
std::cerr << progname()
<< ": " << _("getopt returned unexpected character code") << " "
<< std::hex << opt << "\n";
rc = 1;
break;
}
return rc;
} // Params::option
int Params::setLogLevel(const std::string& optarg)
{
int rc = 0;
const char logLevel = tolower(optarg[0]);
switch (logLevel) {
case 'd': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::debug); break;
case 'i': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::info); break;
case 'w': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::warn); break;
case 'e': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::error); break;
case 'm': Exiv2::LogMsg::setLevel(Exiv2::LogMsg::mute); break;
default:
std::cerr << progname() << ": " << _("Option") << " -Q: "
<< _("Invalid argument") << " \"" << optarg << "\"\n";
rc = 1;
break;
}
return rc;
} // Params::setLogLevel
// http://stackoverflow.com/questions/874134/find-if-string-ends-with-another-string-in-c
static inline bool ends_with(std::string const & value, std::string const & ending,std::string& stub)
{
if (ending.size() > value.size()) return false;
bool bResult = std::equal(ending.rbegin(), ending.rend(), value.rbegin());
stub = bResult ? value.substr(0,value.length() - ending.length()) : value;
return bResult ;
}
int Params::evalGrep( const std::string& optarg)
{
int result=0;
std::string pattern;
std::string ignoreCase("/i");
bool bIgnoreCase = ends_with(optarg,ignoreCase,pattern);
#if defined(EXV_HAVE_REGEX_H)
// try to compile a reg-exp from the input argument and store it in the vector
const size_t i = greps_.size();
greps_.resize(i + 1);
regex_t *pRegex = &greps_[i];
int errcode = regcomp( pRegex, pattern.c_str(), bIgnoreCase ? REG_NOSUB|REG_ICASE : REG_NOSUB);
// there was an error compiling the regexp
if( errcode ) {
size_t length = regerror (errcode, pRegex, NULL, 0);
char *buffer = new char[ length];
regerror (errcode, pRegex, buffer, length);
std::cerr << progname()
<< ": " << _("Option") << " -g: "
<< _("Invalid regexp") << " \"" << optarg << "\": " << buffer << "\n";
// free the memory and drop the regexp
delete[] buffer;
regfree( pRegex);
greps_.resize(i);
result=1;
}
#else
greps_.push_back(Exiv2_grep_key_t(pattern,bIgnoreCase));
#endif
return result;
} // Params::evalGrep
int Params::evalKey( const std::string& optarg)
{
int result=0;
keys_.push_back(optarg);
return result;
} // Params::evalKey
int Params::evalRename(int opt, const std::string& optarg)
{
int rc = 0;
switch (action_) {
case Action::none:
action_ = Action::rename;
switch (opt) {
case 'r':
format_ = optarg;
formatSet_ = true;
break;
case 't': timestamp_ = true; break;
case 'T': timestampOnly_ = true; break;
}
break;
case Action::rename:
if (opt == 'r' && (formatSet_ || timestampOnly_)) {
std::cerr << progname()
<< ": " << _("Ignoring surplus option") << " -r \"" << optarg << "\"\n";
}
else {
format_ = optarg;
formatSet_ = true;
}
break;
default:
std::cerr << progname()
<< ": " << _("Option") << " -" << (char)opt
<< " " << _("is not compatible with a previous option\n");
rc = 1;
break;
}
return rc;
} // Params::evalRename
int Params::evalAdjust(const std::string& optarg)
{
int rc = 0;
switch (action_) {
case Action::none:
case Action::adjust:
if (adjust_) {
std::cerr << progname()
<< ": " << _("Ignoring surplus option -a") << " " << optarg << "\n";
break;
}
action_ = Action::adjust;
adjust_ = parseTime(optarg, adjustment_);
if (!adjust_) {
std::cerr << progname() << ": " << _("Error parsing -a option argument") << " `"
<< optarg << "'\n";
rc = 1;
}
break;
default:
std::cerr << progname()
<< ": " << _("Option -a is not compatible with a previous option\n");
rc = 1;
break;
}
return rc;
} // Params::evalAdjust
int Params::evalYodAdjust(const Yod& yod, const std::string& optarg)
{
int rc = 0;
switch (action_) {
case Action::none: // fall-through
case Action::adjust:
if (yodAdjust_[yod].flag_) {
std::cerr << progname()
<< ": " << _("Ignoring surplus option") << " "
<< yodAdjust_[yod].option_ << " " << optarg << "\n";
break;
}
action_ = Action::adjust;
yodAdjust_[yod].flag_ = true;
if (!Util::strtol(optarg.c_str(), yodAdjust_[yod].adjustment_)) {
std::cerr << progname() << ": " << _("Error parsing") << " "
<< yodAdjust_[yod].option_ << " "
<< _("option argument") << " `" << optarg << "'\n";
rc = 1;
}
break;
default:
std::cerr << progname()
<< ": " << _("Option") << " "
<< yodAdjust_[yod].option_ << " "
<< _("is not compatible with a previous option\n");
rc = 1;
break;
}
return rc;
} // Params::evalYodAdjust
int Params::evalPrint(const std::string& optarg)
{
int rc = 0;
switch (action_) {
case Action::none:
switch (optarg[0]) {
case 's': action_ = Action::print; printMode_ = pmSummary; break;
case 'a': rc = evalPrintFlags("kyct"); break;
case 'e': rc = evalPrintFlags("Ekycv"); break;
case 't': rc = evalPrintFlags("Ekyct"); break;
case 'v': rc = evalPrintFlags("Exgnycv"); break;
case 'h': rc = evalPrintFlags("Exgnycsh"); break;
case 'i': rc = evalPrintFlags("Ikyct"); break;
case 'x': rc = evalPrintFlags("Xkyct"); break;
case 'c': action_ = Action::print; printMode_ = pmComment ; break;
case 'p': action_ = Action::print; printMode_ = pmPreview ; break;
case 'C': action_ = Action::print; printMode_ = pmIccProfile ; break;
case 'R': action_ = Action::print; printMode_ = pmRecursive ; break;
case 'S': action_ = Action::print; printMode_ = pmStructure ; break;
case 'X': action_ = Action::print; printMode_ = pmXMP ; break;
default:
std::cerr << progname() << ": " << _("Unrecognized print mode") << " `"
<< optarg << "'\n";
rc = 1;
break;
}
break;
case Action::print:
std::cerr << progname() << ": "
<< _("Ignoring surplus option -p") << optarg << "\n";
break;
default:
std::cerr << progname() << ": "
<< _("Option -p is not compatible with a previous option\n");
rc = 1;
break;
}
return rc;
} // Params::evalPrint
int Params::evalPrintFlags(const std::string& optarg)
{
int rc = 0;
switch (action_) {
case Action::none:
action_ = Action::print;
printMode_ = pmList;
for (std::size_t i = 0; i < optarg.length(); ++i) {
switch (optarg[i]) {
case 'E': printTags_ |= Exiv2::mdExif; break;
case 'I': printTags_ |= Exiv2::mdIptc; break;
case 'X': printTags_ |= Exiv2::mdXmp; break;
case 'x': printItems_ |= prTag; break;
case 'g': printItems_ |= prGroup; break;
case 'k': printItems_ |= prKey; break;
case 'l': printItems_ |= prLabel; break;
case 'n': printItems_ |= prName; break;
case 'y': printItems_ |= prType; break;
case 'c': printItems_ |= prCount; break;
case 's': printItems_ |= prSize; break;
case 'v': printItems_ |= prValue; break;
case 't': printItems_ |= prTrans; break;
case 'h': printItems_ |= prHex; break;
case 'V': printItems_ |= prSet|prValue;break;
default:
std::cerr << progname() << ": " << _("Unrecognized print item") << " `"
<< optarg[i] << "'\n";
rc = 1;
break;
}
}
break;
case Action::print:
std::cerr << progname() << ": "
<< _("Ignoring surplus option -P") << optarg << "\n";
break;
default:
std::cerr << progname() << ": "
<< _("Option -P is not compatible with a previous option\n");
rc = 1;
break;
}
return rc;
} // Params::evalPrintFlags
int Params::evalDelete(const std::string& optarg)
{
int rc = 0;
switch (action_) {
case Action::none:
action_ = Action::erase;
target_ = 0;
// fallthrough
case Action::erase:
rc = parseCommonTargets(optarg, "erase");
if (rc > 0) {
target_ |= rc;
rc = 0;
}
else {
rc = 1;
}
break;
default:
std::cerr << progname() << ": "
<< _("Option -d is not compatible with a previous option\n");
rc = 1;
break;
}
return rc;
} // Params::evalDelete
int Params::evalExtract(const std::string& optarg)
{
int rc = 0;
switch (action_) {
case Action::none:
case Action::modify:
action_ = Action::extract;
target_ = 0;
// fallthrough
case Action::extract:
rc = parseCommonTargets(optarg, "extract");
if (rc > 0) {
target_ |= rc;
rc = 0;
}
else {
rc = 1;
}
break;
default:
std::cerr << progname() << ": "
<< _("Option -e is not compatible with a previous option\n");
rc = 1;
break;
}
return rc;
} // Params::evalExtract
int Params::evalInsert(const std::string& optarg)
{
int rc = 0;
switch (action_) {
case Action::none:
case Action::modify:
action_ = Action::insert;
target_ = 0;
// fallthrough
case Action::insert:
rc = parseCommonTargets(optarg, "insert");
if (rc > 0) {
target_ |= rc;
rc = 0;
}
else {
rc = 1;
}
break;
default:
std::cerr << progname() << ": "
<< _("Option -i is not compatible with a previous option\n");
rc = 1;
break;
}
return rc;
} // Params::evalInsert
int Params::evalModify(int opt, const std::string& optarg)
{
int rc = 0;
switch (action_) {
case Action::none:
action_ = Action::modify;
// fallthrough
case Action::modify:
case Action::extract:
case Action::insert:
if (opt == 'c') jpegComment_ = parseEscapes(optarg);
if (opt == 'm') cmdFiles_.push_back(optarg); // parse the files later
if (opt == 'M') cmdLines_.push_back(optarg); // parse the commands later
break;
default:
std::cerr << progname() << ": "
<< _("Option") << " -" << (char)opt << " "
<< _("is not compatible with a previous option\n");
rc = 1;
break;
}
return rc;
} // Params::evalModify
int Params::nonoption(const std::string& argv)
{
int rc = 0;
bool action = false;
if (first_) {
// The first non-option argument must be the action
first_ = false;
if (argv == "ad" || argv == "adjust") {
if (action_ != Action::none && action_ != Action::adjust) {
std::cerr << progname() << ": "
<< _("Action adjust is not compatible with the given options\n");
rc = 1;
}
action = true;
action_ = Action::adjust;
}
if (argv == "pr" || argv == "print") {
if (action_ != Action::none && action_ != Action::print) {
std::cerr << progname() << ": "
<< _("Action print is not compatible with the given options\n");
rc = 1;
}
action = true;
action_ = Action::print;
}
if (argv == "rm" || argv == "delete") {
if (action_ != Action::none && action_ != Action::erase) {
std::cerr << progname() << ": "
<< _("Action delete is not compatible with the given options\n");
rc = 1;
}
action = true;
action_ = Action::erase;
}
if (argv == "ex" || argv == "extract") {
if ( action_ != Action::none
&& action_ != Action::extract
&& action_ != Action::modify) {
std::cerr << progname() << ": "
<< _("Action extract is not compatible with the given options\n");
rc = 1;
}
action = true;
action_ = Action::extract;
}
if (argv == "in" || argv == "insert") {
if ( action_ != Action::none
&& action_ != Action::insert
&& action_ != Action::modify) {
std::cerr << progname() << ": "
<< _("Action insert is not compatible with the given options\n");
rc = 1;
}
action = true;
action_ = Action::insert;
}
if (argv == "mv" || argv == "rename") {
if (action_ != Action::none && action_ != Action::rename) {
std::cerr << progname() << ": "
<< _("Action rename is not compatible with the given options\n");
rc = 1;
}
action = true;
action_ = Action::rename;
}
if (argv == "mo" || argv == "modify") {
if (action_ != Action::none && action_ != Action::modify) {
std::cerr << progname() << ": "
<< _("Action modify is not compatible with the given options\n");
rc = 1;
}
action = true;
action_ = Action::modify;
}
if (argv == "fi" || argv == "fixiso") {
if (action_ != Action::none && action_ != Action::fixiso) {
std::cerr << progname() << ": "
<< _("Action fixiso is not compatible with the given options\n");
rc = 1;
}
action = true;
action_ = Action::fixiso;
}
if (argv == "fc" || argv == "fixcom" || argv == "fixcomment") {
if (action_ != Action::none && action_ != Action::fixcom) {
std::cerr << progname() << ": "
<< _("Action fixcom is not compatible with the given options\n");
rc = 1;
}
action = true;
action_ = Action::fixcom;
}
if (action_ == Action::none) {
// if everything else fails, assume print as the default action
action_ = Action::print;
}
}
if (!action) {
files_.push_back(argv);
}
return rc;
} // Params::nonoption
static int readFileToBuf(FILE* f,Exiv2::DataBuf& buf)
{
const int buff_size = 4*1028;
Exiv2::byte* bytes = (Exiv2::byte*)::malloc(buff_size);
int nBytes = 0 ;
bool more = bytes != NULL;
while ( more ) {
char buff[buff_size];
int n = (int) fread(buff,1,buff_size,f);
more = n > 0 ;
if ( more ) {
bytes = (Exiv2::byte*) realloc(bytes,nBytes+n);
memcpy(bytes+nBytes,buff,n);
nBytes += n ;
}
}
if ( nBytes ) {
buf.alloc(nBytes);
memcpy(buf.pData_,(const void*)bytes,nBytes);
}
if ( bytes != NULL ) ::free(bytes) ;
return nBytes;
}
//#define DEBUG
void Params::getStdin(Exiv2::DataBuf& buf)
{
// copy stdin to stdinBuf
if ( stdinBuf.size_ == 0 ) {
#if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW__) || defined(_MSC_VER)
DWORD fdwMode;
_setmode(fileno(stdin), O_BINARY);
Sleep(300);
if ( !GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &fdwMode) ) { // failed: stdin has bytes!
#else
// http://stackoverflow.com/questions/34479795/make-c-not-wait-for-user-input/34479916#34479916
fd_set readfds;
FD_ZERO (&readfds);
FD_SET(STDIN_FILENO, &readfds);
struct timeval timeout = {1,0}; // yes: set timeout seconds,microseconds
// if we have something in the pipe, read it
if (select(1, &readfds, NULL, NULL, &timeout)) {
#endif
#ifdef DEBUG
std::cerr << "stdin has data" << std::endl;
#endif
readFileToBuf(stdin,stdinBuf);
}
#ifdef DEBUG
// this is only used to simulate reading from stdin when debugging
// to simulate exiv2 -pX foo.jpg | exiv2 -iXX- bar.jpg
// exiv2 -pX foo.jpg > ~/temp/stdin ; exiv2 -iXX- bar.jpg
if ( stdinBuf.size_ == 0 ) {
const char* path = "/Users/rmills/temp/stdin";
FILE* f = fopen(path,"rb");
if ( f ) {
readFileToBuf(f,stdinBuf);
fclose(f);
std::cerr << "read stdin from " << path << std::endl;
}
}
#endif
#ifdef DEBUG
std::cerr << "getStdin stdinBuf.size_ = " << stdinBuf.size_ << std::endl;
#endif
}
// copy stdinBuf to buf
if ( stdinBuf.size_ ) {
buf.alloc(stdinBuf.size_);
memcpy(buf.pData_,stdinBuf.pData_,buf.size_);
}
#ifdef DEBUG
std::cerr << "getStdin stdinBuf.size_ = " << stdinBuf.size_ << std::endl;
#endif
} // Params::getStdin()
typedef std::map<std::string,std::string> long_t;
int Params::getopt(int argc, char* const Argv[])
{
char** argv = new char* [argc+1];
argv[argc] = NULL;
long_t longs;
longs["--adjust" ] = "-a";
longs["--binary" ] = "-b";
longs["--comment" ] = "-c";
longs["--delete" ] = "-d";
longs["--days" ] = "-D";
longs["--force" ] = "-f";
longs["--Force" ] = "-F";
longs["--grep" ] = "-g";
longs["--help" ] = "-h";
longs["--insert" ] = "-i";
longs["--keep" ] = "-k";
longs["--key" ] = "-K";
longs["--location" ] = "-l";
longs["--modify" ] = "-m";
longs["--Modify" ] = "-M";
longs["--encode" ] = "-n";
longs["--months" ] = "-O";
longs["--print" ] = "-p";
longs["--Print" ] = "-P";
longs["--quiet" ] = "-q";
longs["--log" ] = "-Q";
longs["--rename" ] = "-r";
longs["--suffix" ] = "-S";
longs["--timestamp"] = "-t";
longs["--Timestamp"] = "-T";
longs["--unknown" ] = "-u";
longs["--verbose" ] = "-v";
longs["--Version" ] = "-V";
longs["--version" ] = "-V";
longs["--years" ] = "-Y";
for ( int i = 0 ; i < argc ; i++ ) {
std::string* arg = new std::string(Argv[i]);
if (longs.find(*arg) != longs.end() ) {
argv[i] = ::strdup(longs[*arg].c_str());
} else {
argv[i] = ::strdup(Argv[i]);
}
delete arg;
}
int rc = Util::Getopt::getopt(argc, argv, optstring_);
// Further consistency checks
if (help_ || version_) return 0;
if (action_ == Action::none) {
// This shouldn't happen since print is taken as default action
std::cerr << progname() << ": " << _("An action must be specified\n");
rc = 1;
}
if ( action_ == Action::adjust
&& !adjust_
&& !yodAdjust_[yodYear].flag_
&& !yodAdjust_[yodMonth].flag_
&& !yodAdjust_[yodDay].flag_) {
std::cerr << progname() << ": "
<< _("Adjust action requires at least one -a, -Y, -O or -D option\n");
rc = 1;
}
if ( action_ == Action::modify
&& cmdFiles_.empty() && cmdLines_.empty() && jpegComment_.empty()) {
std::cerr << progname() << ": "
<< _("Modify action requires at least one -c, -m or -M option\n");
rc = 1;
}
if (0 == files_.size()) {
std::cerr << progname() << ": " << _("At least one file is required\n");
rc = 1;
}
if (rc == 0 && !cmdFiles_.empty()) {
// Parse command files
if (!parseCmdFiles(modifyCmds_, cmdFiles_)) {
std::cerr << progname() << ": " << _("Error parsing -m option arguments\n");
rc = 1;
}
}
if (rc == 0 && !cmdLines_.empty()) {
// Parse command lines
if (!parseCmdLines(modifyCmds_, cmdLines_)) {
std::cerr << progname() << ": " << _("Error parsing -M option arguments\n");
rc = 1;
}
}
if (rc == 0 && (!cmdFiles_.empty() || !cmdLines_.empty())) {
// We'll set them again, after reading the file
Exiv2::XmpProperties::unregisterNs();
}
if ( !directory_.empty()
&& !(action_ == Action::insert || action_ == Action::extract)) {
std::cerr << progname() << ": "
<< _("-l option can only be used with extract or insert actions\n");
rc = 1;
}
if (!suffix_.empty() && !(action_ == Action::insert)) {
std::cerr << progname() << ": "
<< _("-S option can only be used with insert action\n");
rc = 1;
}
if (timestamp_ && !(action_ == Action::rename)) {
std::cerr << progname() << ": "
<< _("-t option can only be used with rename action\n");
rc = 1;
}
if (timestampOnly_ && !(action_ == Action::rename)) {
std::cerr << progname() << ": "
<< _("-T option can only be used with rename action\n");
rc = 1;
}
// cleanup the argument vector
for ( int i = 0 ; i < argc ; i++ ) ::free((void*)argv[i]);
delete [] argv;
return rc;
} // Params::getopt
// *****************************************************************************
// local implementations
namespace {
bool parseTime(const std::string& ts, long& time)
{
std::string hstr, mstr, sstr;
char *cts = new char[ts.length() + 1];
strcpy(cts, ts.c_str());
char *tmp = ::strtok(cts, ":");
if (tmp) hstr = tmp;
tmp = ::strtok(0, ":");
if (tmp) mstr = tmp;
tmp = ::strtok(0, ":");
if (tmp) sstr = tmp;
delete[] cts;
int sign = 1;
long hh(0), mm(0), ss(0);
// [-]HH part
if (!Util::strtol(hstr.c_str(), hh)) return false;
if (hh < 0) {
sign = -1;
hh *= -1;
}
// check for the -0 special case
if (hh == 0 && hstr.find('-') != std::string::npos) sign = -1;
// MM part, if there is one
if (mstr != "") {
if (!Util::strtol(mstr.c_str(), mm)) return false;
if (mm > 59) return false;
if (mm < 0) return false;
}
// SS part, if there is one
if (sstr != "") {
if (!Util::strtol(sstr.c_str(), ss)) return false;
if (ss > 59) return false;
if (ss < 0) return false;
}
time = sign * (hh * 3600 + mm * 60 + ss);
return true;
} // parseTime
int parseCommonTargets(const std::string& optarg,
const std::string& action)
{
int rc = 0;
int target = 0;
int all = Params::ctExif | Params::ctIptc | Params::ctComment | Params::ctXmp;
int extra = Params::ctXmpSidecar|Params::ctExif|Params::ctIptc|Params::ctXmp;
for (size_t i = 0; rc == 0 && i < optarg.size(); ++i) {
switch (optarg[i]) {
case 'e': target |= Params::ctExif; break;
case 'i': target |= Params::ctIptc; break;
case 'x': target |= Params::ctXmp; break;
case 'c': target |= Params::ctComment; break;
case 't': target |= Params::ctThumb; break;
case 'C': target |= Params::ctIccProfile; break;
case 'I': target |= Params::ctIptcRaw;break;
case '-': target |= Params::ctStdInOut;break;
case 'a': target |= all ; break;
case 'X': target |= extra ; // -eX
if ( i > 0 ) { // -eXX or -iXX
target |= Params::ctXmpRaw ;
target &= ~extra; // turn off those bits
}
break;
case 'p':
{
if (strcmp(action.c_str(), "extract") == 0) {
i += (size_t) parsePreviewNumbers(Params::instance().previewNumbers_, optarg, (int) i + 1);
target |= Params::ctPreview;
break;
}
// fallthrough
}
default:
std::cerr << Params::instance().progname() << ": " << _("Unrecognized ")
<< action << " " << _("target") << " `" << optarg[i] << "'\n";
rc = -1;
break;
}
}
return rc ? rc : target;
} // parseCommonTargets
int parsePreviewNumbers(Params::PreviewNumbers& previewNumbers,
const std::string& optarg,
int j)
{
size_t k = j;
for (size_t i = j; i < optarg.size(); ++i) {
std::ostringstream os;
for (k = i; k < optarg.size() && isdigit(optarg[k]); ++k) {
os << optarg[k];
}
if (k > i) {
bool ok = false;
int num = Exiv2::stringTo<int>(os.str(), ok);
if (ok && num >= 0) {
previewNumbers.insert(num);
}
else {
std::cerr << Params::instance().progname() << ": "
<< _("Invalid preview number") << ": " << num << "\n";
}
i = k;
}
if (!(k < optarg.size() && optarg[i] == ',')) break;
}
int ret = static_cast<int>(k - j);
if (ret == 0) {
previewNumbers.insert(0);
}
#ifdef DEBUG
std::cout << "\nThe set now contains: ";
for (Params::PreviewNumbers::const_iterator i = previewNumbers.begin();
i != previewNumbers.end();
++i) {
std::cout << *i << ", ";
}
std::cout << std::endl;
#endif
return (int) (k - j);
} // parsePreviewNumbers
bool parseCmdFiles(ModifyCmds& modifyCmds,
const Params::CmdFiles& cmdFiles)
{
Params::CmdFiles::const_iterator end = cmdFiles.end();
Params::CmdFiles::const_iterator filename = cmdFiles.begin();
for ( ; filename != end; ++filename) {
try {
std::ifstream file(filename->c_str());
bool bStdin = filename->compare("-")== 0;
if (!file && !bStdin) {
std::cerr << *filename << ": "
<< _("Failed to open command file for reading\n");
return false;
}
int num = 0;
std::string line;
while (bStdin?std::getline(std::cin, line):std::getline(file, line)) {
ModifyCmd modifyCmd;
if (parseLine(modifyCmd, line, ++num)) {
modifyCmds.push_back(modifyCmd);
}
}
}
catch (const Exiv2::AnyError& error) {
std::cerr << *filename << ", " << _("line") << " " << error << "\n";
return false;
}
}
return true;
} // parseCmdFile
bool parseCmdLines(ModifyCmds& modifyCmds,
const Params::CmdLines& cmdLines)
{
try {
int num = 0;
Params::CmdLines::const_iterator end = cmdLines.end();
Params::CmdLines::const_iterator line = cmdLines.begin();
for ( ; line != end; ++line) {
ModifyCmd modifyCmd;
if (parseLine(modifyCmd, *line, ++num)) {
modifyCmds.push_back(modifyCmd);
}
}
return true;
}
catch (const Exiv2::AnyError& error) {
std::cerr << _("-M option") << " " << error << "\n";
return false;
}
} // parseCmdLines
#if defined(_MSC_VER) || defined(__MINGW__)
static std::string formatArg(const char* arg)
{
std::string result = "";
char b = ' ' ;
char e = '\\'; std::string E = std::string("\\");
char q = '\''; std::string Q = std::string("'" );
bool qt = false;
char* a = (char*) arg;
while ( *a ) {
if ( *a == b || *a == e || *a == q ) qt = true;
if ( *a == q ) result += E;
if ( *a == e ) result += E;
result += std::string(a,1);
a++ ;
}
if (qt) result = Q + result + Q;
return result;
}
#endif
bool parseLine(ModifyCmd& modifyCmd, const std::string& line, int num)
{
const std::string delim = " \t";
// Skip empty lines and comments
std::string::size_type cmdStart = line.find_first_not_of(delim);
if (cmdStart == std::string::npos || line[cmdStart] == '#') return false;
// Get command and key
std::string::size_type cmdEnd = line.find_first_of(delim, cmdStart+1);
std::string::size_type keyStart = line.find_first_not_of(delim, cmdEnd+1);
std::string::size_type keyEnd = line.find_first_of(delim, keyStart+1);
if ( cmdStart == std::string::npos
|| cmdEnd == std::string::npos
|| keyStart == std::string::npos) {
std::string cmdLine ;
#if defined(_MSC_VER) || defined(__MINGW__)
for ( int i = 1 ; i < __argc ; i++ ) { cmdLine += std::string(" ") + formatArg(__argv[i]) ; }
#endif
throw Exiv2::Error(Exiv2::kerErrorMessage, Exiv2::toString(num)
+ ": " + _("Invalid command line:") + cmdLine);
}
std::string cmd(line.substr(cmdStart, cmdEnd-cmdStart));
CmdId cmdId = commandId(cmd);
if (cmdId == invalidCmdId) {
throw Exiv2::Error(Exiv2::kerErrorMessage, Exiv2::toString(num)
+ ": " + _("Invalid command") + " `" + cmd + "'");
}
Exiv2::TypeId defaultType = Exiv2::invalidTypeId;
std::string key(line.substr(keyStart, keyEnd-keyStart));
MetadataId metadataId = invalidMetadataId;
if (cmdId != reg) {
try {
Exiv2::IptcKey iptcKey(key);
metadataId = iptc;
defaultType = Exiv2::IptcDataSets::dataSetType(iptcKey.tag(),
iptcKey.record());
}
catch (const Exiv2::AnyError&) {}
if (metadataId == invalidMetadataId) {
try {
Exiv2::ExifKey exifKey(key);
metadataId = exif;
defaultType = exifKey.defaultTypeId();
}
catch (const Exiv2::AnyError&) {}
}
if (metadataId == invalidMetadataId) {
try {
Exiv2::XmpKey xmpKey(key);
metadataId = xmp;
defaultType = Exiv2::XmpProperties::propertyType(xmpKey);
}
catch (const Exiv2::AnyError&) {}
}
if (metadataId == invalidMetadataId) {
throw Exiv2::Error(Exiv2::kerErrorMessage, Exiv2::toString(num)
+ ": " + _("Invalid key") + " `" + key + "'");
}
}
std::string value;
Exiv2::TypeId type = defaultType;
bool explicitType = false;
if (cmdId != del) {
// Get type and value
std::string::size_type typeStart = std::string::npos;
if (keyEnd != std::string::npos) typeStart = line.find_first_not_of(delim, keyEnd+1);
std::string::size_type typeEnd = std::string::npos;
if (typeStart != std::string::npos) typeEnd = line.find_first_of(delim, typeStart+1);
std::string::size_type valStart = typeStart;
std::string::size_type valEnd = std::string::npos;
if (valStart != std::string::npos) valEnd = line.find_last_not_of(delim);
if ( cmdId == reg
&& ( keyEnd == std::string::npos
|| valStart == std::string::npos)) {
throw Exiv2::Error(Exiv2::kerErrorMessage, Exiv2::toString(num)
+ ": " + _("Invalid command line") + " " );
}
if ( cmdId != reg
&& typeStart != std::string::npos
&& typeEnd != std::string::npos) {
std::string typeStr(line.substr(typeStart, typeEnd-typeStart));
Exiv2::TypeId tmpType = Exiv2::TypeInfo::typeId(typeStr);
if (tmpType != Exiv2::invalidTypeId) {
valStart = line.find_first_not_of(delim, typeEnd+1);
if (valStart == std::string::npos) {
throw Exiv2::Error(Exiv2::kerErrorMessage, Exiv2::toString(num)
+ ": " + _("Invalid command line") + " " );
}
type = tmpType;
explicitType = true;
}
}
if (valStart != std::string::npos) {
value = parseEscapes(line.substr(valStart, valEnd+1-valStart));
std::string::size_type last = value.length()-1;
if ( (value[0] == '"' && value[last] == '"')
|| (value[0] == '\'' && value[last] == '\'')) {
value = value.substr(1, value.length()-2);
}
}
}
modifyCmd.cmdId_ = cmdId;
modifyCmd.key_ = key;
modifyCmd.metadataId_ = metadataId;
modifyCmd.typeId_ = type;
modifyCmd.explicitType_ = explicitType;
modifyCmd.value_ = value;
if (cmdId == reg) {
// Registration needs to be done immediately as the new namespaces are
// looked up during parsing of subsequent lines (to validate XMP keys).
Exiv2::XmpProperties::registerNs(modifyCmd.value_, modifyCmd.key_);
}
return true;
} // parseLine
CmdId commandId(const std::string& cmdString)
{
int i = 0;
for (; cmdIdAndString[i].cmdId_ != invalidCmdId
&& cmdIdAndString[i].cmdString_ != cmdString; ++i) {}
return cmdIdAndString[i].cmdId_;
}
std::string parseEscapes(const std::string& input)
{
std::string result = "";
for (unsigned int i = 0; i < input.length(); ++i) {
char ch = input[i];
if (ch != '\\') {
result.push_back(ch);
continue;
}
int escapeStart = i;
if (!(input.length() - 1 > i)) {
result.push_back(ch);
continue;
}
++i;
ch = input[i];
switch (ch) {
case '\\': // Escaping of backslash
result.push_back('\\');
break;
case 'r': // Escaping of carriage return
result.push_back('\r');
break;
case 'n': // Escaping of newline
result.push_back('\n');
break;
case 't': // Escaping of tab
result.push_back('\t');
break;
case 'u': // Escaping of unicode
if (input.length() - 4 > i) {
int acc = 0;
for (int j = 0; j < 4; ++j) {
++i;
acc <<= 4;
if (input[i] >= '0' && input[i] <= '9') {
acc |= input[i] - '0';
}
else if (input[i] >= 'a' && input[i] <= 'f') {
acc |= input[i] - 'a' + 10;
}
else if (input[i] >= 'A' && input[i] <= 'F') {
acc |= input[i] - 'A' + 10;
}
else {
acc = -1;
break;
}
}
if (acc == -1) {
result.push_back('\\');
i = escapeStart;
break;
}
std::string ucs2toUtf8 = "";
ucs2toUtf8.push_back((char) ((acc & 0xff00) >> 8));
ucs2toUtf8.push_back((char) (acc & 0x00ff));
if (Exiv2::convertStringCharset (ucs2toUtf8, "UCS-2BE", "UTF-8")) {
result.append (ucs2toUtf8);
}
}
else {
result.push_back('\\');
result.push_back(ch);
}
break;
default:
result.push_back('\\');
result.push_back(ch);
}
}
return result;
}
}