From 1af158840e6f255a279a3226d56116e9c65ae365 Mon Sep 17 00:00:00 2001 From: TekH Date: Mon, 20 Jul 2026 16:31:47 +0200 Subject: [PATCH] refactor(infrastructure): implement Stream-based PDF processing DevExpressPdfProcessor: - ValidateAsync, ValidatePdfAAsync, CheckAttachmentsAsync: Stream parameters - Defensive Position=0 validation (BadRequestException for seekable streams not at beginning) - Remove unsafe Position reset (non-seekable stream compatibility) - Remove PdfProcessingException wrapping (let DevExpress exceptions propagate naturally) DevExpressSwissQrCodeProcessor: - ExtractSwissQrCodeAsync: Stream parameter - Defensive Position=0 validation - Remove unsafe Position reset Memory optimization: MemoryStream.TryGetBuffer fast path for byte[] extraction --- .../PdfProcessing/DevExpressPdfProcessor.cs | 39 ++++++++++++++++--- .../DevExpressSwissQrCodeProcessor.cs | 18 ++++++--- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs b/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs index 12b231d..6a6d380 100644 --- a/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs +++ b/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs @@ -28,6 +28,12 @@ public class DevExpressPdfProcessor : IPdfProcessor throw new BadRequestException("PDF stream cannot be empty"); } + // Defensive validation: Seekable streams must be at Position = 0 + if (pdfStream.CanSeek && pdfStream.Position != 0) + { + throw new BadRequestException("PDF stream must be positioned at the beginning (Position = 0)."); + } + // 2. Read stream to byte array for raw data analysis // (DevExpress needs byte[] for some operations like attachment detection) byte[] pdfBytes; @@ -39,14 +45,18 @@ public class DevExpressPdfProcessor : IPdfProcessor else { // Slow path: copy stream to byte array - pdfStream.Position = 0; using var memoryStream = new MemoryStream(); await pdfStream.CopyToAsync(memoryStream); pdfBytes = memoryStream.ToArray(); } // 3. Load PDF with DevExpress Document API - pdfStream.Position = 0; + // Reset position for DevExpress (seekable streams only) + if (pdfStream.CanSeek) + { + pdfStream.Position = 0; + } + using var processor = new PdfDocumentProcessor(); processor.LoadDocument(pdfStream); @@ -87,6 +97,12 @@ public class DevExpressPdfProcessor : IPdfProcessor throw new BadRequestException("PDF stream cannot be empty"); } + // Defensive validation: Seekable streams must be at Position = 0 + if (pdfStream.CanSeek && pdfStream.Position != 0) + { + throw new BadRequestException("PDF stream must be positioned at the beginning (Position = 0)."); + } + // 2. Read stream to byte array for raw data analysis byte[] pdfBytes; if (pdfStream is MemoryStream ms && ms.TryGetBuffer(out var buffer)) @@ -95,14 +111,18 @@ public class DevExpressPdfProcessor : IPdfProcessor } else { - pdfStream.Position = 0; using var memoryStream = new MemoryStream(); await pdfStream.CopyToAsync(memoryStream); pdfBytes = memoryStream.ToArray(); } // 3. Load PDF with DevExpress Document API - pdfStream.Position = 0; + // Reset position for DevExpress (seekable streams only) + if (pdfStream.CanSeek) + { + pdfStream.Position = 0; + } + using var processor = new PdfDocumentProcessor(); processor.LoadDocument(pdfStream); @@ -175,8 +195,15 @@ public class DevExpressPdfProcessor : IPdfProcessor throw new BadRequestException("PDF stream cannot be empty"); } - // 2. Load PDF with DevExpress Document API (exceptions propagate naturally) - pdfStream.Position = 0; + // Defensive validation: Seekable streams must be at Position = 0 + if (pdfStream.CanSeek && pdfStream.Position != 0) + { + throw new BadRequestException("PDF stream must be positioned at the beginning (Position = 0)."); + } + + // 2. Load PDF with DevExpress Document API + // DevExpress LoadDocument may throw exceptions for corrupted PDFs - let them propagate naturally + // Middleware will catch and convert to 500 Internal Server Error using var processor = new PdfDocumentProcessor(); processor.LoadDocument(pdfStream); diff --git a/DocumentOperator.Infrastructure/Services/QrCodeProcessing/DevExpressSwissQrCodeProcessor.cs b/DocumentOperator.Infrastructure/Services/QrCodeProcessing/DevExpressSwissQrCodeProcessor.cs index 7d50f42..50fd34f 100644 --- a/DocumentOperator.Infrastructure/Services/QrCodeProcessing/DevExpressSwissQrCodeProcessor.cs +++ b/DocumentOperator.Infrastructure/Services/QrCodeProcessing/DevExpressSwissQrCodeProcessor.cs @@ -24,19 +24,27 @@ public sealed class DevExpressSwissQrCodeProcessor : ISwissQrCodeProcessor /// public async Task<(Bill Bill, string[] RawLines)> ExtractSwissQrCodeAsync( - byte[] pdfBytes, + Stream pdfStream, int[]? pageNumbers = null, CancellationToken cancellationToken = default) { - if (pdfBytes.Length == 0) - throw new ArgumentException("PDF document contains no byte data.", nameof(pdfBytes)); + ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream)); + + if (pdfStream.Length == 0) + throw new ArgumentException("PDF stream is empty.", nameof(pdfStream)); + + // Defensive validation: Seekable streams must be at Position = 0 + // Non-seekable streams (e.g., NetworkStream) are not checked + if (pdfStream.CanSeek && pdfStream.Position != 0) + throw new BadRequestException(null, new ArgumentException( + "PDF stream must be positioned at the beginning (Position = 0).", + nameof(pdfStream))); using var pdfDocument = new PdfDocumentProcessor(); - using var pdfStream = new MemoryStream(pdfBytes); pdfDocument.LoadDocument(pdfStream); if (pdfDocument.Document.Pages.Count == 0) - throw new ArgumentException("PDF document contains no pages.", nameof(pdfBytes)); + throw new ArgumentException("PDF document contains no pages.", nameof(pdfStream)); // Determine which pages to scan int[] pagesToScan = DeterminePageNumbers(pdfDocument.Document.Pages.Count, pageNumbers);