PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
kcenon::pacs::web::dicomweb::multipart_parser Class Reference

Parser for multipart/related request bodies. More...

#include <dicomweb_endpoints.h>

Collaboration diagram for kcenon::pacs::web::dicomweb::multipart_parser:
Collaboration graph

Classes

struct  parse_error
 Parse error information. More...
 
struct  parse_result
 Parse result - either parts or error. More...
 

Static Public Member Functions

static auto parse (std::string_view content_type, std::string_view body) -> parse_result
 Parse a multipart/related request body.
 
static auto extract_boundary (std::string_view content_type) -> std::optional< std::string >
 Extract boundary from Content-Type header.
 
static auto extract_type (std::string_view content_type) -> std::optional< std::string >
 Extract type parameter from Content-Type header.
 

Static Private Member Functions

static auto parse_part_headers (std::string_view header_section) -> std::vector< std::pair< std::string, std::string > >
 Parse headers from a part's header section.
 

Detailed Description

Parser for multipart/related request bodies.

Parses incoming multipart/related requests as used by STOW-RS for uploading DICOM instances.

Example
auto parts = multipart_parser::parse(content_type_header, request_body);
for (const auto& part : parts) {
if (part.content_type == "application/dicom") {
// Process DICOM data
}
}
static auto parse(std::string_view content_type, std::string_view body) -> parse_result
Parse a multipart/related request body.

Definition at line 232 of file dicomweb_endpoints.h.

Member Function Documentation

◆ extract_boundary()

auto kcenon::pacs::web::dicomweb::multipart_parser::extract_boundary ( std::string_view content_type) -> std::optional<std::string>
staticnodiscard

Extract boundary from Content-Type header.

Parameters
content_typeThe Content-Type header value
Returns
Boundary string or nullopt if not found

Definition at line 332 of file dicomweb_endpoints.cpp.

333 {
334 // Find boundary parameter in Content-Type header
335 // Example: multipart/related; type="application/dicom"; boundary=----=_Part_123
336 auto boundary_pos = content_type.find("boundary=");
337 if (boundary_pos == std::string_view::npos) {
338 return std::nullopt;
339 }
340
341 auto value_start = boundary_pos + 9; // length of "boundary="
342 if (value_start >= content_type.size()) {
343 return std::nullopt;
344 }
345
346 // Check if boundary is quoted
347 if (content_type[value_start] == '"') {
348 auto end_quote = content_type.find('"', value_start + 1);
349 if (end_quote == std::string_view::npos) {
350 return std::nullopt;
351 }
352 return std::string(content_type.substr(value_start + 1,
353 end_quote - value_start - 1));
354 }
355
356 // Unquoted boundary - find end (semicolon, space, or end of string)
357 auto end_pos = content_type.find_first_of("; \t", value_start);
358 if (end_pos == std::string_view::npos) {
359 end_pos = content_type.size();
360 }
361 return std::string(content_type.substr(value_start, end_pos - value_start));
362}

◆ extract_type()

auto kcenon::pacs::web::dicomweb::multipart_parser::extract_type ( std::string_view content_type) -> std::optional<std::string>
staticnodiscard

Extract type parameter from Content-Type header.

Parameters
content_typeThe Content-Type header value
Returns
Type value or nullopt if not found

Definition at line 364 of file dicomweb_endpoints.cpp.

365 {
366 // Find type parameter in Content-Type header
367 // Example: multipart/related; type="application/dicom"
368 auto type_pos = content_type.find("type=");
369 if (type_pos == std::string_view::npos) {
370 return std::nullopt;
371 }
372
373 auto value_start = type_pos + 5; // length of "type="
374 if (value_start >= content_type.size()) {
375 return std::nullopt;
376 }
377
378 // Check if type is quoted
379 if (content_type[value_start] == '"') {
380 auto end_quote = content_type.find('"', value_start + 1);
381 if (end_quote == std::string_view::npos) {
382 return std::nullopt;
383 }
384 return std::string(content_type.substr(value_start + 1,
385 end_quote - value_start - 1));
386 }
387
388 // Unquoted type
389 auto end_pos = content_type.find_first_of("; \t", value_start);
390 if (end_pos == std::string_view::npos) {
391 end_pos = content_type.size();
392 }
393 return std::string(content_type.substr(value_start, end_pos - value_start));
394}

◆ parse()

auto kcenon::pacs::web::dicomweb::multipart_parser::parse ( std::string_view content_type,
std::string_view body ) -> parse_result
staticnodiscard

Parse a multipart/related request body.

Parameters
content_typeThe Content-Type header value (must include boundary)
bodyThe raw request body
Returns
Parse result containing parts or error

Definition at line 436 of file dicomweb_endpoints.cpp.

