/*
* Copyright (c) 2008-2024 Jonathan Schleifer <js@nil.im>
*
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version 3.0 only,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* version 3.0 for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* version 3.0 along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#import "OFINICategory.h"
#import "OFINICategory+Private.h"
#import "OFArray.h"
#import "OFString.h"
#import "OFStream.h"
#import "OFInvalidArgumentException.h"
#import "OFInvalidFormatException.h"
@interface OFINICategoryPair: OFObject
{
@public
OFString *_key, *_value;
}
@end
@interface OFINICategoryComment: OFObject
{
@public
OFString *_comment;
}
@end
static OFString *
escapeString(OFString *string)
{
OFMutableString *mutableString;
/* FIXME: Optimize */
if (![string hasPrefix: @" "] && ![string hasPrefix: @"\t"] &&
![string hasPrefix: @"\f"] && ![string hasSuffix: @" "] &&
![string hasSuffix: @"\t"] && ![string hasSuffix: @"\f"] &&
![string containsString: @"\""])
return string;
mutableString = [[string mutableCopy] autorelease];
[mutableString replaceOccurrencesOfString: @"\\" withString: @"\\\\"];
[mutableString replaceOccurrencesOfString: @"\f" withString: @"\\f"];
[mutableString replaceOccurrencesOfString: @"\r" withString: @"\\r"];
[mutableString replaceOccurrencesOfString: @"\n" withString: @"\\n"];
[mutableString replaceOccurrencesOfString: @"\"" withString: @"\\\""];
[mutableString insertString: @"\"" atIndex: 0];
[mutableString appendString: @"\""];
[mutableString makeImmutable];
return mutableString;
}
static OFString *
unescapeString(OFString *string)
{
OFMutableString *mutableString;
if (![string hasPrefix: @"\""] || ![string hasSuffix: @"\""])
return string;
string = [string substringWithRange: OFMakeRange(1, string.length - 2)];
mutableString = [[string mutableCopy] autorelease];
[mutableString replaceOccurrencesOfString: @"\\f" withString: @"\f"];
[mutableString replaceOccurrencesOfString: @"\\r" withString: @"\r"];
[mutableString replaceOccurrencesOfString: @"\\n" withString: @"\n"];
[mutableString replaceOccurrencesOfString: @"\\\"" withString: @"\""];
[mutableString replaceOccurrencesOfString: @"\\\\" withString: @"\\"];
[mutableString makeImmutable];
return mutableString;
}
@implementation OFINICategoryPair
- (void)dealloc
{
[_key release];
[_value release];
[super dealloc];
}
- (OFString *)description
{
return [OFString stringWithFormat: @"%@ = %@", _key, _value];
}
@end
@implementation OFINICategoryComment
- (void)dealloc
{
[_comment release];
[super dealloc];
}
- (OFString *)description
{
return [[_comment copy] autorelease];
}
@end
@implementation OFINICategory
@synthesize name = _name;
- (instancetype)of_initWithName: (OFString *)name OF_DIRECT
{
self = [super init];
@try {
_name = [name copy];
_lines = [[OFMutableArray alloc] init];
} @catch (id e) {
[self release];
@throw e;
}
return self;
}
- (instancetype)init
{
OF_INVALID_INIT_METHOD
}
- (void)dealloc
{
[_name release];
[_lines release];
[super dealloc];
}
- (void)of_parseLine: (OFString *)line
{
if (![line hasPrefix: @";"] && ![line hasPrefix: @"#"]) {
OFINICategoryPair *pair;
OFString *key, *value;
size_t pos;
pair = [[[OFINICategoryPair alloc] init] autorelease];
if ((pos = [line rangeOfString: @"="].location) == OFNotFound)
@throw [OFInvalidFormatException exception];
key = unescapeString([line substringToIndex: pos]
.stringByDeletingEnclosingWhitespaces);
value = unescapeString([line substringFromIndex: pos + 1]
.stringByDeletingEnclosingWhitespaces);
pair->_key = [key copy];
pair->_value = [value copy];
[_lines addObject: pair];
} else {
OFINICategoryComment *comment =
[[[OFINICategoryComment alloc] init] autorelease];
comment->_comment = [line copy];
[_lines addObject: comment];
}
}
- (OFString *)stringValueForKey: (OFString *)key
{
return [self stringValueForKey: key defaultValue: nil];
}
- (OFString *)stringValueForKey: (OFString *)key
defaultValue: (OFString *)defaultValue
{
for (id line in _lines) {
OFINICategoryPair *pair;
if (![line isKindOfClass: [OFINICategoryPair class]])
continue;
pair = line;
if ([pair->_key isEqual: key])
return [[pair->_value copy] autorelease];
}
return defaultValue;
}
- (long long)longLongValueForKey: (OFString *)key
defaultValue: (long long)defaultValue
{
void *pool = objc_autoreleasePoolPush();
OFString *value = [self stringValueForKey: key defaultValue: nil];
long long ret;
if (value != nil)
ret = [value longLongValueWithBase: 0];
else
ret = defaultValue;
objc_autoreleasePoolPop(pool);
return ret;
}
- (bool)boolValueForKey: (OFString *)key defaultValue: (bool)defaultValue
{
void *pool = objc_autoreleasePoolPush();
OFString *value = [self stringValueForKey: key defaultValue: nil];
bool ret;
if (value != nil) {
if ([value isEqual: @"true"])
ret = true;
else if ([value isEqual: @"false"])
ret = false;
else
@throw [OFInvalidFormatException exception];
} else
ret = defaultValue;
objc_autoreleasePoolPop(pool);
return ret;
}
- (float)floatValueForKey: (OFString *)key defaultValue: (float)defaultValue
{
void *pool = objc_autoreleasePoolPush();
OFString *value = [self stringValueForKey: key defaultValue: nil];
float ret;
if (value != nil)
ret = value.floatValue;
else
ret = defaultValue;
objc_autoreleasePoolPop(pool);
return ret;
}
- (double)doubleValueForKey: (OFString *)key defaultValue: (double)defaultValue
{
void *pool = objc_autoreleasePoolPush();
OFString *value = [self stringValueForKey: key defaultValue: nil];
double ret;
if (value != nil)
ret = value.doubleValue;
else
ret = defaultValue;
objc_autoreleasePoolPop(pool);
return ret;
}
- (OFArray OF_GENERIC(OFString *) *)arrayValueForKey: (OFString *)key
{
OFMutableArray *ret = [OFMutableArray array];
void *pool = objc_autoreleasePoolPush();
for (id line in _lines) {
OFINICategoryPair *pair;
if (![line isKindOfClass: [OFINICategoryPair class]])
continue;
pair = line;
if ([pair->_key isEqual: key])
[ret addObject: [[pair->_value copy] autorelease]];
}
objc_autoreleasePoolPop(pool);
[ret makeImmutable];
return ret;
}
- (void)setStringValue: (OFString *)string forKey: (OFString *)key
{
void *pool = objc_autoreleasePoolPush();
OFINICategoryPair *pair;
for (id line in _lines) {
if (![line isKindOfClass: [OFINICategoryPair class]])
continue;
pair = line;
if ([pair->_key isEqual: key]) {
OFString *old = pair->_value;
pair->_value = [string copy];
[old release];
objc_autoreleasePoolPop(pool);
return;
}
}
pair = [[[OFINICategoryPair alloc] init] autorelease];
pair->_key = nil;
pair->_value = nil;
@try {
pair->_key = [key copy];
pair->_value = [string copy];
[_lines addObject: pair];
} @catch (id e) {
[pair->_key release];
[pair->_value release];
@throw e;
}
objc_autoreleasePoolPop(pool);
}
- (void)setLongLongValue: (long long)longLongValue forKey: (OFString *)key
{
void *pool = objc_autoreleasePoolPush();
[self setStringValue: [OFString stringWithFormat:
@"%lld", longLongValue]
forKey: key];
objc_autoreleasePoolPop(pool);
}
- (void)setBoolValue: (bool)boolValue forKey: (OFString *)key
{
[self setStringValue: (boolValue ? @"true" : @"false") forKey: key];
}
- (void)setFloatValue: (float)floatValue forKey: (OFString *)key
{
void *pool = objc_autoreleasePoolPush();
[self setStringValue: [OFString stringWithFormat: @"%g", floatValue]
forKey: key];
objc_autoreleasePoolPop(pool);
}
- (void)setDoubleValue: (double)doubleValue forKey: (OFString *)key
{
void *pool = objc_autoreleasePoolPush();
[self setStringValue: [OFString stringWithFormat: @"%g", doubleValue]
forKey: key];
objc_autoreleasePoolPop(pool);
}
- (void)setArrayValue: (OFArray OF_GENERIC(OFString *) *)arrayValue
forKey: (OFString *)key
{
void *pool;
OFMutableArray *pairs;
id const *lines;
size_t count;
bool replaced;
if (arrayValue.count == 0) {
[self removeValueForKey: key];
return;
}
pool = objc_autoreleasePoolPush();
pairs = [OFMutableArray arrayWithCapacity: arrayValue.count];
for (OFString *string in arrayValue) {
OFINICategoryPair *pair;
if (![string isKindOfClass: [OFString class]])
@throw [OFInvalidArgumentException exception];
pair = [[[OFINICategoryPair alloc] init] autorelease];
pair->_key = [key copy];
pair->_value = [string copy];
[pairs addObject: pair];
}
lines = _lines.objects;
count = _lines.count;
replaced = false;
for (size_t i = 0; i < count; i++) {
OFINICategoryPair *pair;
if (![lines[i] isKindOfClass: [OFINICategoryPair class]])
continue;
pair = lines[i];
if ([pair->_key isEqual: key]) {
[_lines removeObjectAtIndex: i];
if (!replaced) {
[_lines insertObjectsFromArray: pairs
atIndex: i];
replaced = true;
/* Continue after inserted pairs */
i += arrayValue.count - 1;
} else
i--; /* Continue at same position */
lines = _lines.objects;
count = _lines.count;
continue;
}
}
if (!replaced)
[_lines addObjectsFromArray: pairs];
objc_autoreleasePoolPop(pool);
}
- (void)removeValueForKey: (OFString *)key
{
void *pool = objc_autoreleasePoolPush();
id const *lines = _lines.objects;
size_t count = _lines.count;
for (size_t i = 0; i < count; i++) {
OFINICategoryPair *pair;
if (![lines[i] isKindOfClass: [OFINICategoryPair class]])
continue;
pair = lines[i];
if ([pair->_key isEqual: key]) {
[_lines removeObjectAtIndex: i];
lines = _lines.objects;
count = _lines.count;
i--; /* Continue at same position */
continue;
}
}
objc_autoreleasePoolPop(pool);
}
- (bool)of_writeToStream: (OFStream *)stream
encoding: (OFStringEncoding)encoding
first: (bool)first
{
if (_lines.count == 0)
return false;
if (_name.length > 0) {
if (first)
[stream writeFormat: @"[%@]\r\n", _name];
else
[stream writeFormat: @"\r\n[%@]\r\n", _name];
}
for (id line in _lines) {
if ([line isKindOfClass: [OFINICategoryComment class]]) {
OFINICategoryComment *comment = line;
[stream writeFormat: @"%@\r\n", comment->_comment];
} else if ([line isKindOfClass: [OFINICategoryPair class]]) {
OFINICategoryPair *pair = line;
OFString *key = escapeString(pair->_key);
OFString *value = escapeString(pair->_value);
OFString *tmp = [OFString
stringWithFormat: @"%@=%@\r\n", key, value];
[stream writeString: tmp encoding: encoding];
} else
@throw [OFInvalidArgumentException exception];
}
return true;
}
- (OFString *)description
{
return [OFString stringWithFormat: @"<%@ \"%@\": %@>",
self.class, _name, _lines];
}
@end