@@ -1,7 +1,7 @@ /* - * Copyright (c) 2008-2023 Jonathan Schleifer + * 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 @@ -22,10 +22,11 @@ #import "OFArray.h" #import "OFCRC16.h" #import "OFData.h" #import "OFDate.h" #import "OFNumber.h" +#import "OFSeekableStream.h" #import "OFStream.h" #import "OFString.h" #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" @@ -240,23 +241,31 @@ function(entry, extension, encoding); return true; } -static void +static size_t readExtensions(OFLHAArchiveEntry *entry, OFStream *stream, OFStringEncoding encoding, bool allowFileName) { - uint16_t size; + size_t consumed = 0; - while ((size = [stream readLittleEndianInt16]) > 0) { + for (;;) { + uint16_t size; OFData *extension; + + size = [stream readLittleEndianInt16]; + consumed += 2; + + if (size == 0) + break; if (size < 2) @throw [OFInvalidFormatException exception]; extension = [stream readDataWithCount: size - 2]; + consumed += extension.count; if (!parseExtension(entry, extension, encoding, allowFileName)) [entry->_extensions addObject: extension]; if (entry->_headerLevel == 1) { @@ -264,10 +273,12 @@ @throw [OFInvalidFormatException exception]; entry->_compressedSize -= size; } } + + return consumed; } static void getFileNameAndDirectoryName(OFLHAArchiveEntry *entry, OFStringEncoding encoding, const char **fileName, size_t *fileNameLength, @@ -333,19 +344,10 @@ self = [super init]; @try { uint32_t date; - _compressionMethod = [[OFString alloc] - initWithCString: header + 2 - encoding: OFStringEncodingASCII - length: 5]; - - if (_compressedSize > UINT32_MAX || - _uncompressedSize > UINT32_MAX) - @throw [OFOutOfRangeException exception]; - memcpy(&_compressedSize, header + 7, 4); _compressedSize = OFFromLittleEndian32((uint32_t)_compressedSize); memcpy(&_uncompressedSize, header + 11, 4); @@ -360,13 +362,17 @@ switch (_headerLevel) { case 0: case 1:; void *pool = objc_autoreleasePoolPush(); + uint8_t extendedAreaSize; uint8_t fileNameLength; OFString *tmp; + if (header[0] < (21 - 2) + 1 + 2) + @throw [OFInvalidFormatException exception]; + _modificationDate = [parseMSDOSDate(date) retain]; fileNameLength = [stream readInt8]; tmp = [stream readStringWithLength: fileNameLength encoding: encoding]; @@ -374,26 +380,82 @@ withString: @"/"]; _fileName = [tmp copy]; _CRC16 = [stream readLittleEndianInt16]; + extendedAreaSize = + header[0] - (21 - 2) - 1 - fileNameLength - 2; + if (_headerLevel == 1) { + if (extendedAreaSize < 3) + @throw [OFInvalidFormatException + exception]; + _operatingSystemIdentifier = [stream readInt8]; - readExtensions(self, stream, encoding, false); + /* + * 1 for the operating system identifier, 2 + * because we don't want to skip the size of + * the next extended header. + */ + extendedAreaSize -= 1 + 2; + } + + /* Skip extended area */ + if ([stream isKindOfClass: [OFSeekableStream class]]) + [(OFSeekableStream *)stream + seekToOffset: extendedAreaSize + whence: OFSeekCurrent]; + else { + char buffer[256]; + + while (extendedAreaSize > 0) + extendedAreaSize -= [stream + readIntoBuffer: buffer + length: extendedAreaSize]; } + if (_headerLevel == 1) + readExtensions(self, stream, encoding, false); + objc_autoreleasePoolPop(pool); break; - case 2: + case 2:; + uint32_t padding = 0; + _modificationDate = [[OFDate alloc] initWithTimeIntervalSince1970: date]; _CRC16 = [stream readLittleEndianInt16]; _operatingSystemIdentifier = [stream readInt8]; - readExtensions(self, stream, encoding, true); + /* + * 21 for header, 2 for CRC16, 1 for operating system + * identifier. + */ + padding = ((header[1] << 8) | header[0]) - 21 - 2 - 1; + + padding -= readExtensions(self, stream, encoding, true); + + /* Skip padding */ + if ([stream isKindOfClass: [OFSeekableStream class]]) + [(OFSeekableStream *)stream + seekToOffset: padding + whence: OFSeekCurrent]; + else { + while (padding > 0) { + char buffer[512]; + size_t min = padding; + + if (min > 512) + min = 512; + + padding -= [stream + readIntoBuffer: buffer + length: min]; + } + } break; default:; OFString *version = [OFString stringWithFormat: @"%u", _headerLevel]; @@ -402,10 +464,15 @@ exceptionWithVersion: version]; } if (_fileName == nil) @throw [OFInvalidFormatException exception]; + + _compressionMethod = [[OFString alloc] + initWithCString: header + 2 + encoding: OFStringEncodingASCII + length: 5]; [_extensions makeImmutable]; } @catch (id e) { [self release]; @throw e; @@ -705,10 +772,18 @@ } /* Zero-length extension to terminate */ [data increaseCountBy: 2]; + /* + * Some implementations only check the first byte to see if the end of + * the archive has been reached, which is 0 for every multiple of 256. + * Add one byte of padding to avoid this. + */ + if ((data.count & 0xFF) == 0) + [data increaseCountBy: 1]; + headerSize = data.count; if (headerSize > UINT16_MAX) @throw [OFOutOfRangeException exception];