437 {
438 parse_result result;
439
440 // Extract boundary from Content-Type
441 auto boundary_opt = extract_boundary(content_type);
442 if (!boundary_opt) {
443 result.error = parse_error{
444 "INVALID_BOUNDARY",
445 "Missing or invalid boundary in Content-Type header"};
446 return result;
447 }
448
449 const std::string& boundary = *boundary_opt;
450 std::string delimiter = "--" + boundary;
451 std::string end_delimiter = "--" + boundary + "--";
452
453 // Find first boundary
454 auto pos = body.find(delimiter);
455 if (pos == std::string_view::npos) {
456 result.error = parse_error{
457 "NO_PARTS",
458 "No parts found in multipart body"};
459 return result;
460 }
461
462 // Skip to after first boundary and CRLF
463 pos += delimiter.size();
464 if (pos < body.size() && body.substr(pos, 2) == "\r\n") {
465 pos += 2;
466 }
467
468 // Parse each part
469 while (pos < body.size()) {
470 // Check for end delimiter
471 if (body.substr(pos).starts_with("--")) {
472 break; // End of multipart
473 }
474
475 // Find next boundary
476 auto next_boundary = body.find(delimiter, pos);
477 if (next_boundary == std::string_view::npos) {
478 // No more parts
479 break;
480 }
481
482 // Extract part content (excluding trailing CRLF before boundary)
483 auto part_content = body.substr(pos, next_boundary - pos);
484 if (part_content.size() >= 2 &&
485 part_content.substr(part_content.size() - 2) == "\r\n") {
486 part_content = part_content.substr(0, part_content.size() - 2);
487 }
488
489 // Split part into headers and body
490 auto header_end = part_content.find("\r\n\r\n");
491 if (header_end == std::string_view::npos) {
492 // Malformed part - skip
493 pos = next_boundary + delimiter.size();
494 if (pos < body.size() && body.substr(pos, 2) == "\r\n") {
495 pos += 2;
496 }
497 continue;
498 }
499
500 auto header_section = part_content.substr(0, header_end);
501 auto body_section = part_content.substr(header_end + 4);
502
503 // Parse headers
504 auto headers = parse_part_headers(header_section);
505
506 // Create multipart_part
507 multipart_part part;
508 for (const auto& [name, value] : headers) {
509 if (name == "content-type") {
510 part.content_type = value;
511 } else if (name == "content-location") {
512 part.content_location = value;
513 } else if (name == "content-id") {
514 part.content_id = value;
515 }
516 }
517
518 // Default content type if not specified
519 if (part.content_type.empty()) {
520 part.content_type = std::string(media_type::dicom);
521 }
522
523 // Copy body data
524 part.data.assign(
525 reinterpret_cast<const uint8_t*>(body_section.data()),
526 reinterpret_cast<const uint8_t*>(body_section.data() +
527 body_section.size()));
528
529 result.parts.push_back(std::move(part));
530
531 // Move to next part
532 pos = next_boundary + delimiter.size();
533 if (pos < body.size() && body.substr(pos, 2) == "\r\n") {
534 pos += 2;
535 }
536 }
537
538 if (result.parts.empty()) {
539 result.error = parse_error{
540 "NO_VALID_PARTS",
541 "No valid parts found in multipart body"};
542 }
543
544 return result;
545}
static auto extract_boundary(std::string_view content_type) -> std::optional< std::string >
Extract boundary from Content-Type header.
static auto parse_part_headers(std::string_view header_section) -> std::vector< std::pair< std::string, std::string > >
Parse headers from a part's header section.
static constexpr std::string_view dicom
std::string_view name

References kcenon::pacs::web::dicomweb::multipart_part::content_id, kcenon::pacs::web::dicomweb::multipart_part::content_location, kcenon::pacs::web::dicomweb::multipart_part::content_type, kcenon::pacs::web::dicomweb::multipart_part::data, kcenon::pacs::web::dicomweb::media_type::dicom, kcenon::pacs::web::dicomweb::multipart_parser::parse_result::error, name, and kcenon::pacs::web::dicomweb::multipart_parser::parse_result::parts.

◆ parse_part_headers()

auto kcenon::pacs::web::dicomweb::multipart_parser::parse_part_headers ( std::string_view header_section) -> std::vector<std::pair<std::string, std::string>>
staticnodiscardprivate

Parse headers from a part's header section.

Parameters
header_sectionThe raw header section
Returns
Map of header name to value

Definition at line 396 of file dicomweb_endpoints.cpp.

397 {
398 std::vector<std::pair<std::string, std::string>> headers;
399
400 size_t pos = 0;
401 while (pos < header_section.size()) {
402 // Find end of line
403 auto line_end = header_section.find("\r\n", pos);
404 if (line_end == std::string_view::npos) {
405 line_end = header_section.size();
406 }
407
408 auto line = header_section.substr(pos, line_end - pos);
409 if (line.empty()) {
410 break;
411 }
412
413 // Parse header: name: value
414 auto colon_pos = line.find(':');
415 if (colon_pos != std::string_view::npos) {
416 auto name = trim(line.substr(0, colon_pos));
417 auto value = trim(line.substr(colon_pos + 1));
418
419 // Convert header name to lowercase for case-insensitive matching
420 std::string name_lower;
421 name_lower.reserve(name.size());
422 for (char c : name) {
423 name_lower += static_cast<char>(std::tolower(
424 static_cast<unsigned char>(c)));
425 }
426
427 headers.emplace_back(std::move(name_lower), std::string(value));
428 }
429
430 pos = line_end + 2; // Skip \r\n
431 }
432
433 return headers;
434}

References name.


The documentation for this class was generated from the following files: