using FluentValidation; using Microsoft.AspNetCore.Http; namespace Features.ImageUploads.Commands.UploadPhoto; /// Validates . public class UploadPhotoValidator : AbstractValidator { private static readonly byte[] PngSignature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; private static readonly byte[] JpegSignature = [0xFF, 0xD8, 0xFF]; private static readonly byte[] RiffSignature = [ 0x52, 0x49, 0x46, 0x46, // "RIFF" ]; private static readonly byte[] WebPMarker = [ 0x57, 0x45, 0x42, 0x50, // "WEBP" ]; private const long MinSizeBytes = 1024; // 1 KiB private const long MaxSizeBytes = 24 * 1024 * 1024; // 24 MiB public UploadPhotoValidator() { RuleFor(x => x.UploadedById).NotEmpty().WithMessage("UploadedById is required."); RuleFor(x => x.File).NotNull().WithMessage("File is required."); RuleFor(x => x.Key).NotEmpty().WithMessage("Key is required."); RuleFor(x => x.File.ContentType) .Must(contentType => contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) ) .WithMessage("File must be an image."); RuleFor(x => x.File.Length) .InclusiveBetween(MinSizeBytes, MaxSizeBytes) .WithMessage("The file must be between 1 KiB and 24 MiB."); RuleFor(x => x.File) .MustAsync(HaveValidImageSignatureAsync) .WithMessage("The file is not a valid PNG, JPEG, or WebP image."); } private static async Task HaveValidImageSignatureAsync( IFormFile file, CancellationToken cancellationToken ) { var buffer = new byte[12]; await using var stream = file.OpenReadStream(); var bytesRead = await stream.ReadAsync( buffer.AsMemory(0, buffer.Length), cancellationToken ); return IsPng(buffer, bytesRead) || IsJpeg(buffer, bytesRead) || IsWebP(buffer, bytesRead); } private static bool IsPng(byte[] buffer, int bytesRead) => bytesRead >= PngSignature.Length && buffer.AsSpan(0, PngSignature.Length).SequenceEqual(PngSignature); private static bool IsJpeg(byte[] buffer, int bytesRead) => bytesRead >= JpegSignature.Length && buffer.AsSpan(0, JpegSignature.Length).SequenceEqual(JpegSignature); private static bool IsWebP(byte[] buffer, int bytesRead) => bytesRead >= 12 && buffer.AsSpan(0, 4).SequenceEqual(RiffSignature) && buffer.AsSpan(8, 4).SequenceEqual(WebPMarker); }