Merge branch 'wip/795906-turkish-locale'. Fixes Bug 795906.

This commit is contained in:
Michael James Gratton 2018-05-17 15:43:16 +10:00
commit ffb4befdd8
17 changed files with 190 additions and 97 deletions

View file

@ -23,10 +23,10 @@ public enum Quoting {
private bool is_special_char(char ch, char[] ar, string? exceptions) { private bool is_special_char(char ch, char[] ar, string? exceptions) {
if (ch > 0x7F || ch.iscntrl()) if (ch > 0x7F || ch.iscntrl())
return true; return true;
if (ch in ar) 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; return false;
} }

View file

@ -73,15 +73,15 @@ public class Geary.Imap.FetchBodyDataSpecifier : BaseObject, Gee.Hashable<FetchB
assert_not_reached(); assert_not_reached();
} }
} }
public static SectionPart deserialize(string value) throws ImapError { public static SectionPart deserialize(string value) throws ImapError {
if (String.is_empty(value)) if (String.is_empty(value))
return NONE; return NONE;
switch (value.down()) { switch (Ascii.strdown(value)) {
case "header": case "header":
return HEADER; return HEADER;
case "header.fields": case "header.fields":
return HEADER_FIELDS; return HEADER_FIELDS;
@ -178,25 +178,23 @@ public class Geary.Imap.FetchBodyDataSpecifier : BaseObject, Gee.Hashable<FetchB
this.subset_start = subset_start; this.subset_start = subset_start;
this.subset_count = subset_count; this.subset_count = subset_count;
this.is_peek = is_peek; this.is_peek = is_peek;
if (field_names != null && field_names.length > 0) { if (field_names != null && field_names.length > 0) {
this.field_names = new Gee.TreeSet<string>((s1, s2) => { this.field_names = new Gee.TreeSet<string>(Ascii.strcmp);
return GLib.strcmp(s1, s2);
});
foreach (string field_name in field_names) { 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)) if (!String.is_empty(converted))
this.field_names.add(converted); this.field_names.add(converted);
} }
} else { } else {
this.field_names = null; this.field_names = null;
} }
// see equal_to() for why the response version is used // see equal_to() for why the response version is used
hashable = serialize_response(); hashable = serialize_response();
} }
/** /**
* Returns the {@link FetchBodyDataSpecifier} in a string ready for a {@link Command}. * Returns the {@link FetchBodyDataSpecifier} in a string ready for a {@link Command}.
* *

View file

@ -64,18 +64,18 @@ public class Geary.Imap.InternalDate : Geary.MessageData.AbstractMessageData, Ge
|| year < 1970) { || year < 1970) {
throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE \"%s\": bad numerical range", internaldate); throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE \"%s\": bad numerical range", internaldate);
} }
// check month (this catches localization problems) // check month (this catches localization problems)
int month = -1; 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++) { for (int ctr = 0; ctr < EN_US_MON_DOWN.length; ctr++) {
if (mon_down == EN_US_MON_DOWN[ctr]) { if (mon_down == EN_US_MON_DOWN[ctr]) {
month = ctr; month = ctr;
break; break;
} }
} }
if (month < 0) if (month < 0)
throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE \"%s\": bad month", internaldate); throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE \"%s\": bad month", internaldate);

View file

@ -77,7 +77,7 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
public static bool is_inbox_name(string name) { public static bool is_inbox_name(string name) {
return Ascii.stri_equal(name, CANONICAL_INBOX_NAME); return Ascii.stri_equal(name, CANONICAL_INBOX_NAME);
} }
/** /**
* Returns true if the string is the ''canonical'' name of the IMAP Inbox. * Returns true if the string is the ''canonical'' name of the IMAP Inbox.
* *
@ -90,9 +90,9 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
* @see is_inbox_name * @see is_inbox_name
*/ */
public static bool is_canonical_inbox_name(string name) { public static bool is_canonical_inbox_name(string name) {
return (name == CANONICAL_INBOX_NAME); return Ascii.str_equal(name, CANONICAL_INBOX_NAME);
} }
/** /**
* Converts a generic {@link FolderPath} into an IMAP mailbox specifier. * Converts a generic {@link FolderPath} into an IMAP mailbox specifier.
*/ */
@ -195,27 +195,27 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
public uint hash() { public uint hash() {
return is_inbox ? Ascii.stri_hash(name) : Ascii.str_hash(name); return is_inbox ? Ascii.stri_hash(name) : Ascii.str_hash(name);
} }
public bool equal_to(MailboxSpecifier other) { public bool equal_to(MailboxSpecifier other) {
if (this == other) if (this == other)
return true; return true;
if (is_inbox) if (is_inbox)
return Ascii.stri_equal(name, other.name); return Ascii.stri_equal(name, other.name);
return (name == other.name); return Ascii.str_equal(name, other.name);
} }
public int compare_to(MailboxSpecifier other) { public int compare_to(MailboxSpecifier other) {
if (this == other) if (this == other)
return 0; return 0;
if (is_inbox && other.is_inbox) if (is_inbox && other.is_inbox)
return 0; return 0;
return GLib.strcmp(name, other.name); return Ascii.strcmp(name, other.name);
} }
public string to_string() { public string to_string() {
return name; return name;
} }

