blob: 2688cfca47b8a95481868b2a834926d17dc8bf8f [file]
// Copyright (c) 2025, 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';
import 'package:path/path.dart' as path;
/// Additions to the base apache/httpd mime set.
const Map<String, List<String>> mimeAdditions = {
'application/dicom': ['dcm'],
'application/manifest+json': ['webmanifest'],
'application/toml': ['toml'],
'image/heic': ['heic'],
'image/heif': ['heif'],
'model/gltf-binary': ['glb'],
'model/gltf+json': ['gltf'],
'text/markdown': ['md', 'markdown'],
'text/x-dart': ['dart'],
};
/// Preferred extensions for mime types.
const Map<String, String> preferredExtensions = {
// For continuity with package:mime 2.0.0.
'application/mathematica': 'nb',
'application/mp21': 'mp21',
// Best practices for preferred file extensions.
'application/postscript': 'ps',
'application/smil+xml': 'smil',
'audio/mpeg': 'mp3',
'audio/ogg': 'ogg',
'image/jpeg': 'jpg',
'model/vrml': 'vrml',
'text/x-asm': 'asm',
'text/x-pascal': 'pas',
'video/mpeg': 'mpg',
'video/quicktime': 'mov',
};
/// Mime type renames.
///
/// Generally these are places where the mime.types table differs from
/// https://www.iana.org/assignments/media-types/media-types.xhtml.
const Map<String, String> mimeConversions = {
'audio/x-aac': 'audio/aac',
};
void main(List<String> args) {
final mimeTypes = readMimeTypes();
// Make any necessary mime name conversions.
for (final oldMime in mimeConversions.keys) {
final newMime = mimeConversions[oldMime]!;
mimeTypes[newMime] = mimeTypes.remove(oldMime)!;
}
// Make sure we don't have conflicts with our preferred extension mappings.
final customExtensions = preferredExtensions.values.toSet();
for (final exts in mimeAdditions.values) {
customExtensions.addAll(exts);
}
for (final entry in mimeTypes.entries.toList()) {
final exts = entry.value;
exts.removeWhere(customExtensions.contains);
if (exts.isEmpty) {
mimeTypes.remove(entry.key);
}
}
// Add additonal mime types.
for (final entry in mimeAdditions.entries) {
final mime = entry.key;
final exts = entry.value;
mimeTypes.putIfAbsent(mime, () => []).addAll(exts);
}
// Use our preferred extensions for specific types.
for (final entry in preferredExtensions.entries) {
final mime = entry.key;
final preferredExtension = entry.value;
final exts = mimeTypes[mime]!;
exts.remove(preferredExtension);
exts.insert(0, preferredExtension);
}
// Sort to normalize the table, but preserve the position of the first file
// extension.
final sortedMimeTypes = <String, List<String>>{};
for (final mime in mimeTypes.keys.toList()..sort()) {
final exts = mimeTypes[mime]!;
final defaultExt = exts.first;
final other = exts.sublist(1)..sort();
sortedMimeTypes[mime] = [defaultExt, ...other];
}
var file = writeMediaTypesFile(sortedMimeTypes);
print('Wrote ${path.relative(file.path)}.');
file = updateMediaTypesMarkdown(sortedMimeTypes);
print('Updated ${path.relative(file.path)}.');
}
Map<String, List<String>> readMimeTypes() {
final dataFile = File(path.join('third_party', 'httpd', 'mime.types'));
print('Reading ${path.relative(dataFile.path)}...');
final mimeToExtensions = <String, List<String>>{};
final wsRegex = RegExp(' +');
for (var line in dataFile
.readAsLinesSync()
.where((line) => !line.startsWith('#') && line.isNotEmpty)) {
line = line.replaceAll('\t', ' ').replaceAll(wsRegex, ' ');
final segments = line.split(' ');
final mime = segments[0];
final extensions = segments.sublist(1);
mimeToExtensions[mime] = extensions;
}
return mimeToExtensions;
}
File writeMediaTypesFile(Map<String, List<String>> mimeToExtensions) {
final out = StringBuffer();
// Write the media to file extensions mapping table.
out.writeln('''
/// A map of mime types to file extensions for that type.
///
/// If a mime type has multiple file extenions, the first in the list will be
/// returned as the preferred file extension for that type.''');
out.writeln('const Map<String, List<String>> mediaToExtensions = {');
for (final mime in mimeToExtensions.keys) {
final exts = mimeToExtensions[mime]!;
final describe = exts.map((ext) => "'$ext'").join(', ');
out.writeln(" '$mime': [$describe],");
}
out.writeln('};');
out.writeln();
// Write the file extension to media type mapping table.
out.writeln('/// A map of file extensions to their associated mime type.');
out.writeln('const Map<String, String> extensionToMedia = {');
final alreadySeen = <String, String>{};
for (final mime in mimeToExtensions.keys) {
final exts = mimeToExtensions[mime]!;
for (final ext in exts) {
if (alreadySeen.containsKey(ext)) {
print(
'- $ext already seen for ${alreadySeen[ext]}, not using for $mime');
} else {
out.writeln(" '$ext': '$mime',");
alreadySeen[ext] = mime;
}
}
}
out.writeln('};');
final outPath = path.join('lib/src/media_types.g.dart');
return _writeDartFile(outPath, out.toString());
}
File updateMediaTypesMarkdown(Map<String, List<String>> mimeToExtensions) {
// Emit the table.
const startMarker = '<!-- start table -->\n';
const endMarker = '<!-- end table -->\n';
final file = File.fromUri(Platform.script.resolve('../doc/media_types.md'));
final contents = file.readAsStringSync();
final prefix =
contents.substring(0, contents.indexOf(startMarker) + startMarker.length);
final suffix = contents.substring(contents.lastIndexOf(endMarker));
final buf = StringBuffer(prefix);
buf.writeln();
buf.write('''
| MIME type | Default | Additional |
| ---------------------------------------- | ----------- | ------------------- |
''');
for (final mime in mimeToExtensions.keys) {
final exts = mimeToExtensions[mime]!.toList();
final defaultExt = exts.first;
exts.remove(defaultExt);
final additional = exts.join(', ');
buf.writeln('| ${mime.padRight(40)} | '
'${defaultExt.padRight(11)} | '
'${additional.padRight(19)} |');
}
buf.writeln();
buf.write(suffix);
file.writeAsStringSync('${buf.toString().trim()}\n');
return file;
}
File _writeDartFile(String filePath, String content) {
final file = File(filePath);
file.writeAsStringSync('''
// Copyright (c) 2025, 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.
// Generated file - do not edit.
$content
''');
// Format the generated file.
Process.runSync(Platform.resolvedExecutable, ['format', filePath]);
return file;
}