From 4e8e5639985cce3498bcdf549314ae73f17fc36e Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 7 Apr 2025 12:22:47 +0200 Subject: [PATCH 1/2] [mime] Try to choose default extension based on external source. Proof-of-concept using Android default extensions. The Debian and Apache mime.types are not trying to assign a *preferred* extension to mime types. They are interpreting existing extensions to guess the content's media type, or passing along existing files with existing extensions, not creating new files with new extensions. If there is a good source for the preferred extension for a media type, then we should use that. There might be media types that are so wide-spanning that no single extension can cover all of them correctly. Like the `application/x-msdownload` media type which the Apache web server associates with `.exe`, `.msi`, `.dll`, `.com` and `.bat` (not not `.cmd`). Those extensions are not interchangable, and using the wrong one would be incorrect. Such a mime type should possibly *not* have a default/preferred extension. --- pkgs/mime/lib/src/extension.dart | 107 ++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 22 deletions(-) diff --git a/pkgs/mime/lib/src/extension.dart b/pkgs/mime/lib/src/extension.dart index 293449a0e0..759d97560d 100644 --- a/pkgs/mime/lib/src/extension.dart +++ b/pkgs/mime/lib/src/extension.dart @@ -14,33 +14,96 @@ import 'default_extension_map.dart'; /// Used by [extensionFromMime]. final Map _defaultMimeTypeMap = { for (var entry in defaultExtensionMap.entries) entry.value: entry.key, - 'application/msword': 'doc', - 'application/vnd.ms-excel': 'xls', - 'application/vnd.ms-powerpoint': 'ppt', - 'application/x-debian-package': 'deb', - 'application/xhtml+xml': 'xhtml', - 'application/xml': 'xml', - 'audio/x-aiff': 'aif', - 'audio/midi': 'mid', + // GENERATED by tool/generate_defaults.dart + 'application/epub+zip': 'epub', + 'application/lrc': 'lrc', + 'application/pgp-signature': 'pgp', + 'application/pkix-cert': 'cer', + 'application/rss+xml': 'rss', + 'application/sdp': 'sdp', + 'application/smil+xml': 'smil', + 'application/ttml+xml': 'ttml', + 'application/vnd.android.ota': 'ota', + 'application/vnd.apple.mpegurl': 'm3u8', + 'application/vnd.ms-pki.stl': 'stl', + 'application/vnd.ms-powerpoint': 'pot', + 'application/vnd.ms-wpl': 'wpl', + 'application/vnd.stardivision.impress': 'sdp', + 'application/vnd.stardivision.writer': 'vor', + 'application/vnd.youtube.yt': 'yt', + 'application/x-android-drm-fl': 'fl', + 'application/x-flac': 'flac', + 'application/x-font': 'pcf', + 'application/x-mobipocket-ebook': 'prc', + 'application/x-mpegurl': 'm3u', + 'application/x-pem-file': 'pem', + 'application/x-pkcs12': 'p12', + 'application/x-subrip': 'srt', + 'application/x-webarchive': 'webarchive', + 'application/x-webarchive-xml': 'webarchivexml', + 'application/x-wifi-config': 'xml', + 'application/x-x509-ca-cert': 'crt', + 'application/x-x509-server-cert': 'crt', + 'application/x-x509-user-cert': 'crt', + 'audio/3gpp': '3ga', + 'audio/aac': 'aac', + 'audio/aac-adts': 'aac', + 'audio/ac3': 'ac3', + 'audio/amr': 'amr', + 'audio/basic': 'snd', + 'audio/flac': 'flac', + 'audio/imelody': 'imy', + 'audio/midi': 'rtx', + 'audio/mobile-xmf': 'mxmf', 'audio/mp4': 'm4a', - 'audio/ogg': 'ogg', + 'audio/mpeg': 'mp3', + 'audio/mpegurl': 'm3u', + 'audio/sp-midi': 'smf', + 'audio/x-matroska': 'mka', + 'audio/x-mpeg': 'mp3', + 'audio/x-mpegurl': 'm3u', + 'audio/x-pn-realaudio': 'ra', + 'image/avif': 'avif', + 'image/bmp': 'bmp', + 'image/gif': 'gif', + 'image/heic': 'heic', + 'image/heic-sequence': 'heics', + 'image/heif': 'heif', + 'image/heif-sequence': 'heifs', + 'image/ico': 'cur', 'image/jpeg': 'jpg', - 'image/tiff': 'tif', - 'image/svg+xml': 'svg', - 'model/vrml': 'vrml', - 'text/calendar': 'ics', - 'text/html': 'html', - 'text/javascript': 'js', - 'text/markdown': 'md', + 'image/webp': 'webp', + 'image/x-adobe-dng': 'dng', + 'image/x-fuji-raf': 'raf', + 'image/x-icon': 'ico', + 'image/x-ms-bmp': 'bmp', + 'image/x-nikon-nrw': 'nrw', + 'image/x-panasonic-rw2': 'rw2', + 'image/x-pentax-pef': 'pef', + 'image/x-samsung-srw': 'srw', + 'image/x-sony-arw': 'arw', + 'text/comma-separated-values': 'csv', 'text/plain': 'txt', - 'text/sgml': 'sgml', - 'text/x-asm': 'asm', - 'text/x-c': 'c', - 'text/x-pascal': 'pas', - 'video/mp4': 'mp4', - 'video/mpeg': 'mpg', + 'text/rtf': 'rtf', + 'text/text': 'phps', + 'text/x-c++hdr': 'hpp', + 'text/x-c++src': 'cpp', + 'text/x-vcard': 'vcf', + 'text/xml': 'xml', + 'video/3gpp': '3gpp', + 'video/3gpp2': '3gpp2', + 'video/avi': 'avi', + 'video/m4v': 'm4v', + 'video/mp2p': 'mpeg', + 'video/mp2t': 'm2ts', + 'video/mp2ts': 'ts', + 'video/mp4': 'm4v', + 'video/mpeg': 'mpeg', 'video/quicktime': 'mov', + 'video/vnd.youtube.yt': 'yt', 'video/x-matroska': 'mkv', + 'video/x-webex': 'wrf', + // END GENERATED }; /// The default file extension for a given MIME type. From 1a9d19ca5e159c02f84ca2b1c70d4fb06591930c Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 7 Apr 2025 12:48:26 +0200 Subject: [PATCH 2/2] Add generator file. Expects a file to be placed in /third_party/preferred_extension.mime.types. (Or pointed to on command line). --- pkgs/mime/tool/generate_defaults.dart | 112 ++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 pkgs/mime/tool/generate_defaults.dart diff --git a/pkgs/mime/tool/generate_defaults.dart b/pkgs/mime/tool/generate_defaults.dart new file mode 100644 index 0000000000..41244325bd --- /dev/null +++ b/pkgs/mime/tool/generate_defaults.dart @@ -0,0 +1,112 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +const startMarker = '\n // GENERATED by tool/generate_defaults.dart\n'; +const endMarker = '\n // END GENERATED\n'; +const androidMimeTypesPath = '../third_party/preferred_extension.mime.types'; +const extensionLibraryPath = '../lib/src/extension.dart'; + +void main(List args) { + var dryRun = false; + var printUsage = false; + var hadInvalidArgument = false; + Uri? mimeTypesOverride; + for (var arg in args) { + if (arg == '-h' || arg == '--help') { + printUsage = true; + } else if (arg == '-n' || arg == '--dryrun' || arg == '--dry-run') { + dryRun = true; + } else if (!arg.startsWith('-') && mimeTypesOverride == null) { + mimeTypesOverride = Directory.current.uri.resolve(arg); + } else { + hadInvalidArgument = true; + stderr.writeln('Unexpected command-line argument: $arg'); + } + } + if (hadInvalidArgument) { + stderr.writeln(usage); + exit(1); + } else if (printUsage) { + stdout.writeln(usage); + exit(0); + } + final script = Platform.script; + final library = script.resolve(extensionLibraryPath); + final libraryFile = File.fromUri(library); + final librarySource = libraryFile.readAsStringSync(); + final int startTagOffset, endTagOffset; + if (librarySource.indexOf(startMarker) case >= 0 && final start) { + startTagOffset = start; + if (librarySource.indexOf(endMarker, start + startMarker.length) + case >= 0 && final end) { + endTagOffset = end; + } else { + stderr.writeln('Library file (${libraryFile.path})\n' + ' does not contain end marker: $endMarker'); + exit(1); + } + } else { + stderr.writeln('Library file (${libraryFile.path})\n' + ' does not contain start marker: $startMarker'); + exit(1); + } + + final androidDefaults = + mimeTypesOverride ?? script.resolve(androidMimeTypesPath); + final defaultsFile = File.fromUri(androidDefaults); + if (!defaultsFile.existsSync()) { + stderr.writeln('Cannot find file: ${androidDefaults.toFilePath()}'); + if (mimeTypesOverride == null) { + stderr.writeln('Download file from rx: \n' + // ignore: missing_whitespace_between_adjacent_strings + ' https://android.googlesource.com/platform/frameworks/' + 'base/+/main/mime/java-res/android.mime.types'); + } + exit(1); + } + final defaults = defaultsFile.readAsLinesSync(); + final mapping = {}; + for (var line in defaults) { + if (line.startsWith('#')) continue; + final parts = line.split(' '); + var mediaType = parts[0]; + var defaultExtension = parts[1]; + if (defaultExtension.startsWith('?')) { + defaultExtension = defaultExtension.substring(1); + } + if (mediaType.startsWith('?')) { + mediaType = mediaType.substring(1); + mapping[mediaType] ??= defaultExtension; + } else { + mapping[mediaType] = defaultExtension; + } + } + final sortedKeys = mapping.keys.toList()..sort(); + final newMapping = + [for (var key in sortedKeys) " '$key': '${mapping[key]}',"].join('\n'); + final newLibrarySource = librarySource.replaceRange( + startTagOffset + startMarker.length, endTagOffset, newMapping); + if (newLibrarySource != librarySource) { + if (!dryRun) { + libraryFile.writeAsStringSync(newLibrarySource); + stdout.writeln('Changes to mapping written to file.'); + } else { + stdout.writeln('Changes to mapping. (Not written as dry-run.)'); + } + } else { + stdout.writeln('No change to mapping'); + } +} + +const String usage = ''' +Usage: dart tool/generate_defaults.dart [-h|-n] [MIMETYPES] + +--help or -h : Print this usage information +--dry-run or -n : Don't write changes back to library +MIMETYPES : Path to file in the `mime.types` format + where the first extension for each media type + is the preferred extension. +''';