Skip to content

Commit

Permalink
Add readEntry, writeEntry, dropEncodingPart and needEscaping functions
Browse files Browse the repository at this point in the history
  • Loading branch information
FreeSlave committed Apr 13, 2016
1 parent be38cba commit aa11b57
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 61 deletions.
72 changes: 66 additions & 6 deletions source/inilike/common.d
Expand Up @@ -291,23 +291,45 @@ unittest
assert(parseLocaleName("ru_RU.UTF-8@mod".dup) == tuple("ru".dup, "RU".dup, "UTF-8".dup, "mod".dup));
}

/**
* Drop encoding part from locale (it's not used in constructing localized keys).
* Returns: Locale string with encoding part dropped out or original string if encoding was not present.
*/
@safe String dropEncodingPart(String)(String locale) pure nothrow if (is(String : const(char)[]))
{
auto t = parseLocaleName(locale);
if (!t.encoding.empty) {
return makeLocaleName(t.lang, t.country, String.init, t.modifier);
}
return locale;
}

///
unittest
{
assert("ru_RU.UTF-8".dropEncodingPart() == "ru_RU");
string locale = "ru_RU";
assert(locale.dropEncodingPart() is locale);
}

/**
* Construct localized key name from key and locale.
* Returns: localized key in form key[locale] dropping encoding out if present.
* See_Also: separateFromLocale
*/
@safe String localizedKey(String)(String key, String locale) pure nothrow if (is(String : const(char)[]))
{
auto t = parseLocaleName(locale);
if (!t.encoding.empty) {
locale = makeLocaleName(t.lang, t.country, String.init, t.modifier);
if (locale.empty) {
return key;
}
return key ~ "[".to!String ~ locale ~ "]".to!String;
return key ~ "[".to!String ~ locale.dropEncodingPart() ~ "]".to!String;
}

///
unittest
{
string key = "Name";
assert(localizedKey(key, "") == key);
assert(localizedKey("Name", "ru_RU") == "Name[ru_RU]");
assert(localizedKey("Name", "ru_RU.UTF-8") == "Name[ru_RU]");
}
Expand Down Expand Up @@ -418,6 +440,30 @@ unittest
assert(chooseLocalizedValue(locale, "ru", "Разработчик", "ru_RU", "Программист") == tuple("ru_RU", "Программист"));
}

/**
* Check if value needs to be escaped. This function is currently tolerant to single slashes.
* Returns: true if value needs to escaped, false otherwise.
*/
@nogc @safe bool needEscaping(string value) nothrow pure
{
for (size_t i=0; i<value.length; ++i) {
char c = value[i];
if (c == '\n' || c == '\t' || c == '\r') {
return true;
}
}
return false;
}

///
unittest
{
assert("new\nline".needEscaping);
assert(!`i have \ slash`.needEscaping);
assert("i\tlike\ttabs".needEscaping);
assert(!"just a text".needEscaping);
}

