Index: src/OFIRI.h ================================================================== --- src/OFIRI.h +++ src/OFIRI.h @@ -111,10 +111,15 @@ * * Returns the empty string if the path is the root. */ @property (readonly, copy, nonatomic) OFString *lastPathComponent; +/** + * @brief The path extension of the IRI. + */ +@property (readonly, copy, nonatomic) OFString *pathExtension; + /** * @brief The query part of the IRI. */ @property OF_NULLABLE_PROPERTY (readonly, copy, nonatomic) OFString *query; @@ -164,10 +169,15 @@ /** * @brief The IRI with the last path component deleted. */ @property (readonly, nonatomic) OFIRI *IRIByDeletingLastPathComponent; +/** + * @brief The IRI with the path extension deleted. + */ +@property (readonly, nonatomic) OFIRI *IRIByDeletingPathExtension; + /** * @brief The IRI with percent-encoding added for all Unicode characters. */ @property (readonly, nonatomic) OFIRI *IRIByAddingPercentEncodingForUnicodeCharacters; @@ -304,10 +314,18 @@ * that the IRI path should have a trailing slash * @return A new IRI with the specified path component appended */ - (OFIRI *)IRIByAppendingPathComponent: (OFString *)component isDirectory: (bool)isDirectory; + +/** + * @brief Returns a new IRI with the specified path extension appended. + * + * @param extension The path extension to append + * @return A new IRI with the specified path extension appended. + */ +- (OFIRI *)IRIByAppendingPathExtension: (OFString *)extension; @end @interface OFCharacterSet (IRICharacterSets) #ifdef OF_HAVE_CLASS_PROPERTIES @property (class, readonly, nonatomic) Index: src/OFIRI.m ================================================================== --- src/OFIRI.m +++ src/OFIRI.m @@ -1152,10 +1152,31 @@ objc_autoreleasePoolPop(pool); return [ret autorelease]; } + +- (OFString *)pathExtension +{ + void *pool = objc_autoreleasePoolPush(); + OFString *ret, *fileName; + size_t pos; + + fileName = self.lastPathComponent; + pos = [fileName rangeOfString: @"." + options: OFStringSearchBackwards].location; + if (pos == OFNotFound || pos == 0) { + objc_autoreleasePoolPop(pool); + return @""; + } + + ret = [fileName substringFromIndex: pos + 1]; + + [ret retain]; + objc_autoreleasePoolPop(pool); + return [ret autorelease]; +} - (OFString *)query { return _percentEncodedQuery.stringByRemovingPercentEncoding; } @@ -1318,10 +1339,26 @@ OFMutableIRI *IRI = [[self mutableCopy] autorelease]; [IRI deleteLastPathComponent]; [IRI makeImmutable]; return IRI; } + +- (OFIRI *)IRIByAppendingPathExtension: (OFString *)extension +{ + OFMutableIRI *IRI = [[self mutableCopy] autorelease]; + [IRI appendPathExtension: extension]; + [IRI makeImmutable]; + return IRI; +} + +- (OFIRI *)IRIByDeletingPathExtension +{ + OFMutableIRI *IRI = [[self mutableCopy] autorelease]; + [IRI deletePathExtension]; + [IRI makeImmutable]; + return IRI; +} - (OFIRI *)IRIByStandardizingPath { OFMutableIRI *IRI = [[self mutableCopy] autorelease]; [IRI standardizePath]; Index: src/OFMutableIRI.h ================================================================== --- src/OFMutableIRI.h +++ src/OFMutableIRI.h @@ -206,15 +206,27 @@ * appended if there is no slash yet */ - (void)appendPathComponent: (OFString *)component isDirectory: (bool)isDirectory; +/** + * @brief Appends the specified path extension + * + * @param extension The path extension to append + */ +- (void)appendPathExtension: (OFString *)extension; + /** * @brief Deletes the last path component. */ - (void)deleteLastPathComponent; +/** + * @brief Deletes the path extension. + */ +- (void)deletePathExtension; + /** * @brief Resolves relative subpaths. */ - (void)standardizePath; Index: src/OFMutableIRI.m ================================================================== --- src/OFMutableIRI.m +++ src/OFMutableIRI.m @@ -322,26 +322,19 @@ return copy; } - (void)appendPathComponent: (OFString *)component { - [self appendPathComponent: component isDirectory: false]; + bool isDirectory = false; #ifdef OF_HAVE_FILES if ([_scheme isEqual: @"file"] && - ![_percentEncodedPath hasSuffix: @"/"] && - [[OFFileManager defaultManager] directoryExistsAtIRI: self]) { - void *pool = objc_autoreleasePoolPush(); - OFString *path = [_percentEncodedPath - stringByAppendingString: @"/"]; - - [_percentEncodedPath release]; - _percentEncodedPath = [path retain]; - - objc_autoreleasePoolPop(pool); - } + [[OFFileManager defaultManager] directoryExistsAtIRI: self]) + isDirectory = true; #endif + + [self appendPathComponent: component isDirectory: isDirectory]; } - (void)appendPathComponent: (OFString *)component isDirectory: (bool)isDirectory { @@ -374,10 +367,43 @@ [_percentEncodedPath release]; _percentEncodedPath = [path retain]; objc_autoreleasePoolPop(pool); } + +- (void)appendPathExtension: (OFString *)extension +{ + void *pool; + OFMutableString *path; + bool isDirectory = false; + + if (_percentEncodedPath.length == 0) + return; + + pool = objc_autoreleasePoolPush(); + path = [[_percentEncodedPath mutableCopy] autorelease]; + + extension = [extension + stringByAddingPercentEncodingWithAllowedCharacters: + [OFCharacterSet IRIPathAllowedCharacterSet]]; + + if ([path hasSuffix: @"/"]) { + [path deleteCharactersInRange: OFMakeRange(path.length - 1, 1)]; + isDirectory = true; + } + + [path appendFormat: @".%@", extension]; + + if (isDirectory) + [path appendString: @"/"]; + + [path makeImmutable]; + [_percentEncodedPath release]; + _percentEncodedPath = [path retain]; + + objc_autoreleasePoolPop(pool); +} - (void)deleteLastPathComponent { void *pool = objc_autoreleasePoolPush(); OFString *path = _percentEncodedPath; @@ -387,20 +413,51 @@ objc_autoreleasePoolPop(pool); return; } if ([path hasSuffix: @"/"]) - path = [path substringToIndex: path.length - 1]; + path = [path substringToIndex: path.length - 1]; pos = [path rangeOfString: @"/" options: OFStringSearchBackwards].location; if (pos == OFNotFound) { objc_autoreleasePoolPop(pool); return; } path = [path substringToIndex: pos + 1]; + [_percentEncodedPath release]; + _percentEncodedPath = [path retain]; + + objc_autoreleasePoolPop(pool); +} + +- (void)deletePathExtension +{ + void *pool = objc_autoreleasePoolPush(); + OFMutableString *path = [[_percentEncodedPath mutableCopy] autorelease]; + bool isDirectory = false; + size_t pos; + + if ([path hasSuffix: @"/"]) { + [path deleteCharactersInRange: OFMakeRange(path.length - 1, 1)]; + isDirectory = true; + } + + pos = [path rangeOfString: @"." + options: OFStringSearchBackwards].location; + if (pos == OFNotFound) { + objc_autoreleasePoolPop(pool); + return; + } + + [path deleteCharactersInRange: OFMakeRange(pos, path.length - pos)]; + + if (isDirectory) + [path appendString: @"/"]; + + [path makeImmutable]; [_percentEncodedPath release]; _percentEncodedPath = [path retain]; objc_autoreleasePoolPop(pool); } Index: tests/OFIRITests.m ================================================================== --- tests/OFIRITests.m +++ tests/OFIRITests.m @@ -287,10 +287,21 @@ lastPathComponent], @"/"); OTAssertEqualObjects(_IRI[4].lastPathComponent, @"foo/bar"); } + +- (void)testPathExtension +{ + OTAssertEqualObjects( + [[OFIRI IRIWithString: @"http://host/path.dir/path.file"] + pathExtension], @"file"); + + OTAssertEqualObjects( + [[OFIRI IRIWithString: @"http://host/path/path.dir/"] + pathExtension], @"dir"); +} - (void)testQuery { OTAssertEqualObjects(_IRI[0].query, @"que#ry=1&f&oo=b=ar"); OTAssertNil(_IRI[3].query); @@ -378,10 +389,36 @@ OTAssertEqualObjects( [[[OFIRI IRIWithString: @"http://host"] IRIByDeletingLastPathComponent] path], @""); } + +- (void)testIRIByAppendingPathExtension +{ + OTAssertEqualObjects( + [[[OFIRI IRIWithString: @"http://host/path.dir/path"] + IRIByAppendingPathExtension: @"file"] path], + @"/path.dir/path.file"); + + OTAssertEqualObjects( + [[[OFIRI IRIWithString: @"http://host/path/path/"] + IRIByAppendingPathExtension: @"dir"] path], + @"/path/path.dir/"); +} + +- (void)testIRIByDeletingPathExtension +{ + OTAssertEqualObjects( + [[[OFIRI IRIWithString: @"http://host/path.dir/path.file"] + IRIByDeletingPathExtension] path], + @"/path.dir/path"); + + OTAssertEqualObjects( + [[[OFIRI IRIWithString: @"http://host/path/path.dir/"] + IRIByDeletingPathExtension] path], + @"/path/path/"); +} - (void)testIRIByAddingPercentEncodingForUnicodeCharacters { OTAssertEqualObjects( _IRI[10].IRIByAddingPercentEncodingForUnicodeCharacters, Index: utils/ofarc/GZIPArchive.m ================================================================== --- utils/ofarc/GZIPArchive.m +++ utils/ofarc/GZIPArchive.m @@ -124,12 +124,11 @@ @"Cannot extract a specific file of a .gz archive!")]; app->_exitStatus = 1; return; } - /* FIXME: Should use IRI-specific path extension deletion. */ - fileName = _archiveIRI.lastPathComponent.stringByDeletingPathExtension; + fileName = _archiveIRI.IRIByDeletingPathExtension.lastPathComponent; if (app->_outputLevel >= 0) [OFStdOut writeString: OF_LOCALIZED(@"extracting_file", @"Extracting %[file]...", @"file", fileName)]; @@ -162,13 +161,12 @@ } } - (void)printFiles: (OFArray OF_GENERIC(OFString *) *)files { - /* FIXME: Should use IRI-specific path extension deletion. */ - OFString *fileName = _archiveIRI.lastPathComponent - .stringByDeletingPathExtension; + OFString *fileName = + _archiveIRI.IRIByDeletingPathExtension.lastPathComponent; if (files.count > 0) { [OFStdErr writeLine: OF_LOCALIZED( @"cannot_print_specific_file_from_gz", @"Cannot print a specific file of a .gz archive!")]; Index: utils/ofarc/ZIPArchive.m ================================================================== --- utils/ofarc/ZIPArchive.m +++ utils/ofarc/ZIPArchive.m @@ -142,12 +142,13 @@ if (partNumber == lastPartNumber) IRI = _archiveIRI; else { OFMutableIRI *copy = [[_archiveIRI mutableCopy] autorelease]; - copy.path = [_archiveIRI.path.stringByDeletingPathExtension - stringByAppendingFormat: @".z%02u", partNumber + 1]; + [copy deletePathExtension]; + [copy appendPathExtension: [OFString + stringWithFormat: @".z%02u", partNumber + 1]]; [copy makeImmutable]; IRI = copy; } return (OFSeekableStream *)[OFIRIHandler openItemAtIRI: IRI mode: @"r"];