From 2660a73f74c221177ee1bf4d5623cc47f94560ec Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:51:58 +0530 Subject: [PATCH 1/8] Update FileSystem.uwp.cs --- .../src/FileSystem/FileSystem.windows.cs | 93 ++++++++++++++++++- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/src/Essentials/src/FileSystem/FileSystem.windows.cs b/src/Essentials/src/FileSystem/FileSystem.windows.cs index d3484157da85..565f147d53f5 100644 --- a/src/Essentials/src/FileSystem/FileSystem.windows.cs +++ b/src/Essentials/src/FileSystem/FileSystem.windows.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.Maui.ApplicationModel; @@ -84,11 +85,87 @@ Task PlatformAppPackageFileExistsAsync(string filename) public partial class FileBase { + // Static mapping for file extensions to MIME types + // Used as fallback when Windows StorageFile.ContentType doesn't provide correct MIME type + static readonly Dictionary ExtensionToMimeTypeMap = new(StringComparer.OrdinalIgnoreCase) + { + // Image formats + { ".jpg", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".png", "image/png" }, + { ".gif", "image/gif" }, + { ".bmp", "image/bmp" }, + { ".svg", "image/svg+xml" }, + { ".webp", "image/webp" }, + { ".tiff", "image/tiff" }, + { ".tif", "image/tiff" }, + { ".ico", "image/x-icon" }, + + // Audio formats + { ".mp3", "audio/mpeg" }, + { ".wav", "audio/wav" }, + { ".flac", "audio/flac" }, + { ".aac", "audio/aac" }, + { ".ogg", "audio/ogg" }, + { ".wma", "audio/x-ms-wma" }, + + // Video formats + { ".mp4", "video/mp4" }, + { ".avi", "video/x-msvideo" }, + { ".mov", "video/quicktime" }, + { ".wmv", "video/x-ms-wmv" }, + { ".webm", "video/webm" }, + { ".mkv", "video/x-matroska" }, + { ".flv", "video/x-flv" }, + + // Document formats + { ".pdf", "application/pdf" }, + { ".doc", "application/msword" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".txt", "text/plain" }, + { ".rtf", "application/rtf" }, + + // Web formats + { ".html", "text/html" }, + { ".htm", "text/html" }, + { ".css", "text/css" }, + { ".js", "application/javascript" }, + { ".json", "application/json" }, + { ".xml", "text/xml" }, + + // Archive formats + { ".zip", "application/zip" }, + { ".rar", "application/x-rar-compressed" }, + { ".7z", "application/x-7z-compressed" }, + { ".tar", "application/x-tar" }, + { ".gz", "application/gzip" } + }; + internal FileBase(IStorageFile file) : this(file?.Path) { File = file; - ContentType = file?.ContentType; + + // Set the ContentType, but prefer our mapping for known problematic extensions + var fileContentType = file?.ContentType; + var extension = Path.GetExtension(file?.Path ?? ""); + + // If Windows provides a generic "application/octet-stream" or empty ContentType, + // try to get a more specific MIME type from our extension mapping + if (string.IsNullOrWhiteSpace(fileContentType) || + fileContentType == "application/octet-stream") + { + var betterContentType = PlatformGetContentType(extension); + ContentType = betterContentType ?? fileContentType; + } + else + { + ContentType = fileContentType; + } } void PlatformInit(FileBase file) @@ -98,8 +175,18 @@ void PlatformInit(FileBase file) internal IStorageFile File { get; set; } - // we can't do anything here, but Windows will take care of it - string PlatformGetContentType(string extension) => null; + // Use extension mapping as fallback when Windows doesn't provide correct MIME type + string PlatformGetContentType(string extension) + { + if (string.IsNullOrWhiteSpace(extension)) + return null; + + extension = extension.ToLowerInvariant(); + if (!extension.StartsWith(".")) + extension = "." + extension; + + return ExtensionToMimeTypeMap.TryGetValue(extension, out var mimeType) ? mimeType : null; + } internal async virtual Task PlatformOpenReadAsync() { From ae87bdb3359fc7060c91938fc6456ce5dc7fd42f Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:14:28 +0530 Subject: [PATCH 2/8] Update FileSystem.uwp.cs --- .../src/FileSystem/FileSystem.windows.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Essentials/src/FileSystem/FileSystem.windows.cs b/src/Essentials/src/FileSystem/FileSystem.windows.cs index 565f147d53f5..8d2d41b8a951 100644 --- a/src/Essentials/src/FileSystem/FileSystem.windows.cs +++ b/src/Essentials/src/FileSystem/FileSystem.windows.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net.Mime; using System.Threading.Tasks; using Microsoft.Maui.ApplicationModel; using Windows.Storage; @@ -90,15 +91,15 @@ public partial class FileBase static readonly Dictionary ExtensionToMimeTypeMap = new(StringComparer.OrdinalIgnoreCase) { // Image formats - { ".jpg", "image/jpeg" }, - { ".jpeg", "image/jpeg" }, + { ".jpg", MediaTypeNames.Image.Jpeg }, + { ".jpeg", MediaTypeNames.Image.Jpeg }, { ".png", "image/png" }, - { ".gif", "image/gif" }, + { ".gif", MediaTypeNames.Image.Gif }, { ".bmp", "image/bmp" }, { ".svg", "image/svg+xml" }, { ".webp", "image/webp" }, - { ".tiff", "image/tiff" }, - { ".tif", "image/tiff" }, + { ".tiff", MediaTypeNames.Image.Tiff }, + { ".tif", MediaTypeNames.Image.Tiff }, { ".ico", "image/x-icon" }, // Audio formats @@ -119,26 +120,26 @@ public partial class FileBase { ".flv", "video/x-flv" }, // Document formats - { ".pdf", "application/pdf" }, + { ".pdf", MediaTypeNames.Application.Pdf }, { ".doc", "application/msword" }, { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, { ".xls", "application/vnd.ms-excel" }, { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, { ".ppt", "application/vnd.ms-powerpoint" }, { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, - { ".txt", "text/plain" }, - { ".rtf", "application/rtf" }, + { ".txt", MediaTypeNames.Text.Plain }, + { ".rtf", MediaTypeNames.Application.Rtf }, // Web formats - { ".html", "text/html" }, - { ".htm", "text/html" }, + { ".html", MediaTypeNames.Text.Html }, + { ".htm", MediaTypeNames.Text.Html }, { ".css", "text/css" }, { ".js", "application/javascript" }, { ".json", "application/json" }, - { ".xml", "text/xml" }, + { ".xml", MediaTypeNames.Text.Xml }, // Archive formats - { ".zip", "application/zip" }, + { ".zip", MediaTypeNames.Application.Zip }, { ".rar", "application/x-rar-compressed" }, { ".7z", "application/x-7z-compressed" }, { ".tar", "application/x-tar" }, From 49295d02c6433e410103a4a269736925a3fc9805 Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:43:43 +0530 Subject: [PATCH 3/8] review changes updated --- .../src/FileSystem/FileSystem.windows.cs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Essentials/src/FileSystem/FileSystem.windows.cs b/src/Essentials/src/FileSystem/FileSystem.windows.cs index 8d2d41b8a951..786a8d0dba7d 100644 --- a/src/Essentials/src/FileSystem/FileSystem.windows.cs +++ b/src/Essentials/src/FileSystem/FileSystem.windows.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.IO; using System.Net.Mime; @@ -88,7 +89,7 @@ public partial class FileBase { // Static mapping for file extensions to MIME types // Used as fallback when Windows StorageFile.ContentType doesn't provide correct MIME type - static readonly Dictionary ExtensionToMimeTypeMap = new(StringComparer.OrdinalIgnoreCase) + static readonly FrozenDictionary ExtensionToMimeTypeMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { // Image formats { ".jpg", MediaTypeNames.Image.Jpeg }, @@ -143,8 +144,9 @@ public partial class FileBase { ".rar", "application/x-rar-compressed" }, { ".7z", "application/x-7z-compressed" }, { ".tar", "application/x-tar" }, + { ".tar.gz", "application/gzip" }, { ".gz", "application/gzip" } - }; + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); internal FileBase(IStorageFile file) : this(file?.Path) @@ -182,11 +184,34 @@ string PlatformGetContentType(string extension) if (string.IsNullOrWhiteSpace(extension)) return null; - extension = extension.ToLowerInvariant(); + // Trim whitespace and convert to lowercase for consistent matching + extension = extension.Trim().ToLowerInvariant(); + if (string.IsNullOrEmpty(extension)) + return null; + if (!extension.StartsWith(".")) extension = "." + extension; - return ExtensionToMimeTypeMap.TryGetValue(extension, out var mimeType) ? mimeType : null; + // Try exact match first + if (ExtensionToMimeTypeMap.TryGetValue(extension, out var mimeType)) + return mimeType; + + // For compound extensions like .tar.gz, try to match longer extensions first + // This handles cases where we have both .gz and .tar.gz in our mapping + var fileName = Path.GetFileNameWithoutExtension(extension); + if (!string.IsNullOrEmpty(fileName) && fileName.Contains('.', StringComparison.Ordinal)) + { + // Try progressively longer extensions (e.g., .tar.gz, then .gz) + var parts = fileName.Split('.'); + for (int i = parts.Length - 1; i >= 0; i--) + { + var compoundExtension = "." + string.Join(".", parts.Skip(i)) + extension; + if (ExtensionToMimeTypeMap.TryGetValue(compoundExtension, out mimeType)) + return mimeType; + } + } + + return null; } internal async virtual Task PlatformOpenReadAsync() From 970c0828e304afa1b445509e8aa4ccc498bb09ac Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Fri, 10 Oct 2025 13:17:33 +0530 Subject: [PATCH 4/8] fixed the errors --- src/Essentials/src/FileSystem/FileSystem.windows.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Essentials/src/FileSystem/FileSystem.windows.cs b/src/Essentials/src/FileSystem/FileSystem.windows.cs index 786a8d0dba7d..23e0c723bae7 100644 --- a/src/Essentials/src/FileSystem/FileSystem.windows.cs +++ b/src/Essentials/src/FileSystem/FileSystem.windows.cs @@ -2,6 +2,7 @@ using System.Collections.Frozen; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Mime; using System.Threading.Tasks; using Microsoft.Maui.ApplicationModel; @@ -196,8 +197,6 @@ string PlatformGetContentType(string extension) if (ExtensionToMimeTypeMap.TryGetValue(extension, out var mimeType)) return mimeType; - // For compound extensions like .tar.gz, try to match longer extensions first - // This handles cases where we have both .gz and .tar.gz in our mapping var fileName = Path.GetFileNameWithoutExtension(extension); if (!string.IsNullOrEmpty(fileName) && fileName.Contains('.', StringComparison.Ordinal)) { From 5b1ca5c5a1f93a61d27a5b3b4ce39c89d32125ac Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:27:53 +0530 Subject: [PATCH 5/8] Update FileSystem_Tests.cs --- .../DeviceTests/Tests/FileSystem_Tests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs b/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs index 90417c1cc279..d83b80e25c0e 100644 --- a/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs +++ b/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs @@ -103,3 +103,26 @@ public async Task CheckFileResultOpenReadAsyncMultipleTimes() } } } + +#if WINDOWS + [Theory] + [InlineData(".jpg", "image/jpeg")] + [InlineData(".JPG", "image/jpeg")] + [InlineData(".Jpg", "image/jpeg")] + [InlineData(".jPg", "image/jpeg")] + [InlineData(".jpg ", "image/jpeg")] // Trailing space + [InlineData(" .jpg", "image/jpeg")] // Leading space + [InlineData(" .jpg ", "image/jpeg")] // Leading and trailing spaces + [InlineData(".png", "image/png")] + [InlineData(".PNG", "image/png")] + [InlineData(".tar.gz", "application/gzip")] + [InlineData(".TAR.GZ", "application/gzip")] + public async Task EnsureFileResultContentType(string extension, string expectedMimeType) + { + string filePath = Path.Combine(FileSystem.CacheDirectory, $"test{extension}"); + await File.WriteAllTextAsync(filePath, $"File Content type is {expectedMimeType}"); + FileResult fileResult = new FileResult(filePath); + Assert.Equal(expectedMimeType, fileResult.ContentType); + File.Delete(filePath); + } +#endif \ No newline at end of file From f745a7d45f5290306d0177c37576733d79ae437e Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Wed, 27 May 2026 11:55:53 +0530 Subject: [PATCH 6/8] AI Summary added --- .../src/FileSystem/FileSystem.windows.cs | 114 ++++-------------- .../DeviceTests/Tests/FileSystem_Tests.cs | 4 +- 2 files changed, 23 insertions(+), 95 deletions(-) diff --git a/src/Essentials/src/FileSystem/FileSystem.windows.cs b/src/Essentials/src/FileSystem/FileSystem.windows.cs index 23e0c723bae7..4b9544273be3 100644 --- a/src/Essentials/src/FileSystem/FileSystem.windows.cs +++ b/src/Essentials/src/FileSystem/FileSystem.windows.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Frozen; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Net.Mime; using System.Threading.Tasks; using Microsoft.Maui.ApplicationModel; +using Microsoft.Win32; using Windows.Storage; using Package = Windows.ApplicationModel.Package; @@ -88,83 +85,19 @@ Task PlatformAppPackageFileExistsAsync(string filename) public partial class FileBase { - // Static mapping for file extensions to MIME types - // Used as fallback when Windows StorageFile.ContentType doesn't provide correct MIME type - static readonly FrozenDictionary ExtensionToMimeTypeMap = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - // Image formats - { ".jpg", MediaTypeNames.Image.Jpeg }, - { ".jpeg", MediaTypeNames.Image.Jpeg }, - { ".png", "image/png" }, - { ".gif", MediaTypeNames.Image.Gif }, - { ".bmp", "image/bmp" }, - { ".svg", "image/svg+xml" }, - { ".webp", "image/webp" }, - { ".tiff", MediaTypeNames.Image.Tiff }, - { ".tif", MediaTypeNames.Image.Tiff }, - { ".ico", "image/x-icon" }, - - // Audio formats - { ".mp3", "audio/mpeg" }, - { ".wav", "audio/wav" }, - { ".flac", "audio/flac" }, - { ".aac", "audio/aac" }, - { ".ogg", "audio/ogg" }, - { ".wma", "audio/x-ms-wma" }, - - // Video formats - { ".mp4", "video/mp4" }, - { ".avi", "video/x-msvideo" }, - { ".mov", "video/quicktime" }, - { ".wmv", "video/x-ms-wmv" }, - { ".webm", "video/webm" }, - { ".mkv", "video/x-matroska" }, - { ".flv", "video/x-flv" }, - - // Document formats - { ".pdf", MediaTypeNames.Application.Pdf }, - { ".doc", "application/msword" }, - { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, - { ".xls", "application/vnd.ms-excel" }, - { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, - { ".ppt", "application/vnd.ms-powerpoint" }, - { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, - { ".txt", MediaTypeNames.Text.Plain }, - { ".rtf", MediaTypeNames.Application.Rtf }, - - // Web formats - { ".html", MediaTypeNames.Text.Html }, - { ".htm", MediaTypeNames.Text.Html }, - { ".css", "text/css" }, - { ".js", "application/javascript" }, - { ".json", "application/json" }, - { ".xml", MediaTypeNames.Text.Xml }, - - // Archive formats - { ".zip", MediaTypeNames.Application.Zip }, - { ".rar", "application/x-rar-compressed" }, - { ".7z", "application/x-7z-compressed" }, - { ".tar", "application/x-tar" }, - { ".tar.gz", "application/gzip" }, - { ".gz", "application/gzip" } - }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); - internal FileBase(IStorageFile file) : this(file?.Path) { File = file; - - // Set the ContentType, but prefer our mapping for known problematic extensions + var fileContentType = file?.ContentType; - var extension = Path.GetExtension(file?.Path ?? ""); - - // If Windows provides a generic "application/octet-stream" or empty ContentType, - // try to get a more specific MIME type from our extension mapping - if (string.IsNullOrWhiteSpace(fileContentType) || - fileContentType == "application/octet-stream") + if (string.IsNullOrWhiteSpace(fileContentType) || + string.Equals(fileContentType, FileMimeTypes.OctetStream, StringComparison.OrdinalIgnoreCase)) { - var betterContentType = PlatformGetContentType(extension); - ContentType = betterContentType ?? fileContentType; + var registryContentType = PlatformGetContentType(Path.GetExtension(file?.Path ?? string.Empty)); + ContentType = !string.IsNullOrWhiteSpace(registryContentType) + ? registryContentType + : fileContentType; } else { @@ -179,35 +112,30 @@ void PlatformInit(FileBase file) internal IStorageFile File { get; set; } - // Use extension mapping as fallback when Windows doesn't provide correct MIME type - string PlatformGetContentType(string extension) + static string PlatformGetContentType(string extension) { if (string.IsNullOrWhiteSpace(extension)) return null; - // Trim whitespace and convert to lowercase for consistent matching - extension = extension.Trim().ToLowerInvariant(); - if (string.IsNullOrEmpty(extension)) - return null; + extension = extension.Trim(); if (!extension.StartsWith(".")) extension = "." + extension; - // Try exact match first - if (ExtensionToMimeTypeMap.TryGetValue(extension, out var mimeType)) - return mimeType; - - var fileName = Path.GetFileNameWithoutExtension(extension); - if (!string.IsNullOrEmpty(fileName) && fileName.Contains('.', StringComparison.Ordinal)) + try { - // Try progressively longer extensions (e.g., .tar.gz, then .gz) - var parts = fileName.Split('.'); - for (int i = parts.Length - 1; i >= 0; i--) +#pragma warning disable CA1416 // Validate platform compatibility + using var key = Registry.ClassesRoot.OpenSubKey(extension); + if (key?.GetValue("Content Type") is string contentType && + !string.IsNullOrWhiteSpace(contentType)) { - var compoundExtension = "." + string.Join(".", parts.Skip(i)) + extension; - if (ExtensionToMimeTypeMap.TryGetValue(compoundExtension, out mimeType)) - return mimeType; + return contentType; } +#pragma warning restore CA1416 // Validate platform compatibility + } + catch + { + // Registry access can fail in sandboxed environments; fall through to default content type. } return null; diff --git a/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs b/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs index d83b80e25c0e..ceb643ca74b8 100644 --- a/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs +++ b/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs @@ -115,8 +115,8 @@ public async Task CheckFileResultOpenReadAsyncMultipleTimes() [InlineData(" .jpg ", "image/jpeg")] // Leading and trailing spaces [InlineData(".png", "image/png")] [InlineData(".PNG", "image/png")] - [InlineData(".tar.gz", "application/gzip")] - [InlineData(".TAR.GZ", "application/gzip")] + [InlineData(".tar.gz", "application/x-gzip")] + [InlineData(".TAR.GZ", "application/x-gzip")] public async Task EnsureFileResultContentType(string extension, string expectedMimeType) { string filePath = Path.Combine(FileSystem.CacheDirectory, $"test{extension}"); From 038544d2d819f408b5e2aaa2b9972dcb54e2173f Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:49:45 +0530 Subject: [PATCH 7/8] Build error addressed --- src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs b/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs index ceb643ca74b8..0c641d85d3cc 100644 --- a/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs +++ b/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs @@ -101,8 +101,6 @@ public async Task CheckFileResultOpenReadAsyncMultipleTimes() File.Delete(filePath); } - } -} #if WINDOWS [Theory] @@ -125,4 +123,6 @@ public async Task EnsureFileResultContentType(string extension, string expectedM Assert.Equal(expectedMimeType, fileResult.ContentType); File.Delete(filePath); } -#endif \ No newline at end of file +#endif + } +} \ No newline at end of file From 448e1a0728af9ceb600fcb94f5c9e051f7aff8ad Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:08:47 +0530 Subject: [PATCH 8/8] Revert "AI Summary added" --- .../src/FileSystem/FileSystem.windows.cs | 114 ++++++++++++++---- .../DeviceTests/Tests/FileSystem_Tests.cs | 5 +- 2 files changed, 96 insertions(+), 23 deletions(-) diff --git a/src/Essentials/src/FileSystem/FileSystem.windows.cs b/src/Essentials/src/FileSystem/FileSystem.windows.cs index 4b9544273be3..23e0c723bae7 100644 --- a/src/Essentials/src/FileSystem/FileSystem.windows.cs +++ b/src/Essentials/src/FileSystem/FileSystem.windows.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Frozen; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Net.Mime; using System.Threading.Tasks; using Microsoft.Maui.ApplicationModel; -using Microsoft.Win32; using Windows.Storage; using Package = Windows.ApplicationModel.Package; @@ -85,19 +88,83 @@ Task PlatformAppPackageFileExistsAsync(string filename) public partial class FileBase { + // Static mapping for file extensions to MIME types + // Used as fallback when Windows StorageFile.ContentType doesn't provide correct MIME type + static readonly FrozenDictionary ExtensionToMimeTypeMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // Image formats + { ".jpg", MediaTypeNames.Image.Jpeg }, + { ".jpeg", MediaTypeNames.Image.Jpeg }, + { ".png", "image/png" }, + { ".gif", MediaTypeNames.Image.Gif }, + { ".bmp", "image/bmp" }, + { ".svg", "image/svg+xml" }, + { ".webp", "image/webp" }, + { ".tiff", MediaTypeNames.Image.Tiff }, + { ".tif", MediaTypeNames.Image.Tiff }, + { ".ico", "image/x-icon" }, + + // Audio formats + { ".mp3", "audio/mpeg" }, + { ".wav", "audio/wav" }, + { ".flac", "audio/flac" }, + { ".aac", "audio/aac" }, + { ".ogg", "audio/ogg" }, + { ".wma", "audio/x-ms-wma" }, + + // Video formats + { ".mp4", "video/mp4" }, + { ".avi", "video/x-msvideo" }, + { ".mov", "video/quicktime" }, + { ".wmv", "video/x-ms-wmv" }, + { ".webm", "video/webm" }, + { ".mkv", "video/x-matroska" }, + { ".flv", "video/x-flv" }, + + // Document formats + { ".pdf", MediaTypeNames.Application.Pdf }, + { ".doc", "application/msword" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".txt", MediaTypeNames.Text.Plain }, + { ".rtf", MediaTypeNames.Application.Rtf }, + + // Web formats + { ".html", MediaTypeNames.Text.Html }, + { ".htm", MediaTypeNames.Text.Html }, + { ".css", "text/css" }, + { ".js", "application/javascript" }, + { ".json", "application/json" }, + { ".xml", MediaTypeNames.Text.Xml }, + + // Archive formats + { ".zip", MediaTypeNames.Application.Zip }, + { ".rar", "application/x-rar-compressed" }, + { ".7z", "application/x-7z-compressed" }, + { ".tar", "application/x-tar" }, + { ".tar.gz", "application/gzip" }, + { ".gz", "application/gzip" } + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + internal FileBase(IStorageFile file) : this(file?.Path) { File = file; - + + // Set the ContentType, but prefer our mapping for known problematic extensions var fileContentType = file?.ContentType; - if (string.IsNullOrWhiteSpace(fileContentType) || - string.Equals(fileContentType, FileMimeTypes.OctetStream, StringComparison.OrdinalIgnoreCase)) + var extension = Path.GetExtension(file?.Path ?? ""); + + // If Windows provides a generic "application/octet-stream" or empty ContentType, + // try to get a more specific MIME type from our extension mapping + if (string.IsNullOrWhiteSpace(fileContentType) || + fileContentType == "application/octet-stream") { - var registryContentType = PlatformGetContentType(Path.GetExtension(file?.Path ?? string.Empty)); - ContentType = !string.IsNullOrWhiteSpace(registryContentType) - ? registryContentType - : fileContentType; + var betterContentType = PlatformGetContentType(extension); + ContentType = betterContentType ?? fileContentType; } else { @@ -112,30 +179,35 @@ void PlatformInit(FileBase file) internal IStorageFile File { get; set; } - static string PlatformGetContentType(string extension) + // Use extension mapping as fallback when Windows doesn't provide correct MIME type + string PlatformGetContentType(string extension) { if (string.IsNullOrWhiteSpace(extension)) return null; - extension = extension.Trim(); + // Trim whitespace and convert to lowercase for consistent matching + extension = extension.Trim().ToLowerInvariant(); + if (string.IsNullOrEmpty(extension)) + return null; if (!extension.StartsWith(".")) extension = "." + extension; - try + // Try exact match first + if (ExtensionToMimeTypeMap.TryGetValue(extension, out var mimeType)) + return mimeType; + + var fileName = Path.GetFileNameWithoutExtension(extension); + if (!string.IsNullOrEmpty(fileName) && fileName.Contains('.', StringComparison.Ordinal)) { -#pragma warning disable CA1416 // Validate platform compatibility - using var key = Registry.ClassesRoot.OpenSubKey(extension); - if (key?.GetValue("Content Type") is string contentType && - !string.IsNullOrWhiteSpace(contentType)) + // Try progressively longer extensions (e.g., .tar.gz, then .gz) + var parts = fileName.Split('.'); + for (int i = parts.Length - 1; i >= 0; i--) { - return contentType; + var compoundExtension = "." + string.Join(".", parts.Skip(i)) + extension; + if (ExtensionToMimeTypeMap.TryGetValue(compoundExtension, out mimeType)) + return mimeType; } -#pragma warning restore CA1416 // Validate platform compatibility - } - catch - { - // Registry access can fail in sandboxed environments; fall through to default content type. } return null; diff --git a/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs b/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs index 0c641d85d3cc..4a1a068d6b42 100644 --- a/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs +++ b/src/Essentials/test/DeviceTests/Tests/FileSystem_Tests.cs @@ -104,6 +104,7 @@ public async Task CheckFileResultOpenReadAsyncMultipleTimes() #if WINDOWS [Theory] + [InlineData(".webp", "image/webp")] [InlineData(".jpg", "image/jpeg")] [InlineData(".JPG", "image/jpeg")] [InlineData(".Jpg", "image/jpeg")] @@ -113,8 +114,8 @@ public async Task CheckFileResultOpenReadAsyncMultipleTimes() [InlineData(" .jpg ", "image/jpeg")] // Leading and trailing spaces [InlineData(".png", "image/png")] [InlineData(".PNG", "image/png")] - [InlineData(".tar.gz", "application/x-gzip")] - [InlineData(".TAR.GZ", "application/x-gzip")] + [InlineData(".tar.gz", "application/gzip")] + [InlineData(".TAR.GZ", "application/gzip")] public async Task EnsureFileResultContentType(string extension, string expectedMimeType) { string filePath = Path.Combine(FileSystem.CacheDirectory, $"test{extension}");