/*
 * Copyright (c) 2008-2023 Jonathan Schleifer <js@nil.im>
 *
 * 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"

#include <string.h>

#import "OFLHAArchiveEntry.h"
#import "OFLHAArchiveEntry+Private.h"
#import "OFArray.h"
#import "OFCRC16.h"
#import "OFData.h"
#import "OFDate.h"
#import "OFNumber.h"
#import "OFStream.h"
#import "OFString.h"

#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;
	uint8_t day = (MSDOSDate & 0x1F);
	uint8_t hour = (MSDOSDate & 0xF800) >> 11;
	uint8_t minute = (MSDOSDate & 0x7E0) >> 5;
	uint8_t second = (MSDOSDate & 0x1F) << 1;
	OFString *dateString;

	dateString = [OFString
	    stringWithFormat: @"%04u-%02u-%02u %02u:%02u:%02u",
			      year, month, day, hour, minute, second];

	return [OFDate dateWithLocalDateString: dateString
					format: @"%Y-%m-%d %H:%M:%S"];
}

static void
parseFileNameExtension(OFLHAArchiveEntry *entry, OFData *extension,
    OFStringEncoding encoding)
{
	[entry->_fileName release];
	entry->_fileName = nil;

	entry->_fileName = [[OFString alloc]
	    initWithCString: (char *)extension.items + 1
		   encoding: encoding
		     length: [extension count] - 1];
}

static void
parseDirectoryNameExtension(OFLHAArchiveEntry *entry, OFData *extension,
    OFStringEncoding encoding)
{
	void *pool = objc_autoreleasePoolPush();
	OFMutableData *data = [[extension mutableCopy] autorelease];
	char *items = data.mutableItems;
	size_t count = data.count;
	OFMutableString *directoryName;

	for (size_t i = 1; i < count; i++)
		if (items[i] == '\xFF')
			items[i] = '/';

	directoryName = [OFMutableString stringWithCString: items + 1
						  encoding: encoding
						    length: count - 1];

	if (![directoryName hasSuffix: @"/"])
		[directoryName appendString: @"/"];

	[directoryName makeImmutable];

	[entry->_directoryName release];
	entry->_directoryName = nil;

	entry->_directoryName = [directoryName copy];

	objc_autoreleasePoolPop(pool);
}

static void
parseCommentExtension(OFLHAArchiveEntry *entry, OFData *extension,
    OFStringEncoding encoding)
{
	[entry->_fileComment release];
	entry->_fileComment = nil;

	entry->_fileComment = [[OFString alloc]
	    initWithCString: (char *)extension.items + 1
		   encoding: encoding
		     length: extension.count - 1];
}

static void
parsePermissionsExtension(OFLHAArchiveEntry *entry, OFData *extension,
    OFStringEncoding encoding)
{
	uint16_t POSIXPermissions;

	if (extension.count != 3)
		@throw [OFInvalidFormatException exception];

	memcpy(&POSIXPermissions, (char *)extension.items + 1, 2);
	POSIXPermissions = OFFromLittleEndian16(POSIXPermissions);

	[entry->_POSIXPermissions release];
	entry->_POSIXPermissions = nil;

	entry->_POSIXPermissions =
	    [[OFNumber alloc] initWithUnsignedShort: POSIXPermissions];
}

static void
parseGIDUIDExtension(OFLHAArchiveEntry *entry, OFData *extension,
    OFStringEncoding encoding)
{
	uint16_t ownerAccountID, groupOwnerAccountID;

	if (extension.count != 5)
		@throw [OFInvalidFormatException exception];

	memcpy(&groupOwnerAccountID, (char *)extension.items + 1, 2);
	groupOwnerAccountID = OFFromLittleEndian16(groupOwnerAccountID);

	memcpy(&ownerAccountID, (char *)extension.items + 3, 2);
	ownerAccountID = OFFromLittleEndian16(ownerAccountID);

	[entry->_groupOwnerAccountID release];
	entry->_groupOwnerAccountID = nil;

	[entry->_ownerAccountID release];
	entry->_ownerAccountID = nil;

	entry->_groupOwnerAccountID =
	    [[OFNumber alloc] initWithUnsignedShort: groupOwnerAccountID];
	entry->_ownerAccountID =
	    [[OFNumber alloc] initWithUnsignedShort: ownerAccountID];
}

static void
parseGroupExtension(OFLHAArchiveEntry *entry, OFData *extension,
    OFStringEncoding encoding)
{
	[entry->_groupOwnerAccountName release];
	entry->_groupOwnerAccountName = nil;

	entry->_groupOwnerAccountName = [[OFString alloc]
	    initWithCString: (char *)extension.items + 1
		   encoding: encoding
		     length: extension.count - 1];
}

static void
parseOwnerExtension(OFLHAArchiveEntry *entry, OFData *extension,
    OFStringEncoding encoding)
{
	[entry->_ownerAccountName release];
	entry->_ownerAccountName = nil;

	entry->_ownerAccountName = [[OFString alloc]
	    initWithCString: (char *)extension.items + 1
		   encoding: encoding
		     length: extension.count - 1];
}

static void
parseModificationDateExtension(OFLHAArchiveEntry *entry, OFData *extension,
    OFStringEncoding encoding)
{
	uint32_t modificationDate;

	if (extension.count != 5)
		@throw [OFInvalidFormatException exception];

	memcpy(&modificationDate, (char *)extension.items + 1, 4);
	modificationDate = OFFromLittleEndian32(modificationDate);

	[entry->_modificationDate release];
	entry->_modificationDate = nil;

	entry->_modificationDate = [[OFDate alloc]
	    initWithTimeIntervalSince1970: modificationDate];
}

static bool
parseExtension(OFLHAArchiveEntry *entry, OFData *extension,
    OFStringEncoding encoding, bool allowFileName)
{
	void (*function)(OFLHAArchiveEntry *, OFData *, OFStringEncoding) =
	    NULL;

	switch (*(char *)[extension itemAtIndex: 0]) {
	case 0x01:
		if (allowFileName)
			function = parseFileNameExtension;
		break;
	case 0x02:
		function = parseDirectoryNameExtension;
		break;
	case 0x3F:
		function = parseCommentExtension;
		break;
	case 0x50:
		function = parsePermissionsExtension;
		break;
	case 0x51:
		function = parseGIDUIDExtension;
		break;
	case 0x52:
		function = parseGroupExtension;
		break;
	case 0x53:
		function = parseOwnerExtension;
		break;
	case 0x54:
		function = parseModificationDateExtension;
		break;
	}

	if (function == NULL)
		return false;

	function(entry, extension, encoding);
	return true;
}

static void
readExtensions(OFLHAArchiveEntry *entry, OFStream *stream,
    OFStringEncoding encoding, bool allowFileName)
{
	uint16_t size;

	while ((size = [stream readLittleEndianInt16]) > 0) {
		OFData *extension;

		if (size < 2)
			@throw [OFInvalidFormatException exception];

		extension = [stream readDataWithCount: size - 2];

		if (!parseExtension(entry, extension, encoding, allowFileName))
			[entry->_extensions addObject: extension];

		if (entry->_headerLevel == 1) {
			if (entry->_compressedSize < size)
				@throw [OFInvalidFormatException exception];

			entry->_compressedSize -= size;
		}
	}
}

static void
getFileNameAndDirectoryName(OFLHAArchiveEntry *entry, OFStringEncoding encoding,
    const char **fileName, size_t *fileNameLength,
    const char **directoryName, size_t *directoryNameLength)
{
	OFMutableData *data;
	char *cString;
	size_t length;
	size_t pos;

	/*
	 * We use OFMutableData to have an autoreleased buffer that we can
	 * return indirectly.
	 */
	data = [OFMutableData
	    dataWithItems: [entry->_directoryName cStringWithEncoding: encoding]
		    count: [entry->_directoryName
			       cStringLengthWithEncoding: encoding]];
	[data addItems: [entry->_fileName cStringWithEncoding: encoding]
		 count: [entry->_fileName cStringLengthWithEncoding: encoding]];

	cString = data.mutableItems;
	length = data.count;
	pos = 0;

	for (size_t i = 0; i < length; i++) {
		if (cString[i] == '/' || cString[i] == '\\') {
			cString[i] = '\xFF';
			pos = i + 1;
		}
	}

	*fileName = cString + pos;
	*fileNameLength = length - pos;
	*directoryName = cString;
	*directoryNameLength = pos;
}

