PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
retrieve_response_parser.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
11
13
14#include <pugixml.hpp>
15
16#include <string>
17#include <string_view>
18
20
21namespace {
22
23// Response-size cap shared with the Source parse path. The http_client
24// write callback already bounds the body to 8 MiB, but the envelope we
25// pass to pugixml is an extracted substring from parse_mtom_response and
26// cannot grow past that cap either. Keep a defensive check in case a
27// future non-libcurl transport is wired without the same write limit.
28constexpr std::size_t kMaxRootXmlBytes = 8 * 1024 * 1024;
29
30// Literal ebRS status strings. Matching the Source behavior (non-Success
31// is an error; PartialSuccess is a typed error rather than a silent
32// success) for consistency between the two actors.
33constexpr std::string_view kSuccessStatus =
34 "urn:oasis:names:tc:ebxml-regrep:ResponseStatusType:Success";
35constexpr std::string_view kPartialSuccessStatus =
36 "urn:oasis:names:tc:ebxml-regrep:ResponseStatusType:PartialSuccess";
37
38// The xop:Include href is "cid:<bare-content-id>". Strip the prefix before
39// matching against the part content-ids that parse_mtom_response already
40// stored without angle brackets.
41std::string strip_cid_href(const std::string& href) {
42 constexpr std::string_view kPrefix = "cid:";
43 if (href.size() >= kPrefix.size() &&
44 href.compare(0, kPrefix.size(), kPrefix) == 0) {
45 return href.substr(kPrefix.size());
46 }
47 return href;
48}
49
50} // namespace
51
53 const std::string& root_xml, const std::vector<mtom_part>& parts,
54 const retrieve_request& req) {
55 if (root_xml.size() > kMaxRootXmlBytes) {
56 return kcenon::common::make_error<document_response>(
58 "RetrieveDocumentSetResponse root part exceeds 8 MiB cap (" +
59 std::to_string(root_xml.size()) + " bytes)",
60 std::string(error_source));
61 }
62
63 pugi::xml_document doc;
64 auto parse = doc.load_buffer(root_xml.data(), root_xml.size(),
65 pugi::parse_default, pugi::encoding_utf8);
66 if (!parse) {
67 return kcenon::common::make_error<document_response>(
69 std::string("failed to parse response envelope: ") +
70 parse.description(),
71 std::string(error_source));
72 }
73
74 // Use local-name() selectors so we do not depend on the specific
75 // namespace prefix the repository emits. Different XDS.b stacks
76 // (Apache CXF, .NET WIF, Gazelle) pick different prefixes for the
77 // xdsb namespace.
78 auto rds_response =
79 doc.select_node(
80 "//*[local-name()='RetrieveDocumentSetResponse']").node();
81 if (!rds_response) {
82 return kcenon::common::make_error<document_response>(
84 "response contains no RetrieveDocumentSetResponse element",
85 std::string(error_source));
86 }
87
88 auto registry_response =
89 rds_response.select_node("*[local-name()='RegistryResponse']").node();
90 if (!registry_response) {
91 return kcenon::common::make_error<document_response>(
93 "response has no RegistryResponse",
94 std::string(error_source));
95 }
96 const std::string status =
97 registry_response.attribute("status").as_string();
98 if (status.empty()) {
99 return kcenon::common::make_error<document_response>(
101 "RegistryResponse is missing status attribute",
102 std::string(error_source));
103 }
104 if (status == kPartialSuccessStatus) {
105 return kcenon::common::make_error<document_response>(
106 static_cast<int>(error_code::registry_partial_success),
107 "repository reported PartialSuccess: " + status,
108 std::string(error_source));
109 }
110 if (status != kSuccessStatus) {
111 // A Failure here means the repository could not locate the
112 // requested document - either the uid is unknown or the requester
113 // has no access. Either way, surface as not-found to the caller.
114 return kcenon::common::make_error<document_response>(
116 "repository reported non-success status: " + status,
117 std::string(error_source));
118 }
119
120 // Walk each DocumentResponse looking for the requested uid. Use
121 // local-name() selectors throughout because repositories emit the
122 // xdsb namespace with different prefixes (xdsb, ns2, ihe-xdsb, etc.).
123 pugi::xml_node match;
124 for (auto dr :
125 rds_response.select_nodes("*[local-name()='DocumentResponse']")) {
126 auto node = dr.node();
127 const std::string uid =
128 node.select_node("*[local-name()='DocumentUniqueId']")
129 .node()
130 .text()
131 .as_string();
132 if (uid == req.document_unique_id) {
133 match = node;
134 break;
135 }
136 }
137 if (!match) {
138 return kcenon::common::make_error<document_response>(
140 "no DocumentResponse matched requested uid " +
142 std::string(error_source));
143 }
144
145 const std::string mime_type =
146 match.select_node("*[local-name()='mimeType']").node().text().as_string();
147
148 auto document_node =
149 match.select_node("*[local-name()='Document']").node();
150 if (!document_node) {
151 return kcenon::common::make_error<document_response>(
153 "matching DocumentResponse has no Document element",
154 std::string(error_source));
155 }
156
157 // Prefer resolving an xop:Include reference to one of the MTOM
158 // attachment parts. Fall back to base64-inlined Document text when the
159 // repository returned a non-MTOM response (some stacks do that for
160 // small payloads or test responders).
161 auto include =
162 document_node.select_node("*[local-name()='Include']").node();
163
165 out.registry_response_status = status;
166 out.document_unique_id = req.document_unique_id;
167 out.repository_unique_id = req.repository_unique_id;
168 out.mime_type = mime_type;
169
170 if (include) {
171 const std::string raw_href = include.attribute("href").as_string();
172 const std::string cid = strip_cid_href(raw_href);
173 if (cid.empty()) {
174 return kcenon::common::make_error<document_response>(
175 static_cast<int>(
177 "xop:Include has empty href",
178 std::string(error_source));
179 }
180 const mtom_part* found = nullptr;
181 for (const auto& p : parts) {
182 if (p.content_id == cid) {
183 found = &p;
184 break;
185 }
186 }
187 if (found == nullptr) {
188 return kcenon::common::make_error<document_response>(
189 static_cast<int>(
191 "xop:Include references unknown cid: " + cid,
192 std::string(error_source));
193 }
194 out.content = found->content;
195 if (out.mime_type.empty()) {
196 out.mime_type = found->mime_type;
197 }
198 return out;
199 }
200
201 // Non-MTOM fallback: Document carries inline base64.
202 const std::string inline_b64 = document_node.text().as_string();
203 if (inline_b64.empty()) {
204 return kcenon::common::make_error<document_response>(
206 "Document element has neither xop:Include nor inline content",
207 std::string(error_source));
208 }
209 // Decode the inline base64 so document_response.content has the
210 // same "already-decoded binary bytes" semantics regardless of
211 // whether the repository used MTOM or inline-base64 framing. Review
212 // Minor-1 fix: avoid divergent caller expectations.
213 out.content = base64_decode_bytes(inline_b64);
214 if (out.content.empty()) {
215 return kcenon::common::make_error<document_response>(
217 "inline Document base64 decode failed or yielded empty bytes",
218 std::string(error_source));
219 }
220 return out;
221}
222
223} // namespace kcenon::pacs::ihe::xds::detail
std::vector< std::uint8_t > base64_decode_bytes(std::string_view b64)
Base64-decode b64 using the same BIO-backed decoder the signer and verifier use internally.
kcenon::common::Result< document_response > parse_retrieve_response(const std::string &root_xml, const std::vector< mtom_part > &parts, const retrieve_request &req)
Parse a RetrieveDocumentSetResponse envelope and resolve the attachment that matches req.
constexpr std::string_view error_source
Short identifier used as the "source" field of make_error.
Definition errors.h:95
Parser for IHE XDS.b ITI-43 RetrieveDocumentSetResponse.
std::string out
One attachment part, mapped 1:1 to an xop:Include in the envelope.
std::vector< std::uint8_t > content
ITI-43 RetrieveDocumentSet response payload.
ITI-43 RetrieveDocumentSet request target.
std::string_view uid
pugi::xml_node match
WS-Security XML-DSig signer backed by OpenSSL EVP_DigestSign.