/**
* Escapes string by replacing special symbols with escaped sequences.
* These symbols are: '\\' (backslash), '\n' (newline), '\r' (carriage return) and '\t' (tab).
Expand All @@ -440,15 +486,27 @@ unittest


/**
* Unescape value.
* Unescape value. If value does not need unescaping this function returns original value.
* Params:
* value = string to unescape
* pairs = pairs of escaped characters and their unescaped forms.
*/
@trusted inout(char)[] doUnescape(inout(char)[] value, in Tuple!(char, char)[] pairs) nothrow pure {
//little optimization to avoid unneeded allocations.
size_t i = 0;
for (; i < value.length; i++) {
if (value[i] == '\\') {
break;
}
}
if (i == value.length) {
return value;
}

auto toReturn = appender!(typeof(value))();
toReturn.put(value[0..i]);

for (size_t i = 0; i < value.length; i++) {
for (; i < value.length; i++) {
if (value[i] == '\\') {
if (i+1 < value.length) {
const char c = value[i+1];
Expand Down Expand Up @@ -495,5 +553,7 @@ unittest
{
assert(`a\\next\nline\top`.unescapeValue() == "a\\next\nline\top"); // notice how the string on the left is raw.
assert(`\\next\nline\top`.unescapeValue() == "\\next\nline\top");
string value = `nounescape`;
assert(value.unescapeValue() is value); //original is returned.
assert(`a\\next\nline\top`.dup.unescapeValue() == "a\\next\nline\top".dup);
}
143 changes: 123 additions & 20 deletions source/inilike/file.d
Expand Up @@ -27,9 +27,9 @@ struct IniLikeLine
*/
enum Type
{
None = 0,
Comment = 1,
KeyValue = 2
None = 0, /// deleted or invalid line
Comment = 1, /// a comment or empty line
KeyValue = 2 /// key-value pair
}

/**
Expand Down Expand Up @@ -99,15 +99,16 @@ class IniLikeGroup
{
public:
/**
* Create instange on IniLikeGroup and set its name to groupName.
* Create instance on IniLikeGroup and set its name to groupName.
*/
protected @nogc @safe this(string groupName) nothrow {
_name = groupName;
}

/**
* Returns: The value associated with the key
* Note: It's an error to access nonexistent value
* Returns: The value associated with the key.
* Note: The value is not unescaped automatically.
* Warning: It's an error to access nonexistent value.
* See_Also: value
*/
@nogc @safe final string opIndex(string key) const nothrow {
Expand All @@ -117,13 +118,7 @@ public:
return _values[*i].value;
}

/**
* Insert new value or replaces the old one if value associated with key already exists.
* Returns: Inserted/updated value or null string if key was not added.
* Throws: $(B Exception) if key is not valid
*/
@safe final string opIndexAssign(string value, string key) {
validateKeyValue(key, value);
private @safe final string setKeyValueImpl(string key, string value) nothrow {
auto pick = key in _indices;
if (pick) {
return (_values[*pick] = IniLikeLine.fromKeyValue(key, value)).value;
Expand All @@ -133,8 +128,24 @@ public:
return value;
}
}

/**
* Ditto, localized version.
* Insert new value or replaces the old one if value associated with key already exists.
* Note: The value is not escaped automatically upon writing. It's your responsibility to escape it.
* Returns: Inserted/updated value or null string if key was not added.
* Throws: IniLikeEntryException if key or value is not valid.
* See_Also: writeEntry
*/
@safe final string opIndexAssign(string value, string key) {
validateKeyValue(key, value);
if (value.needEscaping()) {
throw new IniLikeEntryException("The value needs to be escaped", key, value);
}
return setKeyValueImpl(key, value);
}
/**
* Assign localized value.
* Note: The value is not escaped automatically upon writing. It's your responsibility to escape it.
* See_Also: setLocalizedValue, localizedValue
*/
@safe final string opIndexAssign(string value, string key, string locale) {
Expand All @@ -152,6 +163,8 @@ public:
/**
* Get value by key.
* Returns: The value associated with the key, or defaultValue if group does not contain such item.
* Note: The value is not unescaped automatically.
* See_Also: readEntry, localizedValue
*/
@nogc @safe final string value(string key, string defaultValue = null) const nothrow {
auto pick = key in _indices;
Expand All @@ -164,11 +177,36 @@ public:
return defaultValue;
}

/**
* Get value by key. This function automatically unescape the found value before returning.
* Returns: The unescaped value associated with key or null if not found.
* See_Also: value
*/
@safe final string readEntry(string key, string locale = null) const nothrow {
if (locale.length) {
return localizedValue(key, locale).unescapeValue();
} else {
return value(key).unescapeValue();
}
}

/**
* Set value by key. This function automatically escape the value (you should not escape value yourself) when wriging it.
* Throws: IniLikeEntryException if key or value is not valid.
*/
@safe final string writeEntry(string key, string value, string locale = null) {
validateKeyValue(key, value);
string keyName = localizedKey(key, locale);
return setKeyValueImpl(keyName, value.escapeValue());
}

/**
* Perform locale matching lookup as described in $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s04.html, Localized values for keys).
* Returns: The localized value associated with key and locale, or defaultValue if group does not contain item with this key.
* Returns: The localized value associated with key and locale, or the value associated with non-localized key if group does not contain localized value.
* Note: The value is not unescaped automatically.
* See_Also: value
*/
@safe final string localizedValue(string key, string locale, string defaultValue = null) const nothrow {
@safe final string localizedValue(string key, string locale) const nothrow {
//Any ideas how to get rid of this boilerplate and make less allocations?
const t = parseLocaleName(locale);
auto lang = t.lang;
Expand Down Expand Up @@ -201,7 +239,7 @@ public:
}
}

return value(key, defaultValue);
return value(key);
}

///
Expand Down Expand Up @@ -232,14 +270,16 @@ public:

/**
* Same as localized version of opIndexAssign, but uses function syntax.
* Note: The value is not escaped automatically upon writing. It's your responsibility to escape it.
* Throws: IniLikeEntryException if key or value is not valid.
* See_Also: writeEntry
*/
@safe final void setLocalizedValue(string key, string locale, string value) {
this[key, locale] = value;
}

/**
* Removes entry by key. To remove localized values use localizedKey.
* See_Also: inilike.common.localizedKey
* Removes entry by key.
*/
@safe final void removeEntry(string key) nothrow {
auto pick = key in _indices;
Expand All @@ -248,6 +288,11 @@ public:
}
}

///ditto, but remove entry by localized key
@safe final void removeEntry(string key, string locale) nothrow {
removeEntry(localizedKey(key, locale));
}

/**
* Remove all entries satisying ToDelete function.
* ToDelete should be function accepting string key and value and return boolean.
Expand Down Expand Up @@ -275,6 +320,7 @@ public:
}
}

///
unittest
{
string contents =
Expand Down Expand Up @@ -361,9 +407,12 @@ protected:
* Validate key and value before setting value to key for this group and throw exception if not valid.
* Can be reimplemented in derived classes.
* Default implementation check if key is not empty string, leaving value unchecked.
* Throws: IniLikeEntryException if either key or value is invalid.
*/
@trusted void validateKeyValue(string key, string value) const {
enforce(key.length > 0, "key must not be empty");
if (!key.length) {
throw new IniLikeEntryException("key must not be empty", key, value);
}
}

private:
Expand Down Expand Up @@ -417,6 +466,36 @@ private:
string _fileName;
}

/**
* Exception thrown when trying to set invalid key or value.
*/
class IniLikeEntryException : Exception
{
this(string msg, string key, string value, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe {
super(msg, file, line, next);
_key = key;
_value = value;
}

/**
* The key the value associated with.
*/
@nogc @safe string key() const nothrow pure {
return _key;
}

/**
* The value associated with key.
*/
@nogc @safe string value() const nothrow pure {
return _value;
}

private:
string _key;
string _value;
}

/**
* Ini-like file.
*
Expand Down Expand Up @@ -707,6 +786,8 @@ unittest
# Comment
GenericName=File manager
GenericName[ru]=Файловый менеджер
NeedUnescape=yes\\i\tneed
NeedUnescape[ru]=да\\я\tнуждаюсь
# Another comment
[Another Group]
Name=Commander
Expand Down Expand Up @@ -745,6 +826,17 @@ Comment=Manage files
assert(firstEntry.contains("GenericName[ru]"));
assert(firstEntry["GenericName"] == "File manager");
assert(firstEntry.value("GenericName") == "File manager");

assert(firstEntry.value("NeedUnescape") == `yes\\i\tneed`);
assert(firstEntry.readEntry("NeedUnescape") == "yes\\i\tneed");
assert(firstEntry.localizedValue("NeedUnescape", "ru") == `да\\я\tнуждаюсь`);
assert(firstEntry.readEntry("NeedUnescape", "ru") == "да\\я\tнуждаюсь");

firstEntry.writeEntry("NeedEscape", "i\rneed\nescape");
assert(firstEntry.value("NeedEscape") == `i\rneed\nescape`);
firstEntry.writeEntry("NeedEscape", "мне\rнужно\nэкранирование");
assert(firstEntry.localizedValue("NeedEscape", "ru") == `мне\rнужно\nэкранирование`);

firstEntry["GenericName"] = "Manager of files";
assert(firstEntry["GenericName"] == "Manager of files");
firstEntry["Authors"] = "Unknown";
Expand All @@ -758,6 +850,8 @@ Comment=Manage files

firstEntry.removeEntry("GenericName");
assert(!firstEntry.contains("GenericName"));
firstEntry.removeEntry("GenericName", "ru");
assert(!firstEntry.contains("GenericName[ru]"));
firstEntry["GenericName"] = "File Manager";
assert(firstEntry["GenericName"] == "File Manager");

Expand All @@ -782,6 +876,15 @@ Comment=Manage files
ilf.addGroup("Other Group");
assert(equal(ilf.byGroup().map!(g => g.name), ["First Entry", "Another Group", "Other Group"]));

auto entryException = collectException!IniLikeEntryException(ilf.group("Another Group")[""] = "Value");
assert(entryException !is null);
assert(entryException.key == "");
assert(entryException.value == "Value");
entryException = collectException!IniLikeEntryException(ilf.group("Another Group")["Key"] = "New\nline");
assert(entryException !is null);
assert(entryException.key == "Key");
assert(entryException.value == "New\nline");

const IniLikeFile cilf = ilf;
static assert(is(typeof(cilf.byGroup())));
static assert(is(typeof(cilf.group("First Entry").byKeyValue())));
Expand Down

0 comments on commit aa11b57

Please sign in to comment.