/*
 * 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 <errno.h>

#ifdef HAVE_FCNTL_H
# include <fcntl.h>
#endif

#import "OFDDPSocket.h"
#import "OFDictionary.h"
#import "OFNumber.h"
#import "OFPair.h"
#import "OFSocket.h"
#import "OFSocket+Private.h"

#import "OFAlreadyOpenException.h"
#import "OFBindDDPSocketFailedException.h"
#import "OFGetOptionFailedException.h"
#import "OFInvalidArgumentException.h"
#import "OFNotOpenException.h"
#import "OFOutOfRangeException.h"
#import "OFReadFailedException.h"
#import "OFSetOptionFailedException.h"
#import "OFWriteFailedException.h"

#ifdef HAVE_NET_IF_H
# include <net/if.h>
#endif
#ifdef HAVE_SYS_IOCTL_H
# include <sys/ioctl.h>
#endif

#ifdef OF_HAVE_NETAT_APPLETALK_H
# include <netat/ddp.h>
# include <sys/ioctl.h>

/* Unfortulately, there is no struct for the following in userland headers */
struct ATInterfaceConfig {
	char interfaceName[16];
	unsigned int flags;
	struct at_addr address, router;
	unsigned short netStart, netEnd;
	at_nvestr_t zoneName;
};
#endif

#ifdef OF_HAVE_APPLETALK_IFCONFIG
const OFAppleTalkInterfaceConfigurationKey
    OFAppleTalkInterfaceConfigurationNode =
    @"OFAppleTalkInterfaceConfigurationNode";
const OFAppleTalkInterfaceConfigurationKey
    OFAppleTalkInterfaceConfigurationNetwork =
    @"OFAppleTalkInterfaceConfigurationNetwork";
const OFAppleTalkInterfaceConfigurationKey
    OFAppleTalkInterfaceConfigurationPhase =
    @"OFAppleTalkInterfaceConfigurationPhase";
const OFAppleTalkInterfaceConfigurationKey
    OFAppleTalkInterfaceConfigurationNetworkRange =
    @"OFAppleTalkInterfaceConfigurationNetworkRange";
#endif

@implementation OFDDPSocket
@dynamic delegate;

#ifdef OF_HAVE_APPLETALK_IFCONFIG
+ (void)setConfiguration: (OFAppleTalkInterfaceConfiguration)config
	    forInterface: (OFString *)interfaceName
{
	OFNumber *network, *node, *phase;
	OFPair OF_GENERIC(OFNumber *, OFNumber *) *range;
	int sock;
	struct ifreq request;
	struct sockaddr_at *sat;
	uint16_t rangeStart, rangeEnd;

	if (interfaceName.UTF8StringLength > IFNAMSIZ - 1)
		@throw [OFOutOfRangeException exception];

	network = [config
	    objectForKey: OFAppleTalkInterfaceConfigurationNetwork];
	node = [config objectForKey: OFAppleTalkInterfaceConfigurationNode];
	phase = [config objectForKey: OFAppleTalkInterfaceConfigurationPhase];
	range = [config
	    objectForKey: OFAppleTalkInterfaceConfigurationNetworkRange];

	if (network == nil || node == nil)
		@throw [OFInvalidArgumentException exception];

	if (phase != nil && phase.unsignedCharValue != 1 &&
	    phase.unsignedCharValue != 2)
		@throw [OFInvalidArgumentException exception];

# ifdef OF_MACOS
	if ((sock = socket(AF_APPLETALK, SOCK_RAW, 0)) < 0)
# else
	if ((sock = socket(AF_APPLETALK, SOCK_DGRAM, 0)) < 0)
# endif
		@throw [OFSetOptionFailedException
		    exceptionWithObject: nil
				  errNo: OFSocketErrNo()];

	memset(&request, 0, sizeof(request));
	strncpy(request.ifr_name, interfaceName.UTF8String, IFNAMSIZ - 1);
	sat = (struct sockaddr_at *)&request.ifr_addr;
	sat->sat_family = AF_APPLETALK;
	sat->sat_net = OFToBigEndian16(network.unsignedShortValue);
	sat->sat_node = node.unsignedCharValue;
	/*
	 * The netrange is hidden in sat_zero and different OSes use different
	 * struct names for it, so the portable way is setting sat_zero
	 * directly.
	 */
	sat->sat_zero[0] = (phase != nil ? phase.unsignedCharValue : 2);
	if (range != nil) {
		rangeStart = [range.firstObject unsignedShortValue];
		rangeEnd = [range.secondObject unsignedShortValue];
	} else {
		rangeStart = rangeEnd = network.unsignedShortValue;
	}
	sat->sat_zero[2] = rangeStart >> 8;
	sat->sat_zero[3] = rangeStart & 0xFF;
	sat->sat_zero[4] = rangeEnd >> 8;
	sat->sat_zero[5] = rangeEnd & 0xFF;

	if (ioctl(sock, SIOCSIFADDR, &request) != 0)
		@throw [OFSetOptionFailedException
		    exceptionWithObject: nil
				  errNo: OFSocketErrNo()];

	close(sock);
}

+ (OFAppleTalkInterfaceConfiguration)
    configurationForInterface: (OFString *)interfaceName
{
	int sock;
	struct ifreq request;
	struct sockaddr_at *sat;
# ifndef OF_LINUX
	uint16_t rangeStart, rangeEnd;
	OFPair *range;
# endif

	if (interfaceName.UTF8StringLength > IFNAMSIZ - 1)
		@throw [OFOutOfRangeException exception];

# ifdef OF_MACOS
	if ((sock = socket(AF_APPLETALK, SOCK_RAW, 0)) < 0)
# else
	if ((sock = socket(AF_APPLETALK, SOCK_DGRAM, 0)) < 0)
# endif
		@throw [OFGetOptionFailedException
		    exceptionWithObject: nil
				  errNo: OFSocketErrNo()];

	memset(&request, 0, sizeof(request));
	strncpy(request.ifr_name, interfaceName.UTF8String, IFNAMSIZ - 1);

	if (ioctl(sock, SIOCGIFADDR, &request) < 0) {
		int errNo = OFSocketErrNo();

		/* No AppleTalk configured on this interface. */
		if (errNo == EADDRNOTAVAIL) {
			close(sock);
			return nil;
		}

		@throw [OFGetOptionFailedException exceptionWithObject: nil
								 errNo: errNo];
	}

	sat = (struct sockaddr_at *)&request.ifr_addr;

	close(sock);

# ifndef OF_LINUX
	/*
	 * Linux currently doesn't fill out the phase or netrange.
	 *
	 * The netrange is hidden in sat_zero and different OSes use different
	 * struct names for it, so the portable way is setting sat_zero
	 * directly.
	 */
	rangeStart = sat->sat_zero[2] << 8 | sat->sat_zero[3];
	rangeEnd = sat->sat_zero[4] << 8 | sat->sat_zero[5];
	range = [OFPair
	    pairWithFirstObject: [OFNumber numberWithUnsignedShort: rangeStart]
		   secondObject: [OFNumber numberWithUnsignedShort: rangeEnd]];
# endif

	return [OFDictionary dictionaryWithKeysAndObjects:
	    OFAppleTalkInterfaceConfigurationNode,
	    [OFNumber numberWithUnsignedChar: sat->sat_node],
	    OFAppleTalkInterfaceConfigurationNetwork,
	    [OFNumber numberWithUnsignedShort: OFFromBigEndian16(sat->sat_net)],
# ifndef OF_LINUX
	    OFAppleTalkInterfaceConfigurationPhase,
	    [OFNumber numberWithUnsignedChar: sat->sat_zero[0]],
	    OFAppleTalkInterfaceConfigurationNetworkRange, range,
# endif
	    nil];
}

+ (void)removeConfigurationForInterface: (OFString *)interfaceName
{
	int sock;
	struct ifreq request;

	if (interfaceName.UTF8StringLength > IFNAMSIZ - 1)
		@throw [OFOutOfRangeException exception];

# ifdef OF_MACOS
	if ((sock = socket(AF_APPLETALK, SOCK_RAW, 0)) < 0)
# else
	if ((sock = socket(AF_APPLETALK, SOCK_DGRAM, 0)) < 0)
# endif
		@throw [OFSetOptionFailedException
		    exceptionWithObject: nil
				  errNo: OFSocketErrNo()];

	memset(&request, 0, sizeof(request));
	strncpy(request.ifr_name, interfaceName.UTF8String, IFNAMSIZ - 1);
	request.ifr_addr.sa_family = AF_APPLETALK;

	if (ioctl(sock, SIOCDIFADDR, &request) != 0)
		@throw [OFSetOptionFailedException
		    exceptionWithObject: nil
				  errNo: OFSocketErrNo()];
}
#endif

- (OFSocketAddress)bindToNetwork: (uint16_t)network
			    node: (uint8_t)node
			    port: (uint8_t)port
		    protocolType: (uint8_t)protocolType
{
#ifdef OF_MACOS
	const int one = 1;
	struct ATInterfaceConfig config = { { 0 } };
#endif
	OFSocketAddress address;
#if SOCK_CLOEXEC == 0 && defined(HAVE_FCNTL_H) && defined(FD_CLOEXEC)
	int flags;
#endif

	if (protocolType == 0)
		@throw [OFInvalidArgumentException exception];

	if (_socket != OFInvalidSocketHandle)
		@throw [OFAlreadyOpenException exceptionWithObject: self];

	address = OFSocketAddressMakeAppleTalk(network, node, port);

#if defined(OF_MACOS)
	if ((_socket = socket(address.sockaddr.at.sat_family,
	    SOCK_RAW | SOCK_CLOEXEC, protocolType)) == OFInvalidSocketHandle)
#elif defined(OF_WINDOWS)
	if ((_socket = socket(address.sockaddr.at.sat_family,
	    SOCK_DGRAM | SOCK_CLOEXEC, ATPROTO_BASE + protocolType)) ==
	    OFInvalidSocketHandle)
#else
	if ((_socket = socket(address.sockaddr.at.sat_family,
	    SOCK_DGRAM | SOCK_CLOEXEC, 0)) == OFInvalidSocketHandle)
#endif
		@throw [OFBindDDPSocketFailedException
		    exceptionWithNetwork: network
				    node: node
				    port: port
			    protocolType: protocolType
				  socket: self
				   errNo: OFSocketErrNo()];

	_canBlock = true;

#if SOCK_CLOEXEC == 0 && defined(HAVE_FCNTL_H) && defined(FD_CLOEXEC)
	if ((flags = fcntl(_socket, F_GETFD, 0)) != -1)
		fcntl(_socket, F_SETFD, flags | FD_CLOEXEC);
#endif

	if (bind(_socket, (struct sockaddr *)&address.sockaddr,
	    address.length) != 0) {
		int errNo = OFSocketErrNo();

		closesocket(_socket);
		_socket = OFInvalidSocketHandle;

		@throw [OFBindDDPSocketFailedException
		    exceptionWithNetwork: network
				    node: node
				    port: port
			    protocolType: protocolType
				  socket: self
				   errNo: errNo];
	}

	memset(&address, 0, sizeof(address));
	address.family = OFSocketAddressFamilyAppleTalk;
	address.length = (socklen_t)sizeof(address.sockaddr);

	if (OFGetSockName(_socket, (struct sockaddr *)&address.sockaddr,
	    &address.length) != 0) {
		int errNo = OFSocketErrNo();

		closesocket(_socket);
		_socket = OFInvalidSocketHandle;

		@throw [OFBindDDPSocketFailedException
		    exceptionWithNetwork: network
				    node: node
				    port: port
			    protocolType: protocolType
				  socket: self
				   errNo: errNo];
	}

	if (address.sockaddr.at.sat_family != AF_APPLETALK) {
		closesocket(_socket);
		_socket = OFInvalidSocketHandle;

		@throw [OFBindDDPSocketFailedException
		    exceptionWithNetwork: network
				    node: node
				    port: port
			    protocolType: protocolType
				  socket: self
				   errNo: EAFNOSUPPORT];
	}

#ifdef OF_MACOS
	if (setsockopt(_socket, ATPROTO_NONE, DDP_STRIPHDR, &one,
	    sizeof(one)) != 0 || ioctl(_socket, _IOWR('a', 2,
	    struct ATInterfaceConfig), &config) != 0)
		@throw [OFBindDDPSocketFailedException
		    exceptionWithNetwork: network
				    node: node
				    port: port
			    protocolType: protocolType
				  socket: self
				   errNo: OFSocketErrNo()];

	OFSocketAddressSetAppleTalkNetwork(&address, config.address.s_net);
	OFSocketAddressSetAppleTalkNode(&address, config.address.s_node);
#endif

#if !defined(OF_MACOS) && !defined(OF_WINDOWS)
	_protocolType = protocolType;
#endif

	return address;
}

/*
 * Everybody but macOS and Windows is probably using a netatalk-compatible
 * implementation, which includes the protocol type as the first byte of the
 * data instead of considering it part of the header.
 *
 * The following overrides prepend the protocol type when sending and compare
 * and strip it when receiving.
 *
 * Unfortunately, the downside of this is that the only way to handle receiving
 * a packet with the wrong protocol type is to throw an exception with errNo
 * ENOMSG, while macOS and Windows just filter those out in the kernel.
 * Returning 0 would mean this is indistinguishable from an empty packet, so it
 * has to be an exception.
 */
#if !defined(OF_MACOS) && !defined(OF_WINDOWS)
- (size_t)receiveIntoBuffer: (void *)buffer
		     length: (size_t)length
		     sender: (OFSocketAddress *)sender
{
	ssize_t ret;
	uint8_t protocolType;
	struct iovec iov[2] = {
		{ &protocolType, 1 },
		{ buffer, length }
	};
	struct msghdr msg = {
		.msg_name = (sender != NULL
		    ? (struct sockaddr *)&sender->sockaddr : NULL),
		.msg_namelen = (sender != NULL
		    ? (socklen_t)sizeof(sender->sockaddr) : 0),
		.msg_iov = iov,
		.msg_iovlen = 2
	};

	if (_socket == OFInvalidSocketHandle)
		@throw [OFNotOpenException exceptionWithObject: self];

	if ((ret = recvmsg(_socket, &msg, 0)) < 0)
		@throw [OFReadFailedException
		    exceptionWithObject: self
			requestedLength: length
				  errNo: OFSocketErrNo()];

	if (ret < 1 || protocolType != _protocolType)
		@throw [OFReadFailedException exceptionWithObject: self
						  requestedLength: length
							    errNo: ENOMSG];

	if (sender != NULL) {
		sender->length = msg.msg_namelen;
		sender->family = OFSocketAddressFamilyAppleTalk;
	}

	return ret - 1;
}

- (void)sendBuffer: (const void *)buffer
	    length: (size_t)length
	  receiver: (const OFSocketAddress *)receiver
{
	struct iovec iov[2] = {
		{ &_protocolType, 1 },
		{ (void *)buffer, length }
	};
	struct msghdr msg = {
		.msg_name = (struct sockaddr *)&receiver->sockaddr,
		.msg_namelen = receiver->length,
		.msg_iov = iov,
		.msg_iovlen = 2
	};
	ssize_t bytesWritten;

	if (_socket == OFInvalidSocketHandle)
		@throw [OFNotOpenException exceptionWithObject: self];

	if ((bytesWritten = sendmsg(_socket, &msg, 0)) < 0)
		@throw [OFWriteFailedException
		    exceptionWithObject: self
			requestedLength: length
			   bytesWritten: 0
				  errNo: OFSocketErrNo()];

	if ((size_t)bytesWritten != length + 1) {
		bytesWritten--;

		if (bytesWritten < 0)
			bytesWritten = 0;

		@throw [OFWriteFailedException exceptionWithObject: self
						   requestedLength: length
						      bytesWritten: bytesWritten
							     errNo: 0];
	}
}
#endif
@end
