Coverage Report

Created: 2025-11-09 06:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/exiv2/src/xmpsidecar.cpp
Line
Count
Source
1
// SPDX-License-Identifier: GPL-2.0-or-later
2
#include "xmpsidecar.hpp"
3
4
#include "basicio.hpp"
5
#include "config.h"
6
#include "convert.hpp"
7
#include "error.hpp"
8
#include "futils.hpp"
9
#include "image.hpp"
10
#include "properties.hpp"
11
#include "utils.hpp"
12
#include "xmp_exiv2.hpp"
13
14
#ifdef EXIV2_DEBUG_MESSAGES
15
#include <iostream>
16
#endif
17
18
namespace {
19
constexpr char xmlHeader[] = "<?xpacket begin=\"\xef\xbb\xbf\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n";
20
constexpr auto xmlHdrCnt = std::size(xmlHeader) - 1;  // without the trailing 0-character
21
constexpr auto xmlFooter = "<?xpacket end=\"w\"?>";
22
}  // namespace
23
24
// class member definitions
25
namespace Exiv2 {
26
378
XmpSidecar::XmpSidecar(BasicIo::UniquePtr io, bool create) : Image(ImageType::xmp, mdXmp, std::move(io)) {
27
378
  if (create && io_->open() == 0) {
28
0
    IoCloser closer(*io_);
29
0
    io_->write(reinterpret_cast<const byte*>(xmlHeader), xmlHdrCnt);
30
0
  }
31
378
}  // XmpSidecar::XmpSidecar
32
33
0
std::string XmpSidecar::mimeType() const {
34
0
  return "application/rdf+xml";
35
0
}
36
37
0
void XmpSidecar::setComment(const std::string&) {
38
  // not supported
39
0
  throw(Error(ErrorCode::kerInvalidSettingForImage, "Image comment", "XMP"));
40
0
}
41
42
378
void XmpSidecar::readMetadata() {
43
#ifdef EXIV2_DEBUG_MESSAGES
44
  std::cerr << "Reading XMP file " << io_->path() << "\n";
45
#endif
46
378
  if (io_->open() != 0) {
47
0
    throw Error(ErrorCode::kerDataSourceOpenFailed, io_->path(), strError());
48
0
  }
49
378
  IoCloser closer(*io_);
50
  // Ensure that this is the correct image type
51
378
  if (!isXmpType(*io_, false)) {
52
0
    if (io_->error() || io_->eof())
53
0
      throw Error(ErrorCode::kerFailedToReadImageData);
54
0
    throw Error(ErrorCode::kerNotAnImage, "XMP");
55
0
  }
56
  // Read the XMP packet from the IO stream
57
378
  std::string xmpPacket;
58
378
  const long len = 64 * 1024;
59
378
  byte buf[len];
60
834
  while (auto l = io_->read(buf, len)) {
61
456
    xmpPacket.append(reinterpret_cast<char*>(buf), l);
62
456
  }
63
378
  if (io_->error())
64
0
    throw Error(ErrorCode::kerFailedToReadImageData);
65
378
  clearMetadata();
66
378
  xmpPacket_ = std::move(xmpPacket);
67
378
  if (!xmpPacket_.empty() && XmpParser::decode(xmpData_, xmpPacket_)) {
68
56
#ifndef SUPPRESS_WARNINGS
69
56
    EXV_WARNING << "Failed to decode XMP metadata.\n";
70
56
#endif
71
56
  }
72
73
  // #1112 - store dates to deal with loss of TZ information during conversions
74
378
  for (const auto& xmp : xmpData_) {
75
278
    std::string key(xmp.key());
76
278
    if (Internal::contains(key, "Date")) {
77
128
      dates_[key] = xmp.value().toString();
78
128
    }
79
278
  }
80
81
378
  copyXmpToIptc(xmpData_, iptcData_);
82
378
  copyXmpToExif(xmpData_, exifData_);
83
378
}  // XmpSidecar::readMetadata
84
85
0
static bool matchi(const std::string& key, const char* substr) {
86
0
  return Internal::contains(Internal::lower(key), substr);
87
0
}
88
89
0
void XmpSidecar::writeMetadata() {
90
0
  if (io_->open() != 0) {
91
0
    throw Error(ErrorCode::kerDataSourceOpenFailed, io_->path(), strError());
92
0
  }
93
0
  IoCloser closer(*io_);
94
95
0
  if (!writeXmpFromPacket()) {
96
    // #589 copy XMP tags
97
0
    Exiv2::XmpData copy;
98
0
    for (const auto& xmp : xmpData_) {
99
0
      if (!matchi(xmp.key(), "exif") && !matchi(xmp.key(), "iptc")) {
100
0
        copy[xmp.key()] = xmp.value();
101
0
      }
102
0
    }
103
104
    // run the converters
105
0
    copyExifToXmp(exifData_, xmpData_);
106
0
    copyIptcToXmp(iptcData_, xmpData_);
107
108
    // #1112 - restore dates if they lost their TZ info
109
0
    for (const auto& [sKey, value_orig] : dates_) {
110
0
      Exiv2::XmpKey key(sKey);
111
0
      if (xmpData_.findKey(key) != xmpData_.end()) {
112
0
        std::string value_now(xmpData_[sKey].value().toString());
113
        // std::cout << key << " -> " << value_now << " => " << value_orig << '\n';
114
0
        if (Internal::contains(value_orig, value_now.substr(0, 10))) {
115
0
          xmpData_[sKey] = value_orig;
116
0
        }
117
0
      }
118
0
    }
119
120
    // #589 - restore tags which were modified by the converters
121
0
    for (const auto& xmp : copy) {
122
0
      xmpData_[xmp.key()] = xmp.value();
123
0
    }
124
125
0
    if (XmpParser::encode(xmpPacket_, xmpData_, XmpParser::omitPacketWrapper | XmpParser::useCompactFormat) > 1) {
126
0
#ifndef SUPPRESS_WARNINGS
127
0
      EXV_ERROR << "Failed to encode XMP metadata.\n";
128
0
#endif
129
0
    }
130
0
  }
131
0
  if (!xmpPacket_.empty()) {
132
0
    if (!xmpPacket_.starts_with("<?xml")) {
133
0
      xmpPacket_ = xmlHeader + xmpPacket_ + xmlFooter;
134
0
    }
135
0
    MemIo tempIo;
136
137
    // Write XMP packet
138
0
    if (tempIo.write(reinterpret_cast<const byte*>(xmpPacket_.data()), xmpPacket_.size()) != xmpPacket_.size())
139
0
      throw Error(ErrorCode::kerImageWriteFailed);
140
0
    if (tempIo.error())
141
0
      throw Error(ErrorCode::kerImageWriteFailed);
142
0
    io_->close();
143
0
    io_->transfer(tempIo);  // may throw
144
0
  }
145
0
}  // XmpSidecar::writeMetadata
146
147
// *************************************************************************
148
// free functions
149
378
Image::UniquePtr newXmpInstance(BasicIo::UniquePtr io, bool create) {
150
378
  auto image = std::make_unique<XmpSidecar>(std::move(io), create);
151
378
  if (!image->good()) {
152
0
    return nullptr;
153
0
  }
154
378
  return image;
155
378
}
156
157
7.93k
bool isXmpType(BasicIo& iIo, bool advance) {
158
  /*
159
    Check if the file starts with an optional XML declaration followed by
160
    either an XMP header (<?xpacket ... ?>) or an <x:xmpmeta> element.
161
162
    In addition, in order for empty XmpSidecar objects as created by
163
    Exiv2 to pass the test, just an XML header is also considered ok.
164
   */
165
7.93k
  const int32_t len = 80;
166
7.93k
  byte buf[len];
167
7.93k
  iIo.read(buf, xmlHdrCnt + 1);
168
7.93k
  if (iIo.eof() && 0 == strncmp(reinterpret_cast<const char*>(buf), xmlHeader, xmlHdrCnt)) {
169
3
    return true;
170
3
  }
171
7.92k
  if (iIo.error() || iIo.eof()) {
172
123
    return false;
173
123
  }
174
7.80k
  iIo.read(buf + xmlHdrCnt + 1, len - xmlHdrCnt - 1);
175
7.80k
  if (iIo.error() || iIo.eof()) {
176
10
    return false;
177
10
  }
178
  // Skip leading BOM
179
7.79k
  int32_t start = 0;
180
7.79k
  if (0 == strncmp(reinterpret_cast<const char*>(buf), "\xef\xbb\xbf", 3)) {
181
118
    start = 3;
182
118
  }
183
7.79k
  bool rc = false;
184
7.79k
  std::string head(reinterpret_cast<const char*>(buf + start), len - start);
185
7.79k
  if (head.starts_with("<?xml")) {
186
    // Forward to the next tag
187
23
    auto it = std::find(head.begin() + 5, head.end(), '<');
188
23
    if (it != head.end())
189
15
      head = head.substr(std::distance(head.begin(), it));
190
23
  }
191
7.79k
  if (head.starts_with("<?xpacket") || head.starts_with("<x:xmpmeta")) {
192
1.13k
    rc = true;
193
1.13k
  }
194
7.79k
  if (!advance || !rc) {
195
7.79k
    iIo.seek(-(len - start), BasicIo::cur);  // Swallow the BOM
196
7.79k
  }
197
7.79k
  return rc;
198
7.80k
}
199
200
}  // namespace Exiv2