From b781a5ab0752a92a04b7c4894ae4bba465e1d896 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 5 Jan 2026 15:56:10 +0100 Subject: [PATCH] Refactored MIME handling to be static typed --- .../Chat/IImageSourceExtensions.cs | 51 ++++++------ .../Tools/MIME/ApplicationBuilder.cs | 63 +++++++++++++++ .../Tools/MIME/ApplicationSubtype.cs | 21 +++++ .../Tools/MIME/AudioBuilder.cs | 47 +++++++++++ .../Tools/MIME/AudioSubtype.cs | 16 ++++ app/MindWork AI Studio/Tools/MIME/BaseType.cs | 10 +++ app/MindWork AI Studio/Tools/MIME/Builder.cs | 80 +++++++++++++++++++ app/MindWork AI Studio/Tools/MIME/ISubtype.cs | 6 ++ .../Tools/MIME/ImageBuilder.cs | 44 ++++++++++ .../Tools/MIME/ImageSubtype.cs | 12 +++ app/MindWork AI Studio/Tools/MIME/MIMEType.cs | 16 ++++ .../Tools/MIME/TextBuilder.cs | 45 +++++++++++ .../Tools/MIME/TextSubtype.cs | 13 +++ .../Tools/MIME/VideoBuilder.cs | 42 ++++++++++ .../Tools/MIME/VideoSubtype.cs | 11 +++ 15 files changed, 450 insertions(+), 27 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/MIME/ApplicationBuilder.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/ApplicationSubtype.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/AudioBuilder.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/AudioSubtype.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/BaseType.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/Builder.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/ISubtype.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/ImageBuilder.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/ImageSubtype.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/MIMEType.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/TextBuilder.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/TextSubtype.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/VideoBuilder.cs create mode 100644 app/MindWork AI Studio/Tools/MIME/VideoSubtype.cs diff --git a/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs b/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs index 41706047..c6461643 100644 --- a/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs +++ b/app/MindWork AI Studio/Chat/IImageSourceExtensions.cs @@ -1,3 +1,4 @@ +using AIStudio.Tools.MIME; using AIStudio.Tools.PluginSystem; namespace AIStudio.Chat; @@ -6,7 +7,7 @@ public static class IImageSourceExtensions { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(IImageSourceExtensions).Namespace, nameof(IImageSourceExtensions)); - public static string DetermineMimeType(this IImageSource image) + public static MIMEType DetermineMimeType(this IImageSource image) { switch (image.SourceType) { @@ -18,13 +19,11 @@ public static class IImageSourceExtensions { var mimeEnd = base64Data.IndexOf(';'); if (mimeEnd > 5) - { - return base64Data[5..mimeEnd]; - } + return Builder.FromTextRepresentation(base64Data[5..mimeEnd]); } // Fallback: - return "application/octet-stream"; + return Builder.Create().UseApplication().UseSubtype(ApplicationSubtype.OCTET_STREAM).Build(); } case ContentImageSource.URL: @@ -32,38 +31,36 @@ public static class IImageSourceExtensions // Try to detect the mime type from the URL extension: var uri = new Uri(image.Source); var extension = Path.GetExtension(uri.AbsolutePath).ToLowerInvariant(); - return extension switch - { - ".png" => "image/png", - ".jpg" or ".jpeg" => "image/jpeg", - ".gif" => "image/gif", - ".bmp" => "image/bmp", - ".webp" => "image/webp", - - _ => "application/octet-stream" - }; + return DeriveMIMETypeFromExtension(extension); } case ContentImageSource.LOCAL_PATH: { var extension = Path.GetExtension(image.Source).ToLowerInvariant(); - return extension switch - { - ".png" => "image/png", - ".jpg" or ".jpeg" => "image/jpeg", - ".gif" => "image/gif", - ".bmp" => "image/bmp", - ".webp" => "image/webp", - - _ => "application/octet-stream" - }; + return DeriveMIMETypeFromExtension(extension); } default: - return "application/octet-stream"; + return Builder.Create().UseApplication().UseSubtype(ApplicationSubtype.OCTET_STREAM).Build(); } } - + + private static MIMEType DeriveMIMETypeFromExtension(string extension) + { + var imageBuilder = Builder.Create().UseImage(); + return extension switch + { + ".png" => imageBuilder.UseSubtype(ImageSubtype.PNG).Build(), + ".jpg" or ".jpeg" => imageBuilder.UseSubtype(ImageSubtype.JPEG).Build(), + ".gif" => imageBuilder.UseSubtype(ImageSubtype.GIF).Build(), + ".webp" => imageBuilder.UseSubtype(ImageSubtype.WEBP).Build(), + ".tiff" or ".tif" => imageBuilder.UseSubtype(ImageSubtype.TIFF).Build(), + ".heic" or ".heif" => imageBuilder.UseSubtype(ImageSubtype.HEIC).Build(), + + _ => Builder.Create().UseApplication().UseSubtype(ApplicationSubtype.OCTET_STREAM).Build() + }; + } + /// /// Read the image content as a base64 string. /// diff --git a/app/MindWork AI Studio/Tools/MIME/ApplicationBuilder.cs b/app/MindWork AI Studio/Tools/MIME/ApplicationBuilder.cs new file mode 100644 index 00000000..43cb9443 --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/ApplicationBuilder.cs @@ -0,0 +1,63 @@ +namespace AIStudio.Tools.MIME; + +public class ApplicationBuilder : Builder, ISubtype +{ + private ApplicationBuilder() + { + } + + private ApplicationSubtype subtype; + + public ApplicationBuilder UseSubtype(string subType) + { + this.subtype = subType.ToLowerInvariant() switch + { + "vnd.ms-excel" => ApplicationSubtype.EXCEL_OLD, + "vnd.ms-word" => ApplicationSubtype.WORD_OLD, + "vnd.ms-powerpoint" => ApplicationSubtype.POWERPOINT_OLD, + + "vnd.openxmlformats-officedocument.spreadsheetml.sheet" => ApplicationSubtype.EXCEL, + "vnd.openxmlformats-officedocument.wordprocessingml.document" => ApplicationSubtype.WORD, + "vnd.openxmlformats-officedocument.presentationml.presentation" => ApplicationSubtype.POWERPOINT, + + "octet-stream" => ApplicationSubtype.OCTET_STREAM, + + "json" => ApplicationSubtype.JSON, + "xml" => ApplicationSubtype.XML, + "pdf" => ApplicationSubtype.PDF, + "zip" => ApplicationSubtype.ZIP, + + "x-www-form-urlencoded" => ApplicationSubtype.X_WWW_FORM_URLENCODED, + _ => throw new ArgumentOutOfRangeException(nameof(subType), "Unsupported MIME application subtype.") + }; + + return this; + } + + public ApplicationBuilder UseSubtype(ApplicationSubtype subType) + { + this.subtype = subType; + return this; + } + + #region Implementation of IMIMESubtype + + public MIMEType Build() => new() + { + Type = this, + TextRepresentation = this.subtype switch + { + ApplicationSubtype.EXCEL_OLD => $"{this.baseType}/vnd.ms-excel".ToLowerInvariant(), + ApplicationSubtype.WORD_OLD => $"{this.baseType}/vnd.ms-word".ToLowerInvariant(), + ApplicationSubtype.POWERPOINT_OLD => $"{this.baseType}/vnd.ms-powerpoint".ToLowerInvariant(), + + ApplicationSubtype.EXCEL => $"{this.baseType}/vnd.openxmlformats-officedocument.spreadsheetml.sheet".ToLowerInvariant(), + ApplicationSubtype.WORD => $"{this.baseType}/vnd.openxmlformats-officedocument.wordprocessingml.document".ToLowerInvariant(), + ApplicationSubtype.POWERPOINT => $"{this.baseType}/vnd.openxmlformats-officedocument.presentationml.presentation".ToLowerInvariant(), + + _ => $"{this.baseType}/{this.subtype}".ToLowerInvariant() + } + }; + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/ApplicationSubtype.cs b/app/MindWork AI Studio/Tools/MIME/ApplicationSubtype.cs new file mode 100644 index 00000000..4224815e --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/ApplicationSubtype.cs @@ -0,0 +1,21 @@ +namespace AIStudio.Tools.MIME; + +public enum ApplicationSubtype +{ + OCTET_STREAM, + + JSON, + XML, + PDF, + ZIP, + X_WWW_FORM_URLENCODED, + + WORD_OLD, + WORD, + + EXCEL_OLD, + EXCEL, + + POWERPOINT_OLD, + POWERPOINT, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/AudioBuilder.cs b/app/MindWork AI Studio/Tools/MIME/AudioBuilder.cs new file mode 100644 index 00000000..e869f345 --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/AudioBuilder.cs @@ -0,0 +1,47 @@ +namespace AIStudio.Tools.MIME; + +public class AudioBuilder : Builder, ISubtype +{ + private AudioBuilder() + { + } + + private AudioSubtype subtype; + + public AudioBuilder UseSubtype(string subType) + { + this.subtype = subType.ToLowerInvariant() switch + { + "mpeg" => AudioSubtype.MPEG, + "wav" => AudioSubtype.WAV, + "ogg" => AudioSubtype.OGG, + "aac" => AudioSubtype.AAC, + "flac" => AudioSubtype.FLAC, + "webm" => AudioSubtype.WEBM, + "mp4" => AudioSubtype.MP4, + "mp3" => AudioSubtype.MP3, + "m4a" => AudioSubtype.M4A, + "aiff" => AudioSubtype.AIFF, + + _ => throw new ArgumentException("Unsupported MIME audio subtype.", nameof(subType)) + }; + + return this; + } + + public AudioBuilder UseSubtype(AudioSubtype subType) + { + this.subtype = subType; + return this; + } + + #region Implementation of IMIMESubtype + + public MIMEType Build() => new() + { + Type = this, + TextRepresentation = $"{this.baseType}/{this.subtype}".ToLowerInvariant() + }; + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/AudioSubtype.cs b/app/MindWork AI Studio/Tools/MIME/AudioSubtype.cs new file mode 100644 index 00000000..80ccba24 --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/AudioSubtype.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Tools.MIME; + +public enum AudioSubtype +{ + WAV, + MP3, + OGG, + AAC, + FLAC, + // ReSharper disable once InconsistentNaming + M4A, + MPEG, + MP4, + WEBM, + AIFF +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/BaseType.cs b/app/MindWork AI Studio/Tools/MIME/BaseType.cs new file mode 100644 index 00000000..76443f82 --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/BaseType.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Tools.MIME; + +public enum BaseType +{ + APPLICATION, + AUDIO, + IMAGE, + VIDEO, + TEXT, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/Builder.cs b/app/MindWork AI Studio/Tools/MIME/Builder.cs new file mode 100644 index 00000000..f437731e --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/Builder.cs @@ -0,0 +1,80 @@ +namespace AIStudio.Tools.MIME; + +public class Builder +{ + protected Builder() + { + } + + protected BaseType baseType; + + public static Builder Create() => new(); + + public static MIMEType FromTextRepresentation(string textRepresentation) + { + var parts = textRepresentation.Split('/'); + if (parts.Length != 2) + throw new ArgumentException("Invalid MIME type format.", nameof(textRepresentation)); + + var baseType = parts[0].ToLowerInvariant(); + var subType = parts[1].ToLowerInvariant(); + + var builder = Create(); + + switch (baseType) + { + case "application": + var appBuilder = builder.UseApplication(); + return appBuilder.UseSubtype(subType).Build(); + + case "text": + var textBuilder = builder.UseText(); + return textBuilder.UseSubtype(subType).Build(); + + case "audio": + var audioBuilder = builder.UseAudio(); + return audioBuilder.UseSubtype(subType).Build(); + + case "image": + var imageBuilder = builder.UseImage(); + return imageBuilder.UseSubtype(subType).Build(); + + case "video": + var videoBuilder = builder.UseVideo(); + return videoBuilder.UseSubtype(subType).Build(); + + default: + throw new ArgumentException("Unsupported base type.", nameof(textRepresentation)); + } + } + + public ApplicationBuilder UseApplication() + { + this.baseType = BaseType.APPLICATION; + return (ApplicationBuilder)this; + } + + public TextBuilder UseText() + { + this.baseType = BaseType.TEXT; + return (TextBuilder)this; + } + + public AudioBuilder UseAudio() + { + this.baseType = BaseType.AUDIO; + return (AudioBuilder)this; + } + + public ImageBuilder UseImage() + { + this.baseType = BaseType.IMAGE; + return (ImageBuilder)this; + } + + public VideoBuilder UseVideo() + { + this.baseType = BaseType.VIDEO; + return (VideoBuilder)this; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/ISubtype.cs b/app/MindWork AI Studio/Tools/MIME/ISubtype.cs new file mode 100644 index 00000000..517f6a3e --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/ISubtype.cs @@ -0,0 +1,6 @@ +namespace AIStudio.Tools.MIME; + +public interface ISubtype +{ + public MIMEType Build(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/ImageBuilder.cs b/app/MindWork AI Studio/Tools/MIME/ImageBuilder.cs new file mode 100644 index 00000000..4d12612f --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/ImageBuilder.cs @@ -0,0 +1,44 @@ +namespace AIStudio.Tools.MIME; + +public class ImageBuilder : Builder, ISubtype +{ + private ImageBuilder() + { + } + + private ImageSubtype subtype; + + public ImageBuilder UseSubtype(string subType) + { + this.subtype = subType.ToLowerInvariant() switch + { + "jpeg" or "jpg" => ImageSubtype.JPEG, + "png" => ImageSubtype.PNG, + "gif" => ImageSubtype.GIF, + "webp" => ImageSubtype.WEBP, + "tiff" or "tif" => ImageSubtype.TIFF, + "svg+xml" or "svg" => ImageSubtype.SVG, + "heic" => ImageSubtype.HEIC, + + _ => throw new ArgumentException("Unsupported MIME image subtype.", nameof(subType)) + }; + + return this; + } + + public ImageBuilder UseSubtype(ImageSubtype subType) + { + this.subtype = subType; + return this; + } + + #region Implementation of IMIMESubtype + + public MIMEType Build() => new() + { + Type = this, + TextRepresentation = $"{this.baseType}/{this.subtype}".ToLowerInvariant() + }; + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/ImageSubtype.cs b/app/MindWork AI Studio/Tools/MIME/ImageSubtype.cs new file mode 100644 index 00000000..73b11896 --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/ImageSubtype.cs @@ -0,0 +1,12 @@ +namespace AIStudio.Tools.MIME; + +public enum ImageSubtype +{ + JPEG, + PNG, + GIF, + TIFF, + WEBP, + SVG, + HEIC, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/MIMEType.cs b/app/MindWork AI Studio/Tools/MIME/MIMEType.cs new file mode 100644 index 00000000..adf45e6d --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/MIMEType.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Tools.MIME; + +public record MIMEType +{ + public required ISubtype Type { get; init; } + + public required string TextRepresentation { get; init; } + + #region Overrides of Object + + public override string ToString() => this.TextRepresentation; + + #endregion + + public static implicit operator string(MIMEType mimeType) => mimeType.TextRepresentation; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/TextBuilder.cs b/app/MindWork AI Studio/Tools/MIME/TextBuilder.cs new file mode 100644 index 00000000..abf2b944 --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/TextBuilder.cs @@ -0,0 +1,45 @@ +namespace AIStudio.Tools.MIME; + +public class TextBuilder : Builder, ISubtype +{ + private TextBuilder() + { + } + + private TextSubtype subtype; + + public TextBuilder UseSubtype(string subType) + { + this.subtype = subType.ToLowerInvariant() switch + { + "plain" => TextSubtype.PLAIN, + "html" => TextSubtype.HTML, + "css" => TextSubtype.CSS, + "csv" => TextSubtype.CSV, + "javascript" => TextSubtype.JAVASCRIPT, + "xml" => TextSubtype.XML, + "markdown" => TextSubtype.MARKDOWN, + "json" => TextSubtype.JSON, + + _ => throw new ArgumentException("Unsupported MIME text subtype.", nameof(subType)) + }; + + return this; + } + + public TextBuilder UseSubtype(TextSubtype subType) + { + this.subtype = subType; + return this; + } + + #region Implementation of IMIMESubtype + + public MIMEType Build() => new() + { + Type = this, + TextRepresentation = $"{this.baseType}/{this.subtype}".ToLowerInvariant() + }; + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/TextSubtype.cs b/app/MindWork AI Studio/Tools/MIME/TextSubtype.cs new file mode 100644 index 00000000..c3d34829 --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/TextSubtype.cs @@ -0,0 +1,13 @@ +namespace AIStudio.Tools.MIME; + +public enum TextSubtype +{ + PLAIN, + HTML, + CSS, + CSV, + JAVASCRIPT, + XML, + JSON, + MARKDOWN, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/VideoBuilder.cs b/app/MindWork AI Studio/Tools/MIME/VideoBuilder.cs new file mode 100644 index 00000000..ce6375e7 --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/VideoBuilder.cs @@ -0,0 +1,42 @@ +namespace AIStudio.Tools.MIME; + +public class VideoBuilder : Builder, ISubtype +{ + private VideoBuilder() + { + } + + private VideoSubtype subtype; + + public VideoBuilder UseSubtype(string subType) + { + this.subtype = subType.ToLowerInvariant() switch + { + "mp4" => VideoSubtype.MP4, + "webm" => VideoSubtype.WEBM, + "avi" => VideoSubtype.AVI, + "mov" => VideoSubtype.MOV, + "mkv" => VideoSubtype.MKV, + + _ => throw new ArgumentException("Unsupported MIME video subtype.", nameof(subType)) + }; + + return this; + } + + public VideoBuilder UseSubtype(VideoSubtype subType) + { + this.subtype = subType; + return this; + } + + #region Implementation of IMIMESubtype + + public MIMEType Build() => new() + { + Type = this, + TextRepresentation = $"{this.baseType}/{this.subtype}".ToLowerInvariant() + }; + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/MIME/VideoSubtype.cs b/app/MindWork AI Studio/Tools/MIME/VideoSubtype.cs new file mode 100644 index 00000000..cf152b1b --- /dev/null +++ b/app/MindWork AI Studio/Tools/MIME/VideoSubtype.cs @@ -0,0 +1,11 @@ +namespace AIStudio.Tools.MIME; + +public enum VideoSubtype +{ + MP4, + AVI, + MOV, + MKV, + WEBM, + MPEG, +} \ No newline at end of file