diff --git a/src/engine/mime/mime-content-type.vala b/src/engine/mime/mime-content-type.vala index 397c0f2d..6f7350b3 100644 --- a/src/engine/mime/mime-content-type.vala +++ b/src/engine/mime/mime-content-type.vala @@ -11,18 +11,91 @@ */ public class Geary.Mime.ContentType : Geary.BaseObject { - /* + + /** * MIME wildcard for comparing {@link media_type} and {@link media_subtype}. * * @see is_type */ public const string WILDCARD = "*"; - + /** * Default Content-Type for unknown or unmarked content. */ public const string DEFAULT_CONTENT_TYPE = "application/octet-stream"; - + + + private static Gee.Map TYPES_TO_EXTENSIONS = + new Gee.HashMap(); + + static construct { + // XXX We should be loading file name extension information + // from /etc/mime.types and/or the XDG Shared MIME-info + // Database globs2 file, usually located at + // "/usr/share/mime/globs2" (See: {@link + // https://specifications.freedesktop.org/shared-mime-info-spec/latest/}). + // + // But for now the most part the only things that we have to + // guess this for are inline embeds that don't have filenames, + // i.e. images, so we can hopefully get away with the set + // below for now. + TYPES_TO_EXTENSIONS["image/jpeg"] = ".jpeg"; + TYPES_TO_EXTENSIONS["image/png"] = ".png"; + TYPES_TO_EXTENSIONS["image/gif"] = ".gif"; + TYPES_TO_EXTENSIONS["image/svg+xml"] = ".svg"; + TYPES_TO_EXTENSIONS["image/bmp"] = ".bmp"; + TYPES_TO_EXTENSIONS["image/x-bmp"] = ".bmp"; + } + + public static ContentType deserialize(string str) throws MimeError { + // perform a little sanity checking here, as it doesn't appear the GMime constructor has + // any error-reporting at all + if (String.is_empty(str)) + throw new MimeError.PARSE("Empty MIME Content-Type"); + + if (!str.contains("/")) + throw new MimeError.PARSE("Invalid MIME Content-Type: %s", str); + + return new ContentType.from_gmime(new GMime.ContentType.from_string(str)); + } + + /** + * Attempts to guess the content type for a buffer using GIO sniffing. + */ + public static ContentType guess_type(string? file_name, Geary.Memory.Buffer? buf) throws Error { + string? mime_type = null; + + if (file_name != null) { + // XXX might just want to use xdgmime lib directly here to + // avoid the intermediate glib_content_type step here? + string glib_type = GLib.ContentType.guess(file_name, null, null); + mime_type = GLib.ContentType.get_mime_type(glib_type); + if (Geary.String.is_empty(mime_type)) { + mime_type = null; + } + } + + if (mime_type == null && buf != null) { + int max_len = 4096; + // XXX determine actual max needed buffer size using + // xdg_mime_get_max_buffer_extents? + uint8[] data = (max_len > buf.size) + ? buf.get_bytes()[0:max_len - 1].get_data() + : buf.get_uint8_array(); + + // XXX might just want to use xdgmime lib directly here to + // avoid the intermediate glib_content_type step here? + string glib_type = GLib.ContentType.guess(null, data, null); + mime_type = GLib.ContentType.get_mime_type(glib_type); + } + + if (Geary.String.is_empty(mime_type)) { + mime_type = DEFAULT_CONTENT_TYPE; + } + return deserialize(mime_type); + } + + /** * The type (discrete or concrete) portion of the Content-Type field. * @@ -69,19 +142,7 @@ public class Geary.Mime.ContentType : Geary.BaseObject { media_subtype = content_type.get_media_subtype().strip(); params = new ContentParameters.from_gmime(content_type.get_params()); } - - public static ContentType deserialize(string str) throws MimeError { - // perform a little sanity checking here, as it doesn't appear the GMime constructor has - // any error-reporting at all - if (String.is_empty(str)) - throw new MimeError.PARSE("Empty MIME Content-Type"); - - if (!str.contains("/")) - throw new MimeError.PARSE("Invalid MIME Content-Type: %s", str); - - return new ContentType.from_gmime(new GMime.ContentType.from_string(str)); - } - + /** * Compares the {@link media_type} with the supplied type. * @@ -115,7 +176,14 @@ public class Geary.Mime.ContentType : Geary.BaseObject { public string get_mime_type() { return "%s/%s".printf(media_type, media_subtype); } - + + /** + * Returns the file name extension for this type, if known. + */ + public string? get_file_name_extension() { + return TYPES_TO_EXTENSIONS[get_mime_type()]; + } + /** * Compares the supplied type and subtype with this instance's. * diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index be65cfcc..70a5e4c0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -6,6 +6,7 @@ set(TEST_SRC main.vala testcase.vala # Based on same file in libgee, courtesy Julien Peeters + engine/mime-content-type-test.vala engine/rfc822-mailbox-address-test.vala engine/rfc822-message-test.vala engine/rfc822-message-data-test.vala diff --git a/test/engine/mime-content-type-test.vala b/test/engine/mime-content-type-test.vala new file mode 100644 index 00000000..6e4c418a --- /dev/null +++ b/test/engine/mime-content-type-test.vala @@ -0,0 +1,56 @@ +/* + * Copyright 2016 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +class Geary.Mime.ContentTypeTest : Gee.TestCase { + + public ContentTypeTest() { + base("Geary.Mime.ContentTypeTest"); + add_test("get_file_name_extension", get_file_name_extension); + add_test("guess_type_from_name", guess_type_from_name); + add_test("guess_type_from_buf", guess_type_from_buf); + } + } + + public void get_file_name_extension() { + assert(new ContentType("image", "jpeg", null).get_file_name_extension() == ".jpeg"); + assert(new ContentType("test", "unknown", null).get_file_name_extension() == null); + } + + public void guess_type_from_name() { + try { + assert(ContentType.guess_type("test.png", null).is_type("image", "png")); + } catch (Error err) { + assert_not_reached(); + } + + try { + assert(ContentType.guess_type("foo.test", null).get_mime_type() == ContentType.DEFAULT_CONTENT_TYPE); + } catch (Error err) { + assert_not_reached(); + } + } + + public void guess_type_from_buf() { + Memory.ByteBuffer png = new Memory.ByteBuffer( + {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}, 8 // PNG magic + ); + Memory.ByteBuffer empty = new Memory.ByteBuffer({0x0}, 1); + + try { + assert(ContentType.guess_type(null, png).is_type("image", "png")); + } catch (Error err) { + assert_not_reached(); + } + + try { + assert(ContentType.guess_type(null, empty).get_mime_type() == ContentType.DEFAULT_CONTENT_TYPE); + } catch (Error err) { + assert_not_reached(); + } + } + +} diff --git a/test/main.vala b/test/main.vala index 8d4dff14..7472a1a6 100644 --- a/test/main.vala +++ b/test/main.vala @@ -41,6 +41,7 @@ int main(string[] args) { engine.add_suite(new Geary.IdleManagerTest().get_suite()); engine.add_suite(new Geary.Inet.Test().get_suite()); engine.add_suite(new Geary.JS.Test().get_suite()); + engine.add_suite(new Geary.Mime.ContentTypeTest().get_suite()); engine.add_suite(new Geary.RFC822.MailboxAddressTest().get_suite()); engine.add_suite(new Geary.RFC822.MessageTest().get_suite()); engine.add_suite(new Geary.RFC822.MessageDataTest().get_suite());