diff --git a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java index 9c59593..6d9295c 100644 --- a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java +++ b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java @@ -1,12 +1,12 @@ package com.clearfolio.viewer.service; -import java.util.Optional; -import java.util.UUID; - import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Optional; +import java.util.UUID; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -62,6 +62,8 @@ public UUID submit(MultipartFile file, PolicyOverrideRequest overrideRequest) { PolicyOverrideRequest effectiveOverride = overrideRequest == null ? PolicyOverrideRequest.none() : overrideRequest; + // Security: File type, size limits, and basic content validation are enforced here. + // Processing sandboxing is handled by the worker environment. validationService.validateOrThrow(file, effectiveOverride); String contentHash = contentHash(file); @@ -120,13 +122,7 @@ private String contentHash(MultipartFile file) { digest.update(buffer, 0, read); } - byte[] raw = digest.digest(); - StringBuilder hex = new StringBuilder(raw.length * 2); - for (byte b : raw) { - hex.append(String.format("%02x", b)); - } - - return hex.toString(); + return HexFormat.of().formatHex(digest.digest()); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA-256 digest unavailable", ex); } catch (IOException ex) { diff --git a/src/test/java/com/clearfolio/viewer/service/DefaultDocumentConversionServiceTest.java b/src/test/java/com/clearfolio/viewer/service/DefaultDocumentConversionServiceTest.java index 7d07d6b..8b8280f 100644 --- a/src/test/java/com/clearfolio/viewer/service/DefaultDocumentConversionServiceTest.java +++ b/src/test/java/com/clearfolio/viewer/service/DefaultDocumentConversionServiceTest.java @@ -143,6 +143,33 @@ void returnsSameJobIdForDuplicatePayloads() { assertEquals(1, worker.enqueuedCount()); } + @Test + void submitStoresKnownSha256HexContentHash() { + InMemoryConversionJobRepository repository = new InMemoryConversionJobRepository(); + RecordingConversionWorker worker = new RecordingConversionWorker(); + DocumentConversionService service = new DefaultDocumentConversionService( + repository, + new DefaultDocumentValidationService(new ConversionProperties()), + worker, + new ConversionProperties() + ); + + MockMultipartFile file = new MockMultipartFile( + "file", + "contract.docx", + "application/octet-stream", + "hello".getBytes(StandardCharsets.UTF_8) + ); + + service.submit(file); + + String expected = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"; + ConversionJob job = repository.findByContentHash(expected).orElseThrow(); + assertEquals(64, job.getContentHash().length()); + assertTrue(job.getContentHash().matches("[0-9a-f]{64}")); + assertEquals(expected, job.getContentHash()); + } + @Test void returnsDifferentJobIdsForDifferentPayloads() { ConversionJobRepository repository = new InMemoryConversionJobRepository();