blob: bb8748ec097badd926a84de3683ec828b4b4a643 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "xml_util.h"
#include <memory>
#include <utility>
#include <string>
#include <libxml/tree.h>
#include <base/containers/flat_map.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
namespace {
struct XmlNodeDeleter {
void operator()(xmlNode* p) { xmlFreeNode(p); }
};
using UniqueXmlNodePtr = std::unique_ptr<xmlNode, XmlNodeDeleter>;
struct XmlDocDeleter {
void operator()(xmlDoc* p) { xmlFreeDoc(p); }
};
using UniqueXmlDocPtr = std::unique_ptr<xmlDoc, XmlDocDeleter>;
constexpr char kPwgNs[] = "pwg";
constexpr char kScanNs[] = "scan";
using namespace_map = base::flat_map<std::string, xmlNs*>;
inline const xmlChar* XmlCast(const char* p) {
return reinterpret_cast<const xmlChar*>(p);
}
bool StrEqual(const xmlChar* first, const char* second) {
return xmlStrEqual(first, XmlCast(second));
}
base::Optional<std::string> GetContent(const xmlNode* node) {
if (!node || !node->children) {
LOG(ERROR) << "node does not have content";
return base::nullopt;
}
return reinterpret_cast<const char*>(node->children->content);
}
// Attempt to parse the contents of |node| as an integer.
base::Optional<int> GetIntContent(const xmlNode* node) {
base::Optional<std::string> contents = GetContent(node);
if (!contents)
return base::nullopt;
int value;
if (!base::StringToInt(contents.value(), &value)) {
LOG(ERROR) << "Failed to convert " << contents.value() << " to int";
return base::nullopt;
}
return value;
}
// Serialize the xml tree in |root| into a vector. Consumes |root| and frees
// its memory.
std::vector<uint8_t> Serialize(UniqueXmlNodePtr root) {
UniqueXmlDocPtr doc(xmlNewDoc(XmlCast("1.0")));
// |doc| takes ownership of |root| here.
xmlDocSetRootElement(doc.get(), root.release());
xmlChar* buf;
int length;
xmlDocDumpFormatMemoryEnc(doc.get(), &buf, &length, "UTF-8", true);
std::vector<uint8_t> xml(buf, buf + length);
xmlFree(buf);
return xml;
}
// Adds a new node with namespace |ns| called |name| as a child of |node|.
// Returns a non-owning pointer to the new node.
xmlNode* AddNode(xmlNode* node, xmlNs* ns, const std::string& name) {
xmlNode* child = xmlNewNode(ns, XmlCast(name.c_str()));
xmlAddChild(node, child);
return child;
}
// Adds a new node with namespace |ns| called |name| and containing |content|
// as a child of |node|.
void AddNodeWithContent(xmlNode* node,
xmlNs* ns,
const std::string& name,
const std::string& content) {
xmlNode* child = xmlNewNode(ns, XmlCast(name.c_str()));
xmlNodeSetContent(child, XmlCast(content.c_str()));
xmlAddChild(node, child);
}
UniqueXmlNodePtr SourceCapabilitiesAsXml(const SourceCapabilities& caps,
const std::string& name,
namespace_map* namespaces) {
xmlNs* pwg = (*namespaces)[kPwgNs];
xmlNs* scan = (*namespaces)[kScanNs];
UniqueXmlNodePtr root(xmlNewNode(scan, XmlCast(name.c_str())));
AddNodeWithContent(root.get(), scan, "MinWidth", "16");
AddNodeWithContent(root.get(), scan, "MaxWidth", "2550");
AddNodeWithContent(root.get(), scan, "MinHeight", "16");
AddNodeWithContent(root.get(), scan, "MaxHeight", "3507");
AddNodeWithContent(root.get(), scan, "MaxScanRegions", "1");
xmlNode* profiles = AddNode(root.get(), scan, "SettingProfiles");
xmlNode* profile = AddNode(profiles, scan, "SettingProfile");
xmlNode* color_modes = AddNode(profile, scan, "ColorModes");
for (const std::string& mode : caps.color_modes) {
AddNodeWithContent(color_modes, scan, "ColorMode", mode);
}
xmlNode* document_formats = AddNode(profile, scan, "DocumentFormats");
for (const std::string& format : caps.formats) {
AddNodeWithContent(document_formats, pwg, "DocumentFormat", format);
}
xmlNode* resolutions = AddNode(profile, scan, "SupportedResolutions");
xmlNode* discrete_resolutions =
AddNode(resolutions, scan, "DiscreteResolutions");
for (int resolution : caps.resolutions) {
xmlNode* r = AddNode(discrete_resolutions, scan, "DiscreteResolution");
AddNodeWithContent(r, scan, "XResolution", std::to_string(resolution));
AddNodeWithContent(r, scan, "YResolution", std::to_string(resolution));
}
xmlNode* intents = AddNode(root.get(), scan, "SupportedIntents");
AddNodeWithContent(intents, scan, "Intent", "Document");
AddNodeWithContent(intents, scan, "Intent", "TextAndGraphic");
AddNodeWithContent(intents, scan, "Intent", "Photo");
AddNodeWithContent(intents, scan, "Intent", "Preview");
AddNodeWithContent(root.get(), scan, "MaxOpticalXResolution", "2400");
AddNodeWithContent(root.get(), scan, "MaxOpticalYResolution", "2400");
AddNodeWithContent(root.get(), scan, "RiskyLeftMargin", "0");
AddNodeWithContent(root.get(), scan, "RiskyRightMargin", "0");
AddNodeWithContent(root.get(), scan, "RiskyTopMargin", "0");
AddNodeWithContent(root.get(), scan, "RiskyBottomMargin", "0");
return root;
}
// Serialize a map of UUIDs to JobInfo into the XML format expected for
// eSCL ScannerStatus.
// Returns a node which can be merged into the larger XML document.
UniqueXmlNodePtr JobListAsXml(const base::flat_map<std::string, JobInfo>& jobs,
namespace_map* namespaces) {
xmlNs* pwg = (*namespaces)[kPwgNs];
xmlNs* scan = (*namespaces)[kScanNs];
UniqueXmlNodePtr root(xmlNewNode(scan, XmlCast("Jobs")));
for (const auto& job : jobs) {
const std::string& uuid = job.first;
const JobInfo& info = job.second;
xmlNode* job_info = AddNode(root.get(), scan, "JobInfo");
AddNodeWithContent(job_info, pwg, "JobUri", "/eSCL/ScanJobs/" + uuid);
AddNodeWithContent(job_info, pwg, "JobUuid", "urn:uuid:" + uuid);
// Different scanners are not consistent with how they report scan job age.
// Arbitrarily report age as elapsed seconds.
base::TimeDelta elapsed = base::TimeTicks::Now() - info.created;
AddNodeWithContent(job_info, scan, "Age",
std::to_string(elapsed.InSeconds()));
int images_completed = 0;
int images_to_transfer = 0;
std::string job_state;
// These reason strings are defined in the PWG standard. There are other
// values possible, but for now, just use a typical value.
std::string reason;
switch (info.state) {
case kPending:
images_completed = 1;
images_to_transfer = 1;
job_state = "Pending";
reason = "JobScanning";
break;
case kCanceled:
images_completed = 0;
images_to_transfer = 0;
job_state = "Canceled";
reason = "JobTimedOut";
break;
case kCompleted:
images_completed = 1;
images_to_transfer = 0;
job_state = "Completed";
reason = "JobCompletedSuccessfully";
break;
}
AddNodeWithContent(job_info, pwg, "ImagesCompleted",
std::to_string(images_completed));
AddNodeWithContent(job_info, pwg, "ImagesToTransfer",
std::to_string(images_to_transfer));
AddNodeWithContent(job_info, pwg, "JobState", job_state);
xmlNode* job_state_reasons = AddNode(job_info, pwg, "JobStateReasons");
AddNodeWithContent(job_state_reasons, pwg, "JobStateReason", reason);
}
return root;
}
base::Optional<ScanRegion> ScanRegionFromNode(const xmlNode* node) {
if (!node) {
LOG(ERROR) << "ScanRegion node cannot be null";
return base::nullopt;
}
ScanRegion region;
// node->children is a null-terminated doubly-linked list of nodes. We
// iterate over the '->next' pointers, stopping once we get a null value.
for (xmlNode* child = node->children; child; child = child->next) {
if (StrEqual(child->name, "ContentRegionUnits")) {
base::Optional<std::string> content = GetContent(child);
if (!content) {
LOG(ERROR) << "ContentRegionUnits node is empty";
return base::nullopt;
}
region.units = content.value();
} else if (StrEqual(child->name, "Height")) {
base::Optional<int> content = GetIntContent(child);
if (!content) {
LOG(ERROR) << "Height node does not have valid contents";
return base::nullopt;
}
region.height = content.value();
} else if (StrEqual(child->name, "Width")) {
base::Optional<int> content = GetIntContent(child);
if (!content) {
LOG(ERROR) << "Width node does not have valid contents";
return base::nullopt;
}
region.width = content.value();
} else if (StrEqual(child->name, "XOffset")) {
base::Optional<int> content = GetIntContent(child);
if (!content) {
LOG(ERROR) << "XOffset node does not have valid contents";
return base::nullopt;
}
region.x_offset = content.value();
} else if (StrEqual(child->name, "YOffset")) {
base::Optional<int> content = GetIntContent(child);
if (!content) {
LOG(ERROR) << "YOffset node does not have valid contents";
return base::nullopt;
}
region.y_offset = content.value();
}
}
return region;
}
base::Optional<ColorMode> ColorModeFromString(const std::string& color_mode) {
if (color_mode == "RGB24") {
return kRGB;
} else if (color_mode == "Grayscale8") {
return kGrayscale;
} else if (color_mode == "BlackAndWhite1") {
return kBlackAndWhite;
} else {
return base::nullopt;
}
}
} // namespace
std::vector<uint8_t> ScannerCapabilitiesAsXml(const ScannerCapabilities& caps) {
UniqueXmlNodePtr root(xmlNewNode(nullptr, XmlCast("ScannerCapabilities")));
xmlNs* pwg =
xmlNewNs(root.get(), XmlCast("http://www.pwg.org/schemas/2010/12/sm"),
XmlCast("pwg"));
xmlNs* scan = xmlNewNs(
root.get(), XmlCast("http://schemas.hp.com/imaging/escl/2011/05/03"),
XmlCast("scan"));
xmlNewNs(root.get(), XmlCast("http://www.w3.org/2001/XMLSchema-instance"),
XmlCast("xsi"));
xmlSetNs(root.get(), scan);
namespace_map namespaces;
namespaces[kPwgNs] = pwg;
namespaces[kScanNs] = scan;
AddNodeWithContent(root.get(), pwg, "Version", "2.63");
AddNodeWithContent(root.get(), pwg, "MakeAndModel", caps.make_and_model);
AddNodeWithContent(root.get(), pwg, "SerialNumber", caps.serial_number);
xmlNode* platen = AddNode(root.get(), scan, "Platen");
UniqueXmlNodePtr platen_caps = SourceCapabilitiesAsXml(
caps.platen_capabilities, "PlatenInputCaps", &namespaces);
xmlAddChild(platen, platen_caps.release());
if (caps.adf_capabilities.has_value()) {
xmlNode* adf = AddNode(root.get(), scan, "Adf");
UniqueXmlNodePtr adf_simplex_caps = SourceCapabilitiesAsXml(
caps.adf_capabilities.value(), "AdfSimplexInputCaps", &namespaces);
xmlAddChild(adf, adf_simplex_caps.release());
UniqueXmlNodePtr adf_duplex_caps = SourceCapabilitiesAsXml(
caps.adf_capabilities.value(), "AdfDuplexInputCaps", &namespaces);
xmlAddChild(adf, adf_duplex_caps.release());
}
return Serialize(std::move(root));
}
std::vector<uint8_t> ScannerStatusAsXml(const ScannerStatus& status) {
UniqueXmlNodePtr root(xmlNewNode(nullptr, XmlCast("ScannerStatus")));
xmlNs* scan = xmlNewNs(
root.get(), XmlCast("http://schemas.hp.com/imaging/escl/2011/05/03"),
XmlCast("scan"));
xmlNs* pwg =
xmlNewNs(root.get(), XmlCast("http://www.pwg.org/schemas/2010/12/sm"),
XmlCast("pwg"));
xmlNewNs(root.get(), XmlCast("http://www.w3.org/2001/XMLSchema-instance"),
XmlCast("xsi"));
xmlSetNs(root.get(), scan);
AddNodeWithContent(root.get(), pwg, "Version", "2.6.3");
AddNodeWithContent(root.get(), pwg, "State", status.idle ? "Idle" : "Busy");
AddNodeWithContent(root.get(), scan, "AdfState",
status.adf_empty ? "ScannerAdfEmpty" : "ScannerAdfLoaded");
// Add a list of all of the scan jobs
namespace_map namespaces;
namespaces[kPwgNs] = pwg;
namespaces[kScanNs] = scan;
UniqueXmlNodePtr jobs = JobListAsXml(status.jobs, &namespaces);
xmlAddChild(root.get(), jobs.release());
return Serialize(std::move(root));
}
base::Optional<ScanSettings> ScanSettingsFromXml(
const std::vector<uint8_t>& xml) {
// Since this document doesn't have a url, we use a filler value.
const char* url = "noname.xml";
// We don't know the encoding, so pass NULL to autodetect.
const char* encoding = NULL;
const UniqueXmlDocPtr doc(
xmlReadMemory(reinterpret_cast<const char*>(xml.data()), xml.size(), url,
encoding, /*options=*/0));
if (!doc) {
LOG(ERROR) << "Could not parse data as XML document";
return base::nullopt;
}
const xmlNode* root = xmlDocGetRootElement(doc.get());
if (!root) {
LOG(ERROR) << "XML document does not have root node";
return base::nullopt;
}
ScanSettings settings;
// Iterate over doubly-linked list of nodes in the document.
for (const xmlNode* node = root->children; node; node = node->next) {
if (StrEqual(node->name, "ScanRegions")) {
// Iterate over doubly-linked list of children of the ScanRegions node.
for (xmlNode* child = node->children; child; child = child->next) {
if (StrEqual(child->name, "ScanRegion")) {
base::Optional<ScanRegion> region = ScanRegionFromNode(child);
if (!region) {
LOG(ERROR) << "Failed to parse ScanRegion";
return base::nullopt;
}
settings.regions.push_back(region.value());
}
}
} else if (StrEqual(node->name, "DocumentFormat")) {
base::Optional<std::string> format = GetContent(node);
if (!format) {
LOG(ERROR) << "DocumentFormat node does not have valid contents.";
return base::nullopt;
}
settings.document_format = format.value();
} else if (StrEqual(node->name, "ColorMode")) {
base::Optional<std::string> color_mode_string = GetContent(node);
if (!color_mode_string) {
LOG(ERROR) << "ColorMode node does not have valid contents.";
return base::nullopt;
}
base::Optional<ColorMode> color_mode =
ColorModeFromString(color_mode_string.value());
if (!color_mode) {
LOG(ERROR) << "Invalid ColorMode value: " << color_mode_string.value();
return base::nullopt;
}
settings.color_mode = color_mode.value();
} else if (StrEqual(node->name, "InputSource")) {
base::Optional<std::string> source = GetContent(node);
if (!source) {
LOG(ERROR) << "InputSource node does not have valid contents.";
return base::nullopt;
}
settings.input_source = source.value();
} else if (StrEqual(node->name, "XResolution")) {
base::Optional<int> resolution = GetIntContent(node);
if (!resolution) {
LOG(ERROR) << "XResolution node does not have valid contents.";
return base::nullopt;
}
settings.x_resolution = resolution.value();
} else if (StrEqual(node->name, "YResolution")) {
base::Optional<int> resolution = GetIntContent(node);
if (!resolution) {
LOG(ERROR) << "YResolution node does not have valid contents.";
return base::nullopt;
}
settings.y_resolution = resolution.value();
}
}
return settings;
}