- (instancetype)init
{
	OF_INVALID_INIT_METHOD
}

- (instancetype)of_init
{
	self = [super init];

	@try {
		_compressionMethod = @"-lh0-";
		_modificationDate = [[OFDate alloc] init];
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (instancetype)of_initWithHeader: (char [21])header
			   stream: (OFStream *)stream
			 encoding: (OFStringEncoding)encoding
{
	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);
		_uncompressedSize =
		    OFFromLittleEndian32((uint32_t)_uncompressedSize);

		memcpy(&date, header + 15, 4);
		date = OFFromLittleEndian32(date);

		_headerLevel = header[20];
		_extensions = [[OFMutableArray alloc] init];

		switch (_headerLevel) {
		case 0:
		case 1:;
			void *pool = objc_autoreleasePoolPush();
			uint8_t fileNameLength;
			OFString *tmp;

			_modificationDate = [parseMSDOSDate(date) retain];

			fileNameLength = [stream readInt8];
			tmp = [stream readStringWithLength: fileNameLength
						  encoding: encoding];
			tmp = [tmp stringByReplacingOccurrencesOfString: @"\\"
							     withString: @"/"];
			_fileName = [tmp copy];

			_CRC16 = [stream readLittleEndianInt16];

			if (_headerLevel == 1) {
				_operatingSystemIdentifier = [stream readInt8];

				readExtensions(self, stream, encoding, false);
			}

			objc_autoreleasePoolPop(pool);
			break;
		case 2:
			_modificationDate = [[OFDate alloc]
			    initWithTimeIntervalSince1970: date];

			_CRC16 = [stream readLittleEndianInt16];
			_operatingSystemIdentifier = [stream readInt8];

			readExtensions(self, stream, encoding, true);

			break;
		default:;
			OFString *version = [OFString
			    stringWithFormat: @"%u", _headerLevel];

			@throw [OFUnsupportedVersionException
			    exceptionWithVersion: version];
		}

		if (_fileName == nil)
			@throw [OFInvalidFormatException exception];

		[_extensions makeImmutable];
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	[_compressionMethod release];
	[_fileName release];
	[_directoryName release];
	[_modificationDate release];
	[_fileComment release];
	[_POSIXPermissions release];
	[_ownerAccountID release];
	[_groupOwnerAccountID release];
	[_ownerAccountName release];
	[_groupOwnerAccountName release];
	[_extensions release];

	[super dealloc];
}

- (id)copy
{
	return [self retain];
}

- (id)mutableCopy
{
	OFLHAArchiveEntry *copy = [[OFMutableLHAArchiveEntry alloc]
	    initWithFileName: _fileName];

	@try {
		[copy->_compressionMethod release];
		copy->_compressionMethod = nil;

		[copy->_modificationDate release];
		copy->_modificationDate = nil;

		copy->_directoryName = [_directoryName copy];
		copy->_compressionMethod = [_compressionMethod copy];
		copy->_compressedSize = _compressedSize;
		copy->_uncompressedSize = _uncompressedSize;
		copy->_modificationDate = [_modificationDate copy];
		copy->_headerLevel = _headerLevel;
		copy->_CRC16 = _CRC16;
		copy->_operatingSystemIdentifier = _operatingSystemIdentifier;
		copy->_fileComment = [_fileComment copy];
		copy->_POSIXPermissions = [_POSIXPermissions retain];
		copy->_ownerAccountID = [_ownerAccountID retain];
		copy->_groupOwnerAccountID = [_groupOwnerAccountID retain];
		copy->_ownerAccountName = [_ownerAccountName copy];
		copy->_groupOwnerAccountName = [_groupOwnerAccountName copy];
		copy->_extensions = [_extensions copy];
	} @catch (id e) {
		[copy release];
		@throw e;
	}

	return copy;
}

- (OFString *)fileName
{
	if (_directoryName == nil)
		return _fileName;

	return [_directoryName stringByAppendingString: _fileName];
}

- (OFString *)compressionMethod
{
	return _compressionMethod;
}

- (unsigned long long)compressedSize
{
	return _compressedSize;
}

- (unsigned long long)uncompressedSize
{
	return _uncompressedSize;
}

- (OFDate *)modificationDate
{
	return _modificationDate;
}

- (uint8_t)headerLevel
{
	return _headerLevel;
}

- (uint16_t)CRC16
{
	return _CRC16;
}

- (uint8_t)operatingSystemIdentifier
{
	return _operatingSystemIdentifier;
}

- (OFString *)fileComment
{
	return _fileComment;
}

- (OFNumber *)POSIXPermissions
{
	return _POSIXPermissions;
}

- (OFNumber *)ownerAccountID
{
	return _ownerAccountID;
}

- (OFNumber *)groupOwnerAccountID
{
	return _groupOwnerAccountID;
}

- (OFString *)ownerAccountName
{
	return _ownerAccountName;
}

- (OFString *)groupOwnerAccountName
{
	return _groupOwnerAccountName;
}

- (OFArray OF_GENERIC(OFData *) *)extensions
{
	return _extensions;
}

- (void)of_writeToStream: (OFStream *)stream
		encoding: (OFStringEncoding)encoding
{
	void *pool = objc_autoreleasePoolPush();
	OFMutableData *data = [OFMutableData dataWithCapacity: 24];
	const char *fileName, *directoryName;
	size_t fileNameLength, directoryNameLength;
	uint16_t tmp16;
	uint32_t tmp32;
	size_t headerSize;

	if ([_compressionMethod cStringLengthWithEncoding:
	    OFStringEncodingASCII] != 5)
		@throw [OFInvalidArgumentException exception];

	getFileNameAndDirectoryName(self, encoding, &fileName, &fileNameLength,
	    &directoryName, &directoryNameLength);

	if (fileNameLength > UINT16_MAX - 3 ||
	    directoryNameLength > UINT16_MAX - 3 ||
	    _compressedSize > UINT32_MAX || _uncompressedSize > UINT32_MAX)
		@throw [OFOutOfRangeException exception];

	/* Length. Filled in after we're done. */
	[data increaseCountBy: 2];

	[data addItems: [_compressionMethod
			    cStringWithEncoding: OFStringEncodingASCII]
		 count: 5];

	tmp32 = OFToLittleEndian32((uint32_t)_compressedSize);
	[data addItems: &tmp32 count: sizeof(tmp32)];

	tmp32 = OFToLittleEndian32((uint32_t)_uncompressedSize);
	[data addItems: &tmp32 count: sizeof(tmp32)];

	tmp32 = OFToLittleEndian32(
	    (uint32_t)_modificationDate.timeIntervalSince1970);
	[data addItems: &tmp32 count: sizeof(tmp32)];

	/* Reserved */
	[data increaseCountBy: 1];

	/* Header level */
	[data addItem: "\x02"];

	/* CRC16 */
	tmp16 = OFToLittleEndian16(_CRC16);
	[data addItems: &tmp16 count: sizeof(tmp16)];

	/* Operating system identifier */
	[data addItem: "U"];

	/* Common header. Contains CRC16, which is written at the end. */
	tmp16 = OFToLittleEndian16(5);
	[data addItems: &tmp16 count: sizeof(tmp16)];
	[data addItem: "\x00"];
	[data increaseCountBy: 2];

	tmp16 = OFToLittleEndian16((uint16_t)fileNameLength + 3);
	[data addItems: &tmp16 count: sizeof(tmp16)];
	[data addItem: "\x01"];
	[data addItems: fileName count: fileNameLength];

	if (directoryNameLength > 0) {
		tmp16 = OFToLittleEndian16((uint16_t)directoryNameLength + 3);
		[data addItems: &tmp16 count: sizeof(tmp16)];
		[data addItem: "\x02"];
		[data addItems: directoryName count: directoryNameLength];
	}

	if (_fileComment != nil) {
		size_t fileCommentLength =
		    [_fileComment cStringLengthWithEncoding: encoding];

		if (fileCommentLength > UINT16_MAX - 3)
			@throw [OFOutOfRangeException exception];

		tmp16 = OFToLittleEndian16((uint16_t)fileCommentLength + 3);
		[data addItems: &tmp16 count: sizeof(tmp16)];
		[data addItem: "\x3F"];
		[data addItems: [_fileComment cStringWithEncoding: encoding]
			 count: fileCommentLength];
	}

	if (_POSIXPermissions != nil) {
		tmp16 = OFToLittleEndian16(5);
		[data addItems: &tmp16 count: sizeof(tmp16)];
		[data addItem: "\x50"];

		tmp16 =
		    OFToLittleEndian16(_POSIXPermissions.unsignedShortValue);
		[data addItems: &tmp16 count: sizeof(tmp16)];
	}

	if (_ownerAccountID != nil || _groupOwnerAccountID != nil) {
		if (_ownerAccountID == nil || _groupOwnerAccountID == nil)
			@throw [OFInvalidArgumentException exception];

		tmp16 = OFToLittleEndian16(7);
		[data addItems: &tmp16 count: sizeof(tmp16)];
		[data addItem: "\x51"];

		tmp16 = OFToLittleEndian16(
		    _groupOwnerAccountID.unsignedShortValue);
		[data addItems: &tmp16 count: sizeof(tmp16)];

		tmp16 = OFToLittleEndian16(_ownerAccountID.unsignedShortValue);
		[data addItems: &tmp16 count: sizeof(tmp16)];
	}

	if (_groupOwnerAccountName != nil) {
		size_t length = [_groupOwnerAccountName
		    cStringLengthWithEncoding: encoding];

		if (length > UINT16_MAX - 3)
			@throw [OFOutOfRangeException exception];

		tmp16 = OFToLittleEndian16((uint16_t)length + 3);
		[data addItems: &tmp16 count: sizeof(tmp16)];
		[data addItem: "\x52"];
		[data addItems: [_groupOwnerAccountName
				    cStringWithEncoding: encoding]
			 count: length];
	}

	if (_ownerAccountName != nil) {
		size_t length =
		    [_ownerAccountName cStringLengthWithEncoding: encoding];

		if (length > UINT16_MAX - 3)
			@throw [OFOutOfRangeException exception];

		tmp16 = OFToLittleEndian16((uint16_t)length + 3);
		[data addItems: &tmp16 count: sizeof(tmp16)];
		[data addItem: "\x53"];
		[data addItems: [_ownerAccountName
				    cStringWithEncoding: encoding]
			 count: length];
	}

	for (OFData *extension in _extensions) {
		size_t extensionLength = extension.count;

		if (extension.itemSize != 1)
			@throw [OFInvalidArgumentException exception];

		if (extensionLength > UINT16_MAX - 2)
			@throw [OFOutOfRangeException exception];

		tmp16 = OFToLittleEndian16((uint16_t)extensionLength + 2);
		[data addItems: &tmp16 count: sizeof(tmp16)];
		[data addItems: extension.items count: extension.count];
	}

	/* Zero-length extension to terminate */
	[data increaseCountBy: 2];

	headerSize = data.count;

	if (headerSize > UINT16_MAX)
		@throw [OFOutOfRangeException exception];

	/* Now fill in the size and CRC16 for the entire header */
	tmp16 = OFToLittleEndian16(headerSize);
	memcpy([data mutableItemAtIndex: 0], &tmp16, sizeof(tmp16));

	tmp16 = OFCRC16(0, data.items, data.count);
	tmp16 = OFToLittleEndian16(tmp16);
	memcpy([data mutableItemAtIndex: 27], &tmp16, sizeof(tmp16));

	[stream writeData: data];

	objc_autoreleasePoolPop(pool);
}

- (OFString *)description
{
	void *pool = objc_autoreleasePoolPush();
	OFString *POSIXPermissions = nil;
	OFString *extensions = [_extensions.description
	    stringByReplacingOccurrencesOfString: @"\n"
				      withString: @"\n\t"];
	OFString *ret;

	if (_POSIXPermissions != nil)
		POSIXPermissions = [OFString stringWithFormat: @"%ho",
		    _POSIXPermissions.unsignedShortValue];

	ret = [OFString stringWithFormat:
	    @"<%@:\n"
	    @"\tFile name = %@\n"
	    @"\tCompression method = %@\n"
	    @"\tCompressed size = %llu\n"
	    @"\tUncompressed size = %llu\n"
	    @"\tModification date = %@\n"
	    @"\tHeader level = %u\n"
	    @"\tCRC16 = %04" @PRIX16 @"\n"
	    @"\tOperating system identifier = %c\n"
	    @"\tComment = %@\n"
	    @"\tPOSIX permissions = %@\n"
	    @"\tOwner account ID = %@\n"
	    @"\tGroup owner account ID = %@\n"
	    @"\tOwner account name = %@\n"
	    @"\tGroup owner accounut name = %@\n"
	    @"\tExtensions: %@"
	    @">",
	    self.class, self.fileName, _compressionMethod, _compressedSize,
	    _uncompressedSize, _modificationDate, _headerLevel, _CRC16,
	    _operatingSystemIdentifier, _fileComment, POSIXPermissions,
	    _ownerAccountID, _groupOwnerAccountID, _ownerAccountName,
	    _groupOwnerAccountName, extensions];

	[ret retain];

	objc_autoreleasePoolPop(pool);

	return [ret autorelease];
}
@end
