diff --git a/src/engine/imap/message/imap-data-format.vala b/src/engine/imap/message/imap-data-format.vala index 17a7ed54..64879af6 100644 --- a/src/engine/imap/message/imap-data-format.vala +++ b/src/engine/imap/message/imap-data-format.vala @@ -23,10 +23,10 @@ public enum Quoting { private bool is_special_char(char ch, char[] ar, string? exceptions) { if (ch > 0x7F || ch.iscntrl()) return true; - + if (ch in ar) - return (exceptions != null) ? exceptions.index_of_char(ch) < 0 : true; - + return (exceptions != null) ? Ascii.index_of(exceptions, ch) < 0 : true; + return false; } diff --git a/src/engine/imap/message/imap-fetch-body-data-specifier.vala b/src/engine/imap/message/imap-fetch-body-data-specifier.vala index b45ba897..b2dda653 100644 --- a/src/engine/imap/message/imap-fetch-body-data-specifier.vala +++ b/src/engine/imap/message/imap-fetch-body-data-specifier.vala @@ -73,15 +73,15 @@ public class Geary.Imap.FetchBodyDataSpecifier : BaseObject, Gee.Hashable 0) { - this.field_names = new Gee.TreeSet((s1, s2) => { - return GLib.strcmp(s1, s2); - }); + this.field_names = new Gee.TreeSet(Ascii.strcmp); foreach (string field_name in field_names) { - string converted = field_name.strip().down(); - + string converted = Ascii.strdown(field_name.strip()); + if (!String.is_empty(converted)) this.field_names.add(converted); } } else { this.field_names = null; } - + // see equal_to() for why the response version is used hashable = serialize_response(); } - + /** * Returns the {@link FetchBodyDataSpecifier} in a string ready for a {@link Command}. * diff --git a/src/engine/imap/message/imap-internal-date.vala b/src/engine/imap/message/imap-internal-date.vala index 3240ba0b..50317e21 100644 --- a/src/engine/imap/message/imap-internal-date.vala +++ b/src/engine/imap/message/imap-internal-date.vala @@ -64,18 +64,18 @@ public class Geary.Imap.InternalDate : Geary.MessageData.AbstractMessageData, Ge || year < 1970) { throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE \"%s\": bad numerical range", internaldate); } - + // check month (this catches localization problems) int month = -1; - string mon_down = ((string) mon).down(); + string mon_down = Ascii.strdown(((string) mon)); for (int ctr = 0; ctr < EN_US_MON_DOWN.length; ctr++) { if (mon_down == EN_US_MON_DOWN[ctr]) { month = ctr; - + break; } } - + if (month < 0) throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE \"%s\": bad month", internaldate); diff --git a/src/engine/imap/message/imap-mailbox-specifier.vala b/src/engine/imap/message/imap-mailbox-specifier.vala index ce34d3dd..cf9aea37 100644 --- a/src/engine/imap/message/imap-mailbox-specifier.vala +++ b/src/engine/imap/message/imap-mailbox-specifier.vala @@ -77,7 +77,7 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable 0) { this.mailbox = address[0:atsign]; this.domain = address[atsign + 1:address.length]; @@ -173,12 +173,12 @@ public class Geary.RFC822.MailboxAddress : } string address = mailbox.get_addr(); - int atsign = address.last_index_of_char('@'); + int atsign = Ascii.last_index_of(address, '@'); if (atsign == -1) { // No @ detected, try decoding in case a mailer (wrongly) // encoded the whole thing and re-try address = decode_address_part(address); - atsign = address.last_index_of_char('@'); + atsign = Ascii.last_index_of(address, '@'); } if (atsign >= 0) { diff --git a/src/engine/smtp/smtp-command.vala b/src/engine/smtp/smtp-command.vala index 2a091d24..47db7648 100644 --- a/src/engine/smtp/smtp-command.vala +++ b/src/engine/smtp/smtp-command.vala @@ -56,36 +56,36 @@ public enum Geary.Smtp.Command { assert_not_reached(); } } - + public static Command deserialize(string str) throws SmtpError { - switch (str.down()) { + switch (Ascii.strdown(str)) { case "helo": return HELO; - + case "ehlo": return EHLO; - + case "quit": return QUIT; - + case "help": return HELP; - + case "noop": return NOOP; - + case "rset": return RSET; - + case "auth": return AUTH; - + case "mail": return MAIL; - + case "rcpt": return RCPT; - + case "data": return DATA; diff --git a/src/engine/smtp/smtp-greeting.vala b/src/engine/smtp/smtp-greeting.vala index 3eb91154..676ef250 100644 --- a/src/engine/smtp/smtp-greeting.vala +++ b/src/engine/smtp/smtp-greeting.vala @@ -25,21 +25,21 @@ public class Geary.Smtp.Greeting : Response { return ""; } } - + public static ServerFlavor deserialize(string str) { - switch (str.up()) { + switch (Ascii.strup(str)) { case "SMTP": return SMTP; - + case "ESMTP": return ESMTP; - + default: return UNSPECIFIED; } } } - + public string? domain { get; private set; default = null; } public ServerFlavor flavor { get; private set; default = ServerFlavor.UNSPECIFIED; } public string? message { get; private set; default = null; } diff --git a/src/engine/util/util-ascii.vala b/src/engine/util/util-ascii.vala index 20fecc44..4c803fed 100644 --- a/src/engine/util/util-ascii.vala +++ b/src/engine/util/util-ascii.vala @@ -4,40 +4,80 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ +/** + * US-ASCII string utilities. + * + * Using ASCII-specific, non-localised functions is essential when + * dealing with protocol strings since any case-insensitive + * comparisons may be incorrect under certain locales — especially for + * Turkish, where translating between upper-case and lower-case `i` is + * not necessarily preserved. + */ namespace Geary.Ascii { +public int index_of(string str, char ch) { + // Use a pointer and explicit null check, since testing against + // the length of the string as in a traditional for loop will mean + // a call to strlen(), making the loop O(n^2) + int ret = -1; + char *strptr = str; + int i = 0; + while (*strptr != String.EOS) { + if (*strptr++ == ch) { + ret = i; + break; + } + i++; + } + return ret; +} + +public int last_index_of(string str, char ch) { + // Use a pointer and explicit null check, since testing against + // the length of the string as in a traditional for loop will mean + // a call to strlen(), making the loop O(n^2) + int ret = -1; + char *strptr = str; + int i = 0; + while (*strptr != String.EOS) { + if (*strptr++ == ch) { + ret = i; + } + i++; + } + return ret; +} + public bool get_next_char(string str, ref int index, out char ch) { ch = str[index++]; - + return ch != String.EOS; } -public bool stri_equal(string a, string b) { - // XXX Is this marginally faster than a.down() == b.down() in the - // best case, slower in the worse case, so not worth it? - char *aptr = a; - char *bptr = b; - for (;;) { - int diff = (int) (*aptr).tolower() - (int) (*bptr).tolower(); - if (diff != 0) - return false; +public inline int strcmp(string a, string b) { + return GLib.strcmp(a, b); +} - if (*aptr == String.EOS) - return true; +public inline int stricmp(string a, string b) { + return a.ascii_casecmp(b); +} - aptr++; - bptr++; - } +public inline bool str_equal(string a, string b) { + return a == b; +} + +public inline bool stri_equal(string a, string b) { + return a.ascii_casecmp(b) == 0; } public bool nullable_stri_equal(string? a, string? b) { if (a == null) return (b == null); - + // a != null, so always false if (b == null) return false; - + return stri_equal(a, b); } @@ -55,6 +95,14 @@ public uint nullable_stri_hash(string? str) { return (str != null) ? stri_hash(str) : 0; } +public inline string strdown(string str) { + return str.ascii_down(); +} + +public inline string strup(string str) { + return str.ascii_up(); +} + /** * Returns true if the ASCII string contains only whitespace and at least one numeric character. */ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f1ad286e..381c87c7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -36,6 +36,7 @@ set(TEST_ENGINE_SRC engine/rfc822-message-test.vala engine/rfc822-message-data-test.vala engine/rfc822-utils-test.vala + engine/util-ascii-test.vala engine/util-html-test.vala engine/util-idle-manager-test.vala engine/util-inet-test.vala diff --git a/test/engine/util-ascii-test.vala b/test/engine/util-ascii-test.vala new file mode 100644 index 00000000..483e030a --- /dev/null +++ b/test/engine/util-ascii-test.vala @@ -0,0 +1,44 @@ +/* + * Copyright 2018 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.Ascii.Test : TestCase { + + public Test() { + base("Geary.Ascii.Test"); + add_test("index_of", index_of); + add_test("last_index_of", last_index_of); + } + + public void index_of() throws Error { + assert_int(-1, Ascii.index_of("", 'a')); + assert_int(0, Ascii.index_of("a", 'a')); + assert_int(0, Ascii.index_of("aa", 'a')); + + assert_int(0, Ascii.index_of("abcabc", 'a')); + assert_int(1, Ascii.index_of("abcabc", 'b')); + assert_int(2, Ascii.index_of("abcabc", 'c')); + + assert_int(0, Ascii.index_of("@", '@')); + + assert_int(-1, Ascii.index_of("abc", 'd')); + } + + public void last_index_of() throws Error { + assert_int(-1, Ascii.last_index_of("", 'a')); + assert_int(0, Ascii.last_index_of("a", 'a')); + assert_int(1, Ascii.last_index_of("aa", 'a')); + + assert_int(3, Ascii.last_index_of("abcabc", 'a')); + assert_int(4, Ascii.last_index_of("abcabc", 'b')); + assert_int(5, Ascii.last_index_of("abcabc", 'c')); + + assert_int(0, Ascii.last_index_of("@", '@')); + + assert_int(-1, Ascii.last_index_of("abc", 'd')); + } + +} diff --git a/test/meson.build b/test/meson.build index b844d0de..e74841c4 100644 --- a/test/meson.build +++ b/test/meson.build @@ -32,6 +32,7 @@ geary_test_engine_sources = [ 'engine/rfc822-message-test.vala', 'engine/rfc822-message-data-test.vala', 'engine/rfc822-utils-test.vala', + 'engine/util-ascii-test.vala', 'engine/util-html-test.vala', 'engine/util-idle-manager-test.vala', 'engine/util-inet-test.vala', diff --git a/test/test-engine.vala b/test/test-engine.vala index ab4a0efa..06349aef 100644 --- a/test/test-engine.vala +++ b/test/test-engine.vala @@ -30,6 +30,7 @@ int main(string[] args) { engine.add_suite(new Geary.App.ConversationSetTest().get_suite()); // Depends on ConversationTest and ConversationSetTest passing engine.add_suite(new Geary.App.ConversationMonitorTest().get_suite()); + engine.add_suite(new Geary.Ascii.Test().get_suite()); engine.add_suite(new Geary.HTML.UtilTest().get_suite()); engine.add_suite(new Geary.Imap.DeserializerTest().get_suite()); engine.add_suite(new Geary.Imap.CreateCommandTest().get_suite());