Index: src/Makefile ================================================================== --- src/Makefile +++ src/Makefile @@ -106,10 +106,12 @@ OFXMLNode.m \ OFXMLParser.m \ OFXMLProcessingInstruction.m \ OFZIPArchive.m \ OFZIPArchiveEntry.m \ + OFZooArchive.m \ + OFZooArchiveEntry.m \ ${USE_SRCS_FILES} \ ${USE_SRCS_PLUGINS} \ ${USE_SRCS_SOCKETS} \ ${USE_SRCS_SUBPROCESSES} \ ${USE_SRCS_THREADS} \ Index: src/OFLHAArchive.m ================================================================== --- src/OFLHAArchive.m +++ src/OFLHAArchive.m @@ -414,13 +414,13 @@ if (_toRead == 0) { _atEndOfStream = true; if (_CRC16 != _entry.CRC16) { OFString *actualChecksum = [OFString stringWithFormat: - @"%04" PRIX16, _CRC16]; + @"%04" @PRIX16, _CRC16]; OFString *expectedChecksum = [OFString stringWithFormat: - @"%04" PRIX16, _entry.CRC16]; + @"%04" @PRIX16, _entry.CRC16]; @throw [OFChecksumMismatchException exceptionWithActualChecksum: actualChecksum expectedChecksum: expectedChecksum]; } Index: src/OFLHAArchiveEntry.m ================================================================== --- src/OFLHAArchiveEntry.m +++ src/OFLHAArchiveEntry.m @@ -31,11 +31,10 @@ #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" #import "OFOutOfRangeException.h" #import "OFUnsupportedVersionException.h" -@implementation OFLHAArchiveEntry static OFDate * parseMSDOSDate(uint32_t MSDOSDate) { uint16_t year = ((MSDOSDate & 0xFE000000) >> 25) + 1980; uint8_t month = (MSDOSDate & 0x1E00000) >> 21; @@ -51,10 +50,11 @@ return [OFDate dateWithLocalDateString: dateString format: @"%Y-%m-%d %H:%M:%S"]; } +@implementation OFLHAArchiveEntry static void parseFileNameExtension(OFLHAArchiveEntry *entry, OFData *extension, OFStringEncoding encoding) { [entry->_fileName release]; Index: src/OFZIPArchiveEntry.m ================================================================== --- src/OFZIPArchiveEntry.m +++ src/OFZIPArchiveEntry.m @@ -426,12 +426,12 @@ OFString *ret = [OFString stringWithFormat: @"<%@:\n" @"\tFile name = %@\n" @"\tFile comment = %@\n" @"\tGeneral purpose bit flag = %u\n" - @"\tCompressed size = %" @PRIu64 "\n" - @"\tUncompressed size = %" @PRIu64 "\n" + @"\tCompressed size = %llu\n" + @"\tUncompressed size = %llu\n" @"\tCompression method = %@\n" @"\tModification date = %@\n" @"\tCRC32 = %08" @PRIX32 @"\n" @"\tExtra field = %@\n" @">", ADDED src/OFZooArchive.h Index: src/OFZooArchive.h ================================================================== --- src/OFZooArchive.h +++ src/OFZooArchive.h @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2008-2024 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#import "OFObject.h" +#import "OFString.h" +#import "OFZooArchiveEntry.h" + +OF_ASSUME_NONNULL_BEGIN + +@class OFStream; + +/** + * @class OFZooArchive OFZooArchive.h ObjFW/OFZooArchive.h + * + * @brief A class for accessing and manipulating Zoo files. + */ +OF_SUBCLASSING_RESTRICTED +@interface OFZooArchive: OFObject +{ + OF_KINDOF(OFStream *) _stream; + uint_least8_t _mode; + OFStringEncoding _encoding; + OFZooArchiveEntry *_Nullable _currentEntry; +#ifdef OF_ZOO_ARCHIVE_M +@public +#endif + OFStream *_Nullable _lastReturnedStream; +} + +/** + * @brief The encoding to use for the archive. Defaults to UTF-8. + */ +@property (nonatomic) OFStringEncoding encoding; + +/** + * @brief Creates a new OFZooArchive object with the specified stream. + * + * @param stream A stream from which the Zoo archive will be read. + * For read mode, this needs to be an OFSeekableStream. + * @param mode The mode for the Zoo file. The only valid mode is "r" for + * reading. + * @return A new, autoreleased OFZooArchive + */ ++ (instancetype)archiveWithStream: (OFStream *)stream mode: (OFString *)mode; + +/** + * @brief Creates a new OFZooArchive object with the specified file. + * + * @param IRI The IRI to the Zoo file + * @param mode The mode for the Zoo file. The only valid mode is "r" for + * reading. + * @return A new, autoreleased OFZooArchive + */ ++ (instancetype)archiveWithIRI: (OFIRI *)IRI mode: (OFString *)mode; + +- (instancetype)init OF_UNAVAILABLE; + +/** + * @brief Initializes an already allocated OFZooArchive object with the + * specified stream. + * + * @param stream A stream from which the Zoo archive will be read. + * For read mode, this needs to be an OFSeekableStream. + * @param mode The mode for the Zoo file. The only valid mode is "r" for + * reading. + * @return An initialized OFZooArchive + */ +- (instancetype)initWithStream: (OFStream *)stream + mode: (OFString *)mode OF_DESIGNATED_INITIALIZER; + +/** + * @brief Initializes an already allocated OFZooArchive object with the + * specified file. + * + * @param IRI The IRI to the Zoo file + * @param mode The mode for the Zoo file. The only valid mode is "r" for + * reading. + * @return An initialized OFZooArchive + */ +- (instancetype)initWithIRI: (OFIRI *)IRI mode: (OFString *)mode; + +/** + * @brief Returns the next entry from the Zoo archive or `nil` if all entries + * have been read. + * + * @note This is only available in read mode. + * + * @warning Calling @ref nextEntry will invalidate all streams returned by + * @ref streamForReadingCurrentEntry! Reading from an invalidated + * stream will throw an @ref OFReadFailedException! + * + * @return The next entry from the Zoo archive or `nil` if all entries have + * been read + * @throw OFInvalidFormatException The archive's format is invalid + * @throw OFTruncatedDataException The archive was truncated + */ +- (nullable OFZooArchiveEntry *)nextEntry; + +/** + * @brief Returns a stream for reading the current entry. + * + * @note This is only available in read mode. + * + * @note The returned stream conforms to @ref OFReadyForReadingObserving if the + * underlying stream does so, too. + * + * @return A stream for reading the current entry + */ +- (OFStream *)streamForReadingCurrentEntry; + +/** + * @brief Closes the OFZooArchive. + * + * @throw OFNotOpenException The archive is not open + */ +- (void)close; +@end + +OF_ASSUME_NONNULL_END ADDED src/OFZooArchive.m Index: src/OFZooArchive.m ================================================================== --- src/OFZooArchive.m +++ src/OFZooArchive.m @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2008-2024 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#define OF_ZOO_ARCHIVE_M + +#include "config.h" + +#import "OFZooArchive.h" +#import "OFZooArchiveEntry.h" +#import "OFZooArchiveEntry+Private.h" +#import "OFCRC16.h" +#import "OFIRI.h" +#import "OFIRIHandler.h" +#import "OFLHADecompressingStream.h" +#import "OFSeekableStream.h" +#import "OFStream.h" +#import "OFString.h" + +#import "OFChecksumMismatchException.h" +#import "OFInvalidArgumentException.h" +#import "OFInvalidFormatException.h" +#import "OFNotOpenException.h" +#import "OFTruncatedDataException.h" +#import "OFUnsupportedVersionException.h" + +enum { + modeRead +}; + +OF_DIRECT_MEMBERS +@interface OFZooArchive () +- (void)of_readArchiveHeader; +@end + +OF_DIRECT_MEMBERS +@interface OFZooArchiveFileReadStream: OFStream +{ + OFZooArchive *_archive; + OF_KINDOF(OFStream *) _stream; + OFStream *_decompressedStream; + OFZooArchiveEntry *_entry; + unsigned long long _toRead; + uint16_t _CRC16; + bool _atEndOfStream; +} + +- (instancetype)of_initWithArchive: (OFZooArchive *)archive + stream: (OFStream *)stream + entry: (OFZooArchiveEntry *)entry; +@end + +@implementation OFZooArchive +@synthesize encoding = _encoding; + ++ (instancetype)archiveWithStream: (OFStream *)stream mode: (OFString *)mode +{ + return [[[self alloc] initWithStream: stream mode: mode] autorelease]; +} + ++ (instancetype)archiveWithIRI: (OFIRI *)IRI mode: (OFString *)mode +{ + return [[[self alloc] initWithIRI: IRI mode: mode] autorelease]; +} + +- (instancetype)init +{ + OF_INVALID_INIT_METHOD +} + +- (instancetype)initWithStream: (OFStream *)stream mode: (OFString *)mode +{ + self = [super init]; + + @try { + if ([mode isEqual: @"r"]) + _mode = modeRead; + else + @throw [OFInvalidArgumentException exception]; + + _stream = [stream retain]; + _encoding = OFStringEncodingUTF8; + + if (_mode == modeRead) { + if (![stream isKindOfClass: [OFSeekableStream class]]) + @throw [OFInvalidArgumentException exception]; + + [self of_readArchiveHeader]; + } + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (instancetype)initWithIRI: (OFIRI *)IRI mode: (OFString *)mode +{ + void *pool = objc_autoreleasePoolPush(); + OFStream *stream; + + @try { + if ([mode isEqual: @"a"]) + stream = [OFIRIHandler openItemAtIRI: IRI mode: @"r+"]; + else + stream = [OFIRIHandler openItemAtIRI: IRI mode: mode]; + } @catch (id e) { + [self release]; + @throw e; + } + + self = [self initWithStream: stream mode: mode]; + + objc_autoreleasePoolPop(pool); + + return self; +} + +- (void)dealloc +{ + if (_stream != nil) + [self close]; + + [_currentEntry release]; + + [super dealloc]; +} + +- (void)of_readArchiveHeader +{ + char headerText[20]; + uint32_t firstFileOffset; + + [_stream readIntoBuffer: headerText exactLength: 20]; + + if ([_stream readLittleEndianInt32] != 0xFDC4A7DC) + @throw [OFInvalidFormatException exception]; + + firstFileOffset = [_stream readLittleEndianInt32]; + + if ([_stream readLittleEndianInt32] != ~(uint32_t)(firstFileOffset - 1)) + @throw [OFInvalidFormatException exception]; + + /* Version */ + [_stream readBigEndianInt16]; + + [_stream seekToOffset: firstFileOffset whence: OFSeekSet]; +} + +- (OFZooArchiveEntry *)nextEntry +{ + if (_mode != modeRead) + @throw [OFInvalidArgumentException exception]; + + if (_currentEntry != nil) + [_stream seekToOffset: _currentEntry->_nextHeaderOffset + whence: OFSeekSet]; + + [_currentEntry release]; + _currentEntry = nil; + + @try { + [_lastReturnedStream close]; + } @catch (OFNotOpenException *e) { + /* Might have already been closed by the user - that's fine. */ + } + _lastReturnedStream = nil; + + _currentEntry = [[OFZooArchiveEntry alloc] + of_initWithStream: _stream + encoding: _encoding]; + + if (_currentEntry->_nextHeaderOffset == 0) { + /* + * End of archive is marked by a header that has the next + * header's offset set to 0. + */ + [_currentEntry release]; + _currentEntry = nil; + } + + return _currentEntry; +} + +- (OFStream *)streamForReadingCurrentEntry +{ + if (_mode != modeRead) + @throw [OFInvalidArgumentException exception]; + + if (_currentEntry == nil) + @throw [OFInvalidArgumentException exception]; + + _lastReturnedStream = [[[OFZooArchiveFileReadStream alloc] + of_initWithArchive: self + stream: _stream + entry: _currentEntry] autorelease]; + + return _lastReturnedStream; +} + +- (void)close +{ + if (_stream == nil) + @throw [OFNotOpenException exceptionWithObject: self]; + + @try { + [_lastReturnedStream close]; + } @catch (OFNotOpenException *e) { + /* Might have already been closed by the user - that's fine. */ + } + + _lastReturnedStream = nil; + + [_stream release]; + _stream = nil; +} +@end + +@implementation OFZooArchiveFileReadStream +- (instancetype)of_initWithArchive: (OFZooArchive *)archive + stream: (OFStream *)stream + entry: (OFZooArchiveEntry *)entry +{ + self = [super init]; + + @try { + _archive = [archive retain]; + _stream = [stream retain]; + + switch (entry.compressionMethod) { + case 0: + _decompressedStream = [stream retain]; + break; + case 2: + _decompressedStream = [[OFLHADecompressingStream alloc] + of_initWithStream: stream + distanceBits: 4 + dictionaryBits: 14]; + break; + default: + @throw [OFUnsupportedVersionException + exceptionWithVersion: [OFString + stringWithFormat: @"%u", entry.compressionMethod]]; + } + + _entry = [entry copy]; + _toRead = entry.uncompressedSize; + + [_stream seekToOffset: entry->_dataOffset whence: OFSeekSet]; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + if (_stream != nil && _decompressedStream != nil) + [self close]; + + [_entry release]; + + if (_archive->_lastReturnedStream == self) + _archive->_lastReturnedStream = nil; + + [_archive release]; + + [super dealloc]; +} + +- (bool)lowlevelIsAtEndOfStream +{ + if (_stream == nil) + @throw [OFNotOpenException exceptionWithObject: self]; + + return _atEndOfStream; +} + +- (size_t)lowlevelReadIntoBuffer: (void *)buffer length: (size_t)length +{ + size_t ret; + + if (_stream == nil) + @throw [OFNotOpenException exceptionWithObject: self]; + + if (_atEndOfStream) + return 0; + + if ([_stream isAtEndOfStream] && + !_decompressedStream.hasDataInReadBuffer) + @throw [OFTruncatedDataException exception]; + + if (length > _toRead) + length = (size_t)_toRead; + + ret = [_decompressedStream readIntoBuffer: buffer length: length]; + + _toRead -= ret; + _CRC16 = OFCRC16(_CRC16, buffer, ret); + + if (_toRead == 0) { + _atEndOfStream = true; + + if (_CRC16 != _entry.CRC16) { + OFString *actualChecksum = [OFString stringWithFormat: + @"%04" @PRIX16, _CRC16]; + OFString *expectedChecksum = [OFString stringWithFormat: + @"%04" @PRIX16, _entry.CRC16]; + + @throw [OFChecksumMismatchException + exceptionWithActualChecksum: actualChecksum + expectedChecksum: expectedChecksum]; + } + } + + return ret; +} + +- (bool)hasDataInReadBuffer +{ + return (super.hasDataInReadBuffer || + _decompressedStream.hasDataInReadBuffer); +} + +- (int)fileDescriptorForReading +{ + return ((id )_decompressedStream) + .fileDescriptorForReading; +} + +- (void)close +{ + if (_stream == nil || _decompressedStream == nil) + @throw [OFNotOpenException exceptionWithObject: self]; + + [_stream release]; + _stream = nil; + + [_decompressedStream release]; + _decompressedStream = nil; + + [super close]; +} +@end ADDED src/OFZooArchiveEntry+Private.h Index: src/OFZooArchiveEntry+Private.h ================================================================== --- src/OFZooArchiveEntry+Private.h +++ src/OFZooArchiveEntry+Private.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2008-2024 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#import "OFZooArchive.h" + +OF_ASSUME_NONNULL_BEGIN + +@interface OFZooArchiveEntry () +- (instancetype)of_initWithStream: (OF_KINDOF(OFStream *))stream + encoding: (OFStringEncoding)encoding + OF_METHOD_FAMILY(init) OF_DIRECT; +@end + +OF_ASSUME_NONNULL_END ADDED src/OFZooArchiveEntry.h Index: src/OFZooArchiveEntry.h ================================================================== --- src/OFZooArchiveEntry.h +++ src/OFZooArchiveEntry.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2008-2024 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#import "OFObject.h" +#import "OFArchiveEntry.h" + +OF_ASSUME_NONNULL_BEGIN + +@class OFDate; +@class OFString; + +/** + * @class OFZooArchiveEntry OFZooArchiveEntry.h ObjFW/OFZooArchiveEntry.h + * + * @brief A class which represents an entry in an Zoo archive. + */ +@interface OFZooArchiveEntry: OFObject +{ + uint8_t _compressionMethod; +#ifdef OF_ZOO_ARCHIVE_M +@public +#endif + unsigned long long _nextHeaderOffset, _dataOffset; +@protected + uint16_t _lastModifiedFileDate, _lastModifiedFileTime; + uint16_t _CRC16; + unsigned long long _uncompressedSize, _compressedSize; + bool _deleted; + OFString *_Nullable _fileComment; + OFString *_fileName; + OF_RESERVE_IVARS(OFZooArchiveEntry, 4) +} + +/** + * @brief The compression method of the entry. + */ +@property (readonly, nonatomic) uint8_t compressionMethod; + +/** + * @brief The CRC16 of the file. + */ +@property (readonly, nonatomic) uint16_t CRC16; + +/** + * @brief Whether the file was deleted. + */ +@property (readonly, nonatomic, getter=isDeleted) bool deleted; + +- (instancetype)init OF_UNAVAILABLE; +@end + +OF_ASSUME_NONNULL_END ADDED src/OFZooArchiveEntry.m Index: src/OFZooArchiveEntry.m ================================================================== --- src/OFZooArchiveEntry.m +++ src/OFZooArchiveEntry.m @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2008-2024 Jonathan Schleifer + * + * All rights reserved. + * + * This file is part of ObjFW. It may be distributed under the terms of the + * Q Public License 1.0, which can be found in the file LICENSE.QPL included in + * the packaging of this file. + * + * Alternatively, it may be distributed under the terms of the GNU General + * Public License, either version 2 or 3, which can be found in the file + * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this + * file. + */ + +#include "config.h" + +#import "OFZooArchiveEntry.h" +#import "OFZooArchiveEntry+Private.h" +#import "OFDate.h" +#import "OFSeekableStream.h" +#import "OFStream.h" +#import "OFString.h" + +#import "OFInvalidFormatException.h" + +@implementation OFZooArchiveEntry +@synthesize compressionMethod = _compressionMethod, CRC16 = _CRC16; +@synthesize uncompressedSize = _uncompressedSize; +@synthesize compressedSize = _compressedSize, deleted = _deleted; +@synthesize fileComment = _fileComment, fileName = _fileName; + +- (instancetype)init +{ + OF_INVALID_INIT_METHOD +} + +- (instancetype)of_initWithStream: (OF_KINDOF(OFStream *))stream + encoding: (OFStringEncoding)encoding +{ + self = [super init]; + + @try { + void *pool = objc_autoreleasePoolPush(); + uint32_t commentOffset; + uint16_t commentLength; + + if ([stream readLittleEndianInt32] != 0xFDC4A7DC) + @throw [OFInvalidFormatException exception]; + + /* Type seems to be always 2 */ + if ([stream readInt8] != 2) + @throw [OFInvalidFormatException exception]; + + _compressionMethod = [stream readInt8]; + _nextHeaderOffset = [stream readLittleEndianInt32]; + _dataOffset = [stream readLittleEndianInt32]; + _lastModifiedFileDate = [stream readLittleEndianInt16]; + _lastModifiedFileTime = [stream readLittleEndianInt16]; + _CRC16 = [stream readLittleEndianInt16]; + _uncompressedSize = [stream readLittleEndianInt32]; + _compressedSize = [stream readLittleEndianInt32]; + /* Version */ + [stream readBigEndianInt16]; + _deleted = [stream readInt8]; + /* Unknown. Most likely padding to get to 2 byte alignment? */ + [stream readInt8]; + commentOffset = [stream readLittleEndianInt32]; + commentLength = [stream readLittleEndianInt16]; + + _fileName = [[stream readLineWithEncoding: encoding] retain]; + + if (commentOffset != 0) { + [stream seekToOffset: commentOffset whence: OFSeekSet]; + _fileComment = [[stream + readStringWithLength: commentLength + encoding: encoding] retain]; + } + + objc_autoreleasePoolPop(pool); + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + [_fileComment release]; + [_fileName release]; + + [super dealloc]; +} + +- (id)copy +{ + return [self retain]; +} + +- (OFDate *)modificationDate +{ + void *pool = objc_autoreleasePoolPush(); + uint16_t year = ((_lastModifiedFileDate & 0xFE00) >> 9) + 1980; + uint8_t month = (_lastModifiedFileDate & 0x1E0) >> 5; + uint8_t day = (_lastModifiedFileDate & 0x1F); + uint8_t hour = (_lastModifiedFileTime & 0xF800) >> 11; + uint8_t minute = (_lastModifiedFileTime & 0x7E0) >> 5; + uint8_t second = (_lastModifiedFileTime & 0x1F) << 1; + OFDate *date; + OFString *dateString; + + dateString = [OFString + stringWithFormat: @"%04u-%02u-%02u %02u:%02u:%02u", + year, month, day, hour, minute, second]; + + date = [[OFDate alloc] initWithLocalDateString: dateString + format: @"%Y-%m-%d %H:%M:%S"]; + + objc_autoreleasePoolPop(pool); + + return [date autorelease]; +} + +- (OFString *)description +{ + void *pool = objc_autoreleasePoolPush(); + OFString *ret = [OFString stringWithFormat: + @"<%@: \n" + @"\tFile name = %@\n" + @"\tFile comment = %@\n" + @"\tCompressed size = %llu\n" + @"\tUncompressed size = %llu\n" + @"\tCompression method = %u\n" + @"\tModification date = %@\n" + @"\tCRC16 = %04" @PRIX16 @"\n" + @"\tDeleted = %u\n" + @">", + self.class, _fileName, _fileComment, _compressedSize, + _uncompressedSize, _compressionMethod, self.modificationDate, + _CRC16, _deleted]; + + [ret retain]; + + objc_autoreleasePoolPop(pool); + + return [ret autorelease]; +} +@end Index: src/ObjFW.h ================================================================== --- src/ObjFW.h +++ src/ObjFW.h @@ -64,10 +64,12 @@ #import "OFLHAArchiveEntry.h" #import "OFTarArchive.h" #import "OFTarArchiveEntry.h" #import "OFZIPArchive.h" #import "OFZIPArchiveEntry.h" +#import "OFZooArchive.h" +#import "OFZooArchiveEntry.h" #import "OFFileManager.h" #ifdef OF_HAVE_FILES # import "OFFile.h" #endif #import "OFINIFile.h"