27constexpr const char* kCrLf =
"\r\n";
28constexpr const char* kSoapStartCid =
"root.message@cxf.apache.org";
30std::string make_boundary() {
31 static thread_local std::mt19937_64 rng(
32 static_cast<std::uint64_t
>(
33 std::chrono::steady_clock::now().time_since_epoch().count()));
34 std::uniform_int_distribution<std::uint64_t> dist;
35 std::ostringstream oss;
36 oss <<
"----=_Part_kcenon_" << std::hex << dist(rng) << dist(rng);
40bool contains(
const std::string& haystack,
const std::string&
needle) {
41 return haystack.find(
needle) != std::string::npos;
44bool bytes_contain(
const std::vector<std::uint8_t>& haystack,
45 const std::string&
needle) {
46 if (haystack.size() <
needle.size())
return false;
47 const auto n =
needle.size();
48 for (std::size_t i = 0; i + n <= haystack.size(); ++i) {
50 haystack.begin() +
static_cast<std::ptrdiff_t
>(i))) {
60 const std::string& envelope_xml,
const std::vector<mtom_part>& parts) {
61 if (envelope_xml.empty()) {
62 return kcenon::common::make_error<packaged_mtom>(
69 for (
int attempt = 0; attempt < 8; ++attempt) {
70 boundary = make_boundary();
71 if (contains(envelope_xml, boundary))
continue;
72 bool collides =
false;
73 for (
const auto& p : parts) {
74 if (bytes_contain(p.content, boundary)) {
85 return kcenon::common::make_error<packaged_mtom>(
87 "failed to generate a collision-free MIME boundary",
93 std::string(
"multipart/related; boundary=\"") + boundary +
94 std::string(
"\"; type=\"application/xop+xml\"; start=\"<") +
96 std::string(
">\"; start-info=\"application/soap+xml; action=\\\"urn:ihe:iti:2007:RegisterDocumentSet-b\\\"\"");
99 body.reserve(envelope_xml.size() + 4096);
104 body +=
"Content-Type: application/xop+xml; charset=UTF-8; type=\"application/soap+xml\"";
106 body +=
"Content-Transfer-Encoding: 8bit";
108 body +=
"Content-ID: <";
109 body += kSoapStartCid;
113 body += envelope_xml;
116 for (
const auto& p : parts) {
117 if (p.content_id.empty()) {
118 return kcenon::common::make_error<packaged_mtom>(
120 "part has empty content-id", std::string(
error_source));
122 if (p.content.empty()) {
123 return kcenon::common::make_error<packaged_mtom>(
125 "part '" + p.content_id +
"' has empty content",
132 body +=
"Content-Type: ";
133 body += (p.mime_type.empty() ?
"application/octet-stream" : p.mime_type);
135 body +=
"Content-Transfer-Encoding: binary";
137 body +=
"Content-ID: <";
138 body += p.content_id;
142 body.append(
reinterpret_cast<const char*
>(p.content.data()),
152 out.body = std::move(body);
162std::string extract_boundary_from_header(std::string_view header) {
163 if (header.empty())
return {};
164 constexpr std::string_view kName =
"boundary=";
165 std::size_t pos = std::string::npos;
166 for (std::size_t i = 0; i + kName.size() <= header.size(); ++i) {
168 for (std::size_t j = 0; j < kName.size(); ++j) {
169 const char c = header[i + j];
171 (c >=
'A' && c <= 'Z') ? static_cast<char>(c + 32) : c;
172 if (lc != kName[j]) {
178 pos = i + kName.size();
182 if (pos == std::string::npos)
return {};
183 if (pos < header.size() && header[pos] ==
'"') {
185 const auto end = header.find(
'"', pos);
186 if (end == std::string::npos)
return {};
187 return std::string(header.substr(pos, end - pos));
189 std::size_t end = pos;
190 while (end < header.size() && header[end] !=
';' &&
191 header[end] !=
' ' && header[end] !=
'\t' &&
192 header[end] !=
'\r' && header[end] !=
'\n') {
195 return std::string(header.substr(pos, end - pos));
203std::string detect_boundary(
const std::string& body) {
204 const auto pos = body.find(
"--");
205 if (pos == std::string::npos)
return {};
206 const std::size_t start = pos + 2;
207 std::size_t end = start;
208 while (end < body.size()) {
209 const char c = body[end];
210 if (c ==
'\r' || c ==
'\n' || c ==
' ' || c ==
'\t' || c ==
';' ||
216 if (end == start)
return {};
217 return body.substr(start, end - start);
222bool iequals_prefix(std::string_view line, std::string_view prefix) {
223 if (
line.size() < prefix.size())
return false;
224 for (std::size_t i = 0; i < prefix.size(); ++i) {
225 const char a =
line[i];
226 const char b = prefix[i];
227 const char la = (a >=
'A' && a <= 'Z') ? static_cast<char>(a + 32) : a;
228 const char lb = (b >=
'A' && b <= 'Z') ? static_cast<char>(b + 32) : b;
229 if (la != lb)
return false;
234std::string trim(std::string_view s) {
235 std::size_t start = 0;
236 std::size_t end = s.size();
237 while (start < end && (s[start] ==
' ' || s[start] ==
'\t')) ++start;
238 while (end > start && (s[end - 1] ==
' ' || s[end - 1] ==
'\t' ||
239 s[end - 1] ==
'\r' || s[end - 1] ==
'\n')) {
242 return std::string(s.substr(start, end - start));
247std::string strip_cid_brackets(std::string_view raw) {
248 std::string
out = trim(raw);
249 if (
out.size() >= 2 &&
out.front() ==
'<' &&
out.back() ==
'>') {
258 const std::string& content_type_header,
const std::string& body) {
262 return kcenon::common::make_error<parsed_mtom>(
271 std::size_t leading = 0;
272 while (leading < body.size() &&
273 (body[leading] ==
' ' || body[leading] ==
'\t' ||
274 body[leading] ==
'\r' || body[leading] ==
'\n')) {
277 if (body.compare(leading, 5,
"<?xml") == 0 ||
278 body.compare(leading, 5,
"<soap") == 0 ||
279 body.compare(leading, 14,
"<soap:Envelope") == 0) {
290 std::string boundary = extract_boundary_from_header(content_type_header);
291 if (boundary.empty()) {
292 boundary = detect_boundary(body);
294 if (boundary.empty()) {
295 return kcenon::common::make_error<parsed_mtom>(
297 "no MIME boundary detected in response body or Content-Type",
301 const std::string delim =
"--" + boundary;
306 std::vector<std::string> segments;
307 std::size_t cursor = 0;
308 while (cursor < body.size()) {
309 const auto next = body.find(delim, cursor);
310 if (next == std::string::npos)
break;
312 segments.push_back(body.substr(cursor, next - cursor));
314 cursor = next + delim.size();
316 if (cursor + 2 <= body.size() && body[cursor] ==
'-' &&
317 body[cursor + 1] ==
'-') {
322 if (segments.empty()) {
326 return kcenon::common::make_error<parsed_mtom>(
328 "multipart body yielded zero parts",
337 bool first_part_consumed =
false;
338 for (
auto& seg : segments) {
341 std::size_t start = 0;
342 while (start < seg.size() &&
343 (seg[start] ==
'\r' || seg[start] ==
'\n')) {
346 if (start >= seg.size())
continue;
348 const auto header_end = seg.find(
"\r\n\r\n", start);
349 const std::size_t content_start =
350 (header_end == std::string::npos) ? seg.size() : header_end + 4;
351 if (content_start > seg.size())
continue;
354 const std::string_view header_block(
356 (header_end == std::string::npos ? seg.size() - start
357 : header_end - start));
358 std::string mime_type;
359 std::string content_id;
361 while (pos < header_block.size()) {
362 const auto eol = header_block.find(
"\r\n", pos);
363 const std::size_t line_end =
364 (eol == std::string::npos) ? header_block.size() : eol;
365 const std::string_view line = header_block.substr(pos, line_end - pos);
366 if (iequals_prefix(line,
"Content-Type:")) {
367 mime_type = trim(line.substr(std::strlen(
"Content-Type:")));
368 const auto semi = mime_type.find(
';');
369 if (semi != std::string::npos) {
370 mime_type = trim(std::string_view(mime_type).substr(0, semi));
372 }
else if (iequals_prefix(line,
"Content-ID:")) {
373 content_id = strip_cid_brackets(
374 line.substr(std::strlen(
"Content-ID:")));
376 if (eol == std::string::npos)
break;
382 std::size_t content_end = seg.size();
383 while (content_end > content_start &&
384 (seg[content_end - 1] ==
'\r' ||
385 seg[content_end - 1] ==
'\n')) {
388 if (content_end < content_start) content_end = content_start;
389 const std::size_t content_len = content_end - content_start;
391 if (!first_part_consumed) {
392 out.root_xml.assign(seg, content_start, content_len);
393 first_part_consumed =
true;
400 reinterpret_cast<const std::uint8_t*
>(seg.data() + content_start),
401 reinterpret_cast<const std::uint8_t*
>(seg.data() + content_end));
402 out.parts.push_back(std::move(mp));
405 if (
out.root_xml.empty()) {
406 return kcenon::common::make_error<parsed_mtom>(
408 "multipart body had no root part with XML payload",
MTOM / XOP multipart/related packager for ITI-41.
kcenon::common::Result< packaged_mtom > package_mtom(const std::string &envelope_xml, const std::vector< mtom_part > &parts)
Assemble a multipart/related MTOM body.
kcenon::common::Result< parsed_mtom > parse_mtom_response(const std::string &content_type_header, const std::string &body)
Parse a multipart/related MTOM response body.
constexpr std::string_view error_source
Short identifier used as the "source" field of make_error.
@ consumer_response_mtom_malformed
@ mtom_boundary_generation_failed
@ consumer_response_missing_mtom_root
@ contains
CONTAINS - Parent contains child.
One attachment part, mapped 1:1 to an xop:Include in the envelope.
std::vector< std::uint8_t > content
Fully assembled MTOM request body.
Result of parsing an inbound multipart/related MTOM response.