View file

@ -134,35 +134,35 @@ public abstract class Geary.Imap.StringParameter : Geary.Imap.Parameter {
public bool is_empty() { public bool is_empty() {
return String.is_empty(ascii); return String.is_empty(ascii);
} }
/** /**
* Case-sensitive comparison. * Case-sensitive comparison.
*/ */
public bool equals_cs(string value) { public bool equals_cs(string value) {
return (ascii == value); return Ascii.str_equal(ascii, value);
} }
/** /**
* Case-insensitive comparison. * Case-insensitive comparison.
*/ */
public bool equals_ci(string value) { public bool equals_ci(string value) {
return Ascii.stri_equal(ascii, value); return Ascii.stri_equal(ascii, value);
} }
/** /**
* Returns the string lowercased. * Returns the string lowercased.
*/ */
public string as_lower() { public string as_lower() {
return ascii.down(); return Ascii.strdown(ascii);
} }
/** /**
* Returns the string uppercased. * Returns the string uppercased.
*/ */
public string as_upper() { public string as_upper() {
return ascii.up(); return Ascii.strup(ascii);
} }
/** /**
* Converts the {@link ascii} to a signed 32-bit integer, clamped between clamp_min and * Converts the {@link ascii} to a signed 32-bit integer, clamped between clamp_min and
* clamp_max. * clamp_max.

View file

@ -64,17 +64,17 @@ public class Geary.Imap.ResponseCodeType : BaseObject, Gee.Hashable<ResponseCode
public ResponseCodeType.from_parameter(StringParameter stringp) throws ImapError { public ResponseCodeType.from_parameter(StringParameter stringp) throws ImapError {
init(stringp.ascii); init(stringp.ascii);
} }
private void init(string ascii) throws ImapError { private void init(string ascii) throws ImapError {
// note that is_quoting_required() also catches empty strings (as they require quoting) // note that is_quoting_required() also catches empty strings (as they require quoting)
if (DataFormat.is_quoting_required(ascii) != DataFormat.Quoting.OPTIONAL) if (DataFormat.is_quoting_required(ascii) != DataFormat.Quoting.OPTIONAL)
throw new ImapError.INVALID("\"%s\" cannot be represented as a ResponseCodeType", ascii); throw new ImapError.INVALID("\"%s\" cannot be represented as a ResponseCodeType", ascii);
// store lowercased so it's easily compared with const strings above // store lowercased so it's easily compared with const strings above
original = ascii; original = ascii;
value = ascii.down(); value = Ascii.strdown(ascii);
} }
public bool is_value(string str) { public bool is_value(string str) {
return Ascii.stri_equal(value, str); return Ascii.stri_equal(value, str);
} }

View file

@ -78,7 +78,7 @@ public class Geary.Mime.ContentParameters : BaseObject {
return (stored != null) ? Ascii.stri_equal(stored, value) : false; return (stored != null) ? Ascii.stri_equal(stored, value) : false;
} }
/** /**
* Returns true if the attribute has the supplied value (case-sensitive comparison). * Returns true if the attribute has the supplied value (case-sensitive comparison).
* *
@ -86,10 +86,10 @@ public class Geary.Mime.ContentParameters : BaseObject {
*/ */
public bool has_value_cs(string attribute, string value) { public bool has_value_cs(string attribute, string value) {
string? stored = params.get(attribute); string? stored = params.get(attribute);
return (stored != null) ? (stored == value) : false; return (stored != null) ? Ascii.str_equal(stored, value) : false;
} }
/** /**
* Add or replace the parameter. * Add or replace the parameter.
* *

View file

@ -31,24 +31,24 @@ public enum Geary.Mime.DispositionType {
*/ */
public static DispositionType deserialize(string? str, out bool is_unknown) { public static DispositionType deserialize(string? str, out bool is_unknown) {
is_unknown = false; is_unknown = false;
if (String.is_empty_or_whitespace(str)) if (String.is_empty_or_whitespace(str))
return UNSPECIFIED; return UNSPECIFIED;
switch (str.down()) { switch (Ascii.strdown(str)) {
case "inline": case "inline":
return INLINE; return INLINE;
case "attachment": case "attachment":
return ATTACHMENT; return ATTACHMENT;
default: default:
is_unknown = true; is_unknown = true;
return ATTACHMENT; return ATTACHMENT;
} }
} }
/** /**
* Returns null if value is {@link UNSPECIFIED} * Returns null if value is {@link UNSPECIFIED}
*/ */

View file

@ -48,24 +48,24 @@ public enum Geary.Mime.MultipartSubtype {
public static MultipartSubtype from_content_type(ContentType? content_type, out bool is_unknown) { public static MultipartSubtype from_content_type(ContentType? content_type, out bool is_unknown) {
if (content_type == null || !content_type.has_media_type("multipart")) { if (content_type == null || !content_type.has_media_type("multipart")) {
is_unknown = true; is_unknown = true;
return MIXED; return MIXED;
} }
is_unknown = false; is_unknown = false;
switch (content_type.media_subtype.down()) { switch (Ascii.strdown(content_type.media_subtype)) {
case "mixed": case "mixed":
return MIXED; return MIXED;
case "alternative": case "alternative":
return ALTERNATIVE; return ALTERNATIVE;
case "related": case "related":
return RELATED; return RELATED;
default: default:
is_unknown = true; is_unknown = true;
return MIXED; return MIXED;
} }
} }

View file

@ -127,7 +127,7 @@ public class Geary.RFC822.MailboxAddress :
this.source_route = null; this.source_route = null;
this.address = address; this.address = address;
int atsign = address.last_index_of_char('@'); int atsign = Ascii.last_index_of(address, '@');
if (atsign > 0) { if (atsign > 0) {
this.mailbox = address[0:atsign]; this.mailbox = address[0:atsign];
this.domain = address[atsign + 1:address.length]; this.domain = address[atsign + 1:address.length];
@ -173,12 +173,12 @@ public class Geary.RFC822.MailboxAddress :
} }
string address = mailbox.get_addr(); string address = mailbox.get_addr();
int atsign = address.last_index_of_char('@'); int atsign = Ascii.last_index_of(address, '@');
if (atsign == -1) { if (atsign == -1) {
// No @ detected, try decoding in case a mailer (wrongly) // No @ detected, try decoding in case a mailer (wrongly)
// encoded the whole thing and re-try // encoded the whole thing and re-try
address = decode_address_part(address); address = decode_address_part(address);
atsign = address.last_index_of_char('@'); atsign = Ascii.last_index_of(address, '@');
} }
if (atsign >= 0) { if (atsign >= 0) {

View file

@ -56,36 +56,36 @@ public enum Geary.Smtp.Command {
assert_not_reached(); assert_not_reached();
} }
} }
public static Command deserialize(string str) throws SmtpError { public static Command deserialize(string str) throws SmtpError {
switch (str.down()) { switch (Ascii.strdown(str)) {
case "helo": case "helo":
return HELO; return HELO;
case "ehlo": case "ehlo":
return EHLO; return EHLO;
case "quit": case "quit":
return QUIT; return QUIT;
case "help": case "help":
return HELP; return HELP;
case "noop": case "noop":
return NOOP; return NOOP;
case "rset": case "rset":
return RSET; return RSET;
case "auth": case "auth":
return AUTH; return AUTH;
case "mail": case "mail":
return MAIL; return MAIL;
case "rcpt": case "rcpt":
return RCPT; return RCPT;
case "data": case "data":
return DATA; return DATA;

View file

@ -25,21 +25,21 @@ public class Geary.Smtp.Greeting : Response {
return ""; return "";
} }
} }
public static ServerFlavor deserialize(string str) { public static ServerFlavor deserialize(string str) {
switch (str.up()) { switch (Ascii.strup(str)) {
case "SMTP": case "SMTP":
return SMTP; return SMTP;
case "ESMTP": case "ESMTP":
return ESMTP; return ESMTP;
default: default:
return UNSPECIFIED; return UNSPECIFIED;
} }
} }
} }
public string? domain { get; private set; default = null; } public string? domain { get; private set; default = null; }
public ServerFlavor flavor { get; private set; default = ServerFlavor.UNSPECIFIED; } public ServerFlavor flavor { get; private set; default = ServerFlavor.UNSPECIFIED; }
public string? message { get; private set; default = null; } public string? message { get; private set; default = null; }

View file

@ -4,40 +4,80 @@
* (version 2.1 or later). See the COPYING file in this distribution. * (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 { 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) { public bool get_next_char(string str, ref int index, out char ch) {
ch = str[index++]; ch = str[index++];
return ch != String.EOS; return ch != String.EOS;
} }
public bool stri_equal(string a, string b) { public inline int strcmp(string a, string b) {
// XXX Is this marginally faster than a.down() == b.down() in the return GLib.strcmp(a, b);
// 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;
if (*aptr == String.EOS) public inline int stricmp(string a, string b) {
return true; return a.ascii_casecmp(b);
}
aptr++; public inline bool str_equal(string a, string b) {
bptr++; 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) { public bool nullable_stri_equal(string? a, string? b) {
if (a == null) if (a == null)
return (b == null); return (b == null);
// a != null, so always false // a != null, so always false
if (b == null) if (b == null)
return false; return false;
return stri_equal(a, b); return stri_equal(a, b);
} }
@ -55,6 +95,14 @@ public uint nullable_stri_hash(string? str) {
return (str != null) ? stri_hash(str) : 0; 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. * Returns true if the ASCII string contains only whitespace and at least one numeric character.
*/ */

View file

@ -36,6 +36,7 @@ set(TEST_ENGINE_SRC
engine/rfc822-message-test.vala engine/rfc822-message-test.vala
engine/rfc822-message-data-test.vala engine/rfc822-message-data-test.vala
engine/rfc822-utils-test.vala engine/rfc822-utils-test.vala
engine/util-ascii-test.vala
engine/util-html-test.vala engine/util-html-test.vala
engine/util-idle-manager-test.vala engine/util-idle-manager-test.vala
engine/util-inet-test.vala engine/util-inet-test.vala

View file

@ -0,0 +1,44 @@
/*
* Copyright 2018 Michael Gratton <mike@vee.net>
*
* 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'));
}
}

View file

@ -32,6 +32,7 @@ geary_test_engine_sources = [
'engine/rfc822-message-test.vala', 'engine/rfc822-message-test.vala',
'engine/rfc822-message-data-test.vala', 'engine/rfc822-message-data-test.vala',
'engine/rfc822-utils-test.vala', 'engine/rfc822-utils-test.vala',
'engine/util-ascii-test.vala',
'engine/util-html-test.vala', 'engine/util-html-test.vala',
'engine/util-idle-manager-test.vala', 'engine/util-idle-manager-test.vala',
'engine/util-inet-test.vala', 'engine/util-inet-test.vala',

View file

@ -30,6 +30,7 @@ int main(string[] args) {
engine.add_suite(new Geary.App.ConversationSetTest().get_suite()); engine.add_suite(new Geary.App.ConversationSetTest().get_suite());
// Depends on ConversationTest and ConversationSetTest passing // Depends on ConversationTest and ConversationSetTest passing
engine.add_suite(new Geary.App.ConversationMonitorTest().get_suite()); 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.HTML.UtilTest().get_suite());
engine.add_suite(new Geary.Imap.DeserializerTest().get_suite()); engine.add_suite(new Geary.Imap.DeserializerTest().get_suite());
engine.add_suite(new Geary.Imap.CreateCommandTest().get_suite()); engine.add_suite(new Geary.Imap.CreateCommandTest().get_suite());