blob: bb8748ec097badd926a84de3683ec828b4b4a643 [file] [log] [blame]
Fletcher Woodruffdd635822020-04-23 12:49:39 -06001// Copyright 2020 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "xml_util.h"
6
7#include <memory>
8#include <utility>
9#include <string>
10
Fletcher Woodruffdd635822020-04-23 12:49:39 -060011#include <libxml/tree.h>
12
Fletcher Woodruffa6d238f2020-05-18 16:25:43 -060013#include <base/containers/flat_map.h>
14#include <base/strings/string_number_conversions.h>
15#include <base/strings/string_util.h>
16
Fletcher Woodruffdd635822020-04-23 12:49:39 -060017namespace {
18
19struct XmlNodeDeleter {
20 void operator()(xmlNode* p) { xmlFreeNode(p); }
21};
22using UniqueXmlNodePtr = std::unique_ptr<xmlNode, XmlNodeDeleter>;
23
24struct XmlDocDeleter {
25 void operator()(xmlDoc* p) { xmlFreeDoc(p); }
26};
27using UniqueXmlDocPtr = std::unique_ptr<xmlDoc, XmlDocDeleter>;
28
29constexpr char kPwgNs[] = "pwg";
30constexpr char kScanNs[] = "scan";
31using namespace_map = base::flat_map<std::string, xmlNs*>;
32
33inline const xmlChar* XmlCast(const char* p) {
34 return reinterpret_cast<const xmlChar*>(p);
35}
36
Fletcher Woodruffa6d238f2020-05-18 16:25:43 -060037bool StrEqual(const xmlChar* first, const char* second) {
38 return xmlStrEqual(first, XmlCast(second));
39}
40
41base::Optional<std::string> GetContent(const xmlNode* node) {
42 if (!node || !node->children) {
43 LOG(ERROR) << "node does not have content";
44 return base::nullopt;
45 }
46 return reinterpret_cast<const char*>(node->children->content);
47}
48
49// Attempt to parse the contents of |node| as an integer.
50base::Optional<int> GetIntContent(const xmlNode* node) {
51 base::Optional<std::string> contents = GetContent(node);
52 if (!contents)
53 return base::nullopt;
54 int value;
55 if (!base::StringToInt(contents.value(), &value)) {
56 LOG(ERROR) << "Failed to convert " << contents.value() << " to int";
57 return base::nullopt;
58 }
59 return value;
60}
61
Fletcher Woodruffdd635822020-04-23 12:49:39 -060062// Serialize the xml tree in |root| into a vector. Consumes |root| and frees
63// its memory.
64std::vector<uint8_t> Serialize(UniqueXmlNodePtr root) {
65 UniqueXmlDocPtr doc(xmlNewDoc(XmlCast("1.0")));
66 // |doc| takes ownership of |root| here.
67 xmlDocSetRootElement(doc.get(), root.release());
68
69 xmlChar* buf;
70 int length;
71 xmlDocDumpFormatMemoryEnc(doc.get(), &buf, &length, "UTF-8", true);
72 std::vector<uint8_t> xml(buf, buf + length);
73 xmlFree(buf);
74
75 return xml;
76}
77
78// Adds a new node with namespace |ns| called |name| as a child of |node|.
79// Returns a non-owning pointer to the new node.
80xmlNode* AddNode(xmlNode* node, xmlNs* ns, const std::string& name) {
81 xmlNode* child = xmlNewNode(ns, XmlCast(name.c_str()));
82 xmlAddChild(node, child);
83 return child;
84}
85
86// Adds a new node with namespace |ns| called |name| and containing |content|
87// as a child of |node|.
88void AddNodeWithContent(xmlNode* node,
89 xmlNs* ns,
90 const std::string& name,
91 const std::string& content) {
92 xmlNode* child = xmlNewNode(ns, XmlCast(name.c_str()));
93 xmlNodeSetContent(child, XmlCast(content.c_str()));
94 xmlAddChild(node, child);
95}
96
97UniqueXmlNodePtr SourceCapabilitiesAsXml(const SourceCapabilities& caps,
98 const std::string& name,
99 namespace_map* namespaces) {
100 xmlNs* pwg = (*namespaces)[kPwgNs];
101 xmlNs* scan = (*namespaces)[kScanNs];
102 UniqueXmlNodePtr root(xmlNewNode(scan, XmlCast(name.c_str())));
103
104 AddNodeWithContent(root.get(), scan, "MinWidth", "16");
105 AddNodeWithContent(root.get(), scan, "MaxWidth", "2550");
106 AddNodeWithContent(root.get(), scan, "MinHeight", "16");
107 AddNodeWithContent(root.get(), scan, "MaxHeight", "3507");
108 AddNodeWithContent(root.get(), scan, "MaxScanRegions", "1");
109
110 xmlNode* profiles = AddNode(root.get(), scan, "SettingProfiles");
111 xmlNode* profile = AddNode(profiles, scan, "SettingProfile");
112 xmlNode* color_modes = AddNode(profile, scan, "ColorModes");
113 for (const std::string& mode : caps.color_modes) {
114 AddNodeWithContent(color_modes, scan, "ColorMode", mode);
115 }
116 xmlNode* document_formats = AddNode(profile, scan, "DocumentFormats");
117 for (const std::string& format : caps.formats) {
118 AddNodeWithContent(document_formats, pwg, "DocumentFormat", format);
119 }
120
121 xmlNode* resolutions = AddNode(profile, scan, "SupportedResolutions");
122 xmlNode* discrete_resolutions =
123 AddNode(resolutions, scan, "DiscreteResolutions");
124 for (int resolution : caps.resolutions) {
125 xmlNode* r = AddNode(discrete_resolutions, scan, "DiscreteResolution");
126 AddNodeWithContent(r, scan, "XResolution", std::to_string(resolution));
127 AddNodeWithContent(r, scan, "YResolution", std::to_string(resolution));
128 }
129
130 xmlNode* intents = AddNode(root.get(), scan, "SupportedIntents");
131 AddNodeWithContent(intents, scan, "Intent", "Document");
132 AddNodeWithContent(intents, scan, "Intent", "TextAndGraphic");
133 AddNodeWithContent(intents, scan, "Intent", "Photo");
134 AddNodeWithContent(intents, scan, "Intent", "Preview");
135
136 AddNodeWithContent(root.get(), scan, "MaxOpticalXResolution", "2400");
137 AddNodeWithContent(root.get(), scan, "MaxOpticalYResolution", "2400");
138 AddNodeWithContent(root.get(), scan, "RiskyLeftMargin", "0");
139 AddNodeWithContent(root.get(), scan, "RiskyRightMargin", "0");
140 AddNodeWithContent(root.get(), scan, "RiskyTopMargin", "0");
141 AddNodeWithContent(root.get(), scan, "RiskyBottomMargin", "0");
142
143 return root;
144}
145
Fletcher Woodruff0afc5162020-05-18 16:11:59 -0600146// Serialize a map of UUIDs to JobInfo into the XML format expected for
147// eSCL ScannerStatus.
148// Returns a node which can be merged into the larger XML document.
149UniqueXmlNodePtr JobListAsXml(const base::flat_map<std::string, JobInfo>& jobs,
150 namespace_map* namespaces) {
151 xmlNs* pwg = (*namespaces)[kPwgNs];
152 xmlNs* scan = (*namespaces)[kScanNs];
153
154 UniqueXmlNodePtr root(xmlNewNode(scan, XmlCast("Jobs")));
155 for (const auto& job : jobs) {
156 const std::string& uuid = job.first;
157 const JobInfo& info = job.second;
158 xmlNode* job_info = AddNode(root.get(), scan, "JobInfo");
159 AddNodeWithContent(job_info, pwg, "JobUri", "/eSCL/ScanJobs/" + uuid);
160 AddNodeWithContent(job_info, pwg, "JobUuid", "urn:uuid:" + uuid);
161
162 // Different scanners are not consistent with how they report scan job age.
163 // Arbitrarily report age as elapsed seconds.
164 base::TimeDelta elapsed = base::TimeTicks::Now() - info.created;
165 AddNodeWithContent(job_info, scan, "Age",
166 std::to_string(elapsed.InSeconds()));
167
168 int images_completed = 0;
169 int images_to_transfer = 0;
170 std::string job_state;
171 // These reason strings are defined in the PWG standard. There are other
172 // values possible, but for now, just use a typical value.
173 std::string reason;
174 switch (info.state) {
175 case kPending:
176 images_completed = 1;
177 images_to_transfer = 1;
178 job_state = "Pending";
179 reason = "JobScanning";
180 break;
181 case kCanceled:
182 images_completed = 0;
183 images_to_transfer = 0;
184 job_state = "Canceled";
185 reason = "JobTimedOut";
186 break;
187 case kCompleted:
188 images_completed = 1;
189 images_to_transfer = 0;
190 job_state = "Completed";
191 reason = "JobCompletedSuccessfully";
192 break;
193 }
194
195 AddNodeWithContent(job_info, pwg, "ImagesCompleted",
196 std::to_string(images_completed));
197 AddNodeWithContent(job_info, pwg, "ImagesToTransfer",
198 std::to_string(images_to_transfer));
199 AddNodeWithContent(job_info, pwg, "JobState", job_state);
200 xmlNode* job_state_reasons = AddNode(job_info, pwg, "JobStateReasons");
201 AddNodeWithContent(job_state_reasons, pwg, "JobStateReason", reason);
202 }
203
204 return root;
205}
206
Fletcher Woodruffa6d238f2020-05-18 16:25:43 -0600207base::Optional<ScanRegion> ScanRegionFromNode(const xmlNode* node) {
208 if (!node) {
209 LOG(ERROR) << "ScanRegion node cannot be null";
210 return base::nullopt;
211 }
212
213 ScanRegion region;
214 // node->children is a null-terminated doubly-linked list of nodes. We
215 // iterate over the '->next' pointers, stopping once we get a null value.
216 for (xmlNode* child = node->children; child; child = child->next) {
217 if (StrEqual(child->name, "ContentRegionUnits")) {
218 base::Optional<std::string> content = GetContent(child);
219 if (!content) {
220 LOG(ERROR) << "ContentRegionUnits node is empty";
221 return base::nullopt;
222 }
223 region.units = content.value();
224 } else if (StrEqual(child->name, "Height")) {
225 base::Optional<int> content = GetIntContent(child);
226 if (!content) {
227 LOG(ERROR) << "Height node does not have valid contents";
228 return base::nullopt;
229 }
230 region.height = content.value();
231 } else if (StrEqual(child->name, "Width")) {
232 base::Optional<int> content = GetIntContent(child);
233 if (!content) {
234 LOG(ERROR) << "Width node does not have valid contents";
235 return base::nullopt;
236 }
237 region.width = content.value();
238 } else if (StrEqual(child->name, "XOffset")) {
239 base::Optional<int> content = GetIntContent(child);
240 if (!content) {
241 LOG(ERROR) << "XOffset node does not have valid contents";
242 return base::nullopt;
243 }
244 region.x_offset = content.value();
245 } else if (StrEqual(child->name, "YOffset")) {
246 base::Optional<int> content = GetIntContent(child);
247 if (!content) {
248 LOG(ERROR) << "YOffset node does not have valid contents";
249 return base::nullopt;
250 }
251 region.y_offset = content.value();
252 }
253 }
254
255 return region;
256}
257
258base::Optional<ColorMode> ColorModeFromString(const std::string& color_mode) {
259 if (color_mode == "RGB24") {
260 return kRGB;
261 } else if (color_mode == "Grayscale8") {
262 return kGrayscale;
263 } else if (color_mode == "BlackAndWhite1") {
264 return kBlackAndWhite;
265 } else {
266 return base::nullopt;
267 }
268}
269
Fletcher Woodruffdd635822020-04-23 12:49:39 -0600270} // namespace
271
272std::vector<uint8_t> ScannerCapabilitiesAsXml(const ScannerCapabilities& caps) {
273 UniqueXmlNodePtr root(xmlNewNode(nullptr, XmlCast("ScannerCapabilities")));
274
275 xmlNs* pwg =
276 xmlNewNs(root.get(), XmlCast("http://www.pwg.org/schemas/2010/12/sm"),
277 XmlCast("pwg"));
278 xmlNs* scan = xmlNewNs(
279 root.get(), XmlCast("http://schemas.hp.com/imaging/escl/2011/05/03"),
280 XmlCast("scan"));
281 xmlNewNs(root.get(), XmlCast("http://www.w3.org/2001/XMLSchema-instance"),
282 XmlCast("xsi"));
283 xmlSetNs(root.get(), scan);
284
285 namespace_map namespaces;
286 namespaces[kPwgNs] = pwg;
287 namespaces[kScanNs] = scan;
288
289 AddNodeWithContent(root.get(), pwg, "Version", "2.63");
290 AddNodeWithContent(root.get(), pwg, "MakeAndModel", caps.make_and_model);
291 AddNodeWithContent(root.get(), pwg, "SerialNumber", caps.serial_number);
292
293 xmlNode* platen = AddNode(root.get(), scan, "Platen");
294 UniqueXmlNodePtr platen_caps = SourceCapabilitiesAsXml(
295 caps.platen_capabilities, "PlatenInputCaps", &namespaces);
296 xmlAddChild(platen, platen_caps.release());
297
Paul Moyf07f53b2021-02-03 10:30:00 -0700298 if (caps.adf_capabilities.has_value()) {
299 xmlNode* adf = AddNode(root.get(), scan, "Adf");
300 UniqueXmlNodePtr adf_simplex_caps = SourceCapabilitiesAsXml(
301 caps.adf_capabilities.value(), "AdfSimplexInputCaps", &namespaces);
302 xmlAddChild(adf, adf_simplex_caps.release());
303 UniqueXmlNodePtr adf_duplex_caps = SourceCapabilitiesAsXml(
304 caps.adf_capabilities.value(), "AdfDuplexInputCaps", &namespaces);
305 xmlAddChild(adf, adf_duplex_caps.release());
306 }
307
Fletcher Woodruffdd635822020-04-23 12:49:39 -0600308 return Serialize(std::move(root));
309}
Fletcher Woodruff70ccf552020-04-17 14:08:06 -0600310
311std::vector<uint8_t> ScannerStatusAsXml(const ScannerStatus& status) {
312 UniqueXmlNodePtr root(xmlNewNode(nullptr, XmlCast("ScannerStatus")));
313
314 xmlNs* scan = xmlNewNs(
315 root.get(), XmlCast("http://schemas.hp.com/imaging/escl/2011/05/03"),
316 XmlCast("scan"));
317 xmlNs* pwg =
318 xmlNewNs(root.get(), XmlCast("http://www.pwg.org/schemas/2010/12/sm"),
319 XmlCast("pwg"));
320 xmlNewNs(root.get(), XmlCast("http://www.w3.org/2001/XMLSchema-instance"),
321 XmlCast("xsi"));
322 xmlSetNs(root.get(), scan);
323
324 AddNodeWithContent(root.get(), pwg, "Version", "2.6.3");
325 AddNodeWithContent(root.get(), pwg, "State", status.idle ? "Idle" : "Busy");
Paul Moyaa3120f2021-02-10 16:20:44 -0700326 AddNodeWithContent(root.get(), scan, "AdfState",
327 status.adf_empty ? "ScannerAdfEmpty" : "ScannerAdfLoaded");
Fletcher Woodruff70ccf552020-04-17 14:08:06 -0600328
Fletcher Woodruff0afc5162020-05-18 16:11:59 -0600329 // Add a list of all of the scan jobs
330 namespace_map namespaces;
331 namespaces[kPwgNs] = pwg;
332 namespaces[kScanNs] = scan;
333 UniqueXmlNodePtr jobs = JobListAsXml(status.jobs, &namespaces);
334 xmlAddChild(root.get(), jobs.release());
335
Fletcher Woodruff70ccf552020-04-17 14:08:06 -0600336 return Serialize(std::move(root));
337}
Fletcher Woodruffa6d238f2020-05-18 16:25:43 -0600338
339base::Optional<ScanSettings> ScanSettingsFromXml(
340 const std::vector<uint8_t>& xml) {
341 // Since this document doesn't have a url, we use a filler value.
342 const char* url = "noname.xml";
343 // We don't know the encoding, so pass NULL to autodetect.
344 const char* encoding = NULL;
345 const UniqueXmlDocPtr doc(
346 xmlReadMemory(reinterpret_cast<const char*>(xml.data()), xml.size(), url,
347 encoding, /*options=*/0));
348 if (!doc) {
349 LOG(ERROR) << "Could not parse data as XML document";
350 return base::nullopt;
351 }
352
353 const xmlNode* root = xmlDocGetRootElement(doc.get());
354 if (!root) {
355 LOG(ERROR) << "XML document does not have root node";
356 return base::nullopt;
357 }
358
359 ScanSettings settings;
360 // Iterate over doubly-linked list of nodes in the document.
361 for (const xmlNode* node = root->children; node; node = node->next) {
362 if (StrEqual(node->name, "ScanRegions")) {
363 // Iterate over doubly-linked list of children of the ScanRegions node.
364 for (xmlNode* child = node->children; child; child = child->next) {
365 if (StrEqual(child->name, "ScanRegion")) {
366 base::Optional<ScanRegion> region = ScanRegionFromNode(child);
367 if (!region) {
368 LOG(ERROR) << "Failed to parse ScanRegion";
369 return base::nullopt;
370 }
371 settings.regions.push_back(region.value());
372 }
373 }
374 } else if (StrEqual(node->name, "DocumentFormat")) {
375 base::Optional<std::string> format = GetContent(node);
376 if (!format) {
377 LOG(ERROR) << "DocumentFormat node does not have valid contents.";
378 return base::nullopt;
379 }
380 settings.document_format = format.value();
381 } else if (StrEqual(node->name, "ColorMode")) {
382 base::Optional<std::string> color_mode_string = GetContent(node);
383 if (!color_mode_string) {
384 LOG(ERROR) << "ColorMode node does not have valid contents.";
385 return base::nullopt;
386 }
387
388 base::Optional<ColorMode> color_mode =
389 ColorModeFromString(color_mode_string.value());
390 if (!color_mode) {
391 LOG(ERROR) << "Invalid ColorMode value: " << color_mode_string.value();
392 return base::nullopt;
393 }
394 settings.color_mode = color_mode.value();
395 } else if (StrEqual(node->name, "InputSource")) {
396 base::Optional<std::string> source = GetContent(node);
397 if (!source) {
398 LOG(ERROR) << "InputSource node does not have valid contents.";
399 return base::nullopt;
400 }
401 settings.input_source = source.value();
402 } else if (StrEqual(node->name, "XResolution")) {
403 base::Optional<int> resolution = GetIntContent(node);
404 if (!resolution) {
405 LOG(ERROR) << "XResolution node does not have valid contents.";
406 return base::nullopt;
407 }
408 settings.x_resolution = resolution.value();
409 } else if (StrEqual(node->name, "YResolution")) {
410 base::Optional<int> resolution = GetIntContent(node);
411 if (!resolution) {
412 LOG(ERROR) << "YResolution node does not have valid contents.";
413 return base::nullopt;
414 }
415 settings.y_resolution = resolution.value();
416 }
417 }
418
419 return settings;
420}