/*
 * btxml.c
 *
 * Creates a backup of the Nokia 6310i via bluetooth. Outputs data to
 * stdout in xml format. This is plug'n'play, no need to enter any data
 * on the host or phone side.
 * Just saw that it somehow works for Ericsson T610 and T68i, too. They
 * don't support text mode sms... :-(
 *
 * Copyright (C) 2004 by Andreas Oberritter
 *
 * Homepage: http://www.saftware.de/
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * rev 0.3 (2004/02/14)
 * - ATE0 to disable echo on ericsson
 *
 * rev 0.2 (2004/02/14)
 * - set auth & encrypt to off
 *
 * rev 0.1 (2004/02/12)
 * - initial release
 *
 * TODO: pdu parser for sms
 */

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <bluetooth/rfcomm.h>

/******************************************************************************/

#define CACHE_TIMEOUT	60
#define CACHE_SIZE_MAX	0x10000

struct cache_item {
	bdaddr_t addr;
	time_t time;
	bool valid;
};

static enum {
	MANUF_UNKNOWN,
	MANUF_ERICSSON,
	MANUF_NOKIA,
} manuf;

static struct cache_item cache[CACHE_SIZE_MAX];
static size_t cache_size;

/******************************************************************************/

static void bt_cache_add(bdaddr_t *addr)
{
	struct cache_item *item;

	for (item = &cache[0]; item < &cache[CACHE_SIZE_MAX]; item++) {
		if (item->valid)
			continue;
		bacpy(&item->addr, addr);
		item->time = time(NULL);
		item->valid = true;
		cache_size++;
	}
}

/******************************************************************************/

static void bt_cache_clear(void)
{
	struct cache_item *item;
	time_t now;
	size_t removed = 0;
	size_t count = 0;

	now = time(NULL);

	for (item = &cache[0]; item < &cache[CACHE_SIZE_MAX]; item++) {
		if (count == cache_size)
			break;
		if (!item->valid)
			continue;
		count++;
		if (now - item->time < CACHE_TIMEOUT)
			continue;
		item->valid = false;
		removed++;
	}

	cache_size -= removed;
}

/******************************************************************************/

static bool bt_cache_find(bdaddr_t *addr)
{
	struct cache_item *item;
	size_t count = 0;

	for (item = &cache[0]; item < &cache[CACHE_SIZE_MAX]; item++) {
		if (count == cache_size)
			break;
		if (!item->valid)
			continue;
		if (!bacmp(&item->addr, addr))
			return true;
		count++;
	}

	return false;
}

/******************************************************************************/

static void at_send(FILE *fp, const char *fmt, va_list ap)
{
	fprintf(fp, "AT");
	vfprintf(fp, fmt, ap);
	fprintf(fp, "\r\n");
}

/******************************************************************************/

static ssize_t at_recv(FILE *fp, char *dest)
{
	char *line = NULL;
	size_t len = 0;
	size_t ret = 0;
	ssize_t read;

	while ((read = getline(&line, &len, fp)) != -1) {
		if (!read)
			continue;
		if ((line[read - 1] == '\n') && (--read == 0))
			continue;
		if ((line[read - 1] == '\r') && (--read == 0))
			continue;

		line[read++] = '\0';

		if (!strcmp(line, "OK"))
			break;
		if ((!strcmp(line, "ERROR")) ||
			(!strncmp(line, "+CME ERROR:", 10)) ||
			(!strncmp(line, "+CMS ERROR:", 10))) {
			ret = -1;
			break;
		}

		if (dest) {
			if (ret)
				dest[-1] = ' ';
			memcpy(dest, line, read);
			dest += read;
		}

		ret++;
	}

	free(line);

	return ret;
}

/******************************************************************************/

static int at_cmd(FILE *fp, char *buf, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	at_send(fp, fmt, ap);
	va_end(ap);

	return at_recv(fp, buf);
}

/******************************************************************************/

static int at_parse_phonebook_entry(FILE *fp, size_t num)
{
	char buf[0x1000], *ptr, *start, *end;
	char *number_ptr, *name_ptr;
	ssize_t number_len, name_len;

	if (at_cmd(fp, buf, "+CPBR=%u", num) != 1)
		return -1;

	ptr = buf;
	if (!strncmp(ptr, "+CPBR: ", 7))
		ptr += 7;

	puts("\t\t<contact>");

	if (((start = strchr(ptr, '\"'))) && (end = strchr(++start, '\"'))) {
		number_ptr = start;
		number_len = end - start;
	}
	else {
		number_ptr = NULL;
	}

	if (((start = strchr(++end, '\"'))) &&
		(end = strrchr(&ptr[strlen(ptr) - 1], '\"'))) {
		name_ptr = ++start;
		name_len = end - start;
	}
	else {
		name_ptr = NULL;
	}

	if ((number_ptr) && (name_ptr)) {
		printf("\t\t\t<name>%.*s</name>\n", name_len, name_ptr);
		printf("\t\t\t<number>%.*s</number>\n", number_len, number_ptr);
	}
	else {
		printf("\t\t\t<raw>%s</raw>\n", ptr);
	}

	puts("\t\t</contact>");

	fflush(stdout);
	return 0;
}

/******************************************************************************/

static int at_parse_phonebook(FILE *fp, const char *name)
{
	char buf[0x1000], *ptr;
	size_t start, end, used, size, i, found;

	if (at_cmd(fp, NULL, "+CPBS=%s", name) != 0)
		return -1;

	if ((manuf == MANUF_NOKIA) || (manuf == MANUF_UNKNOWN)) {
		if (at_cmd(fp, buf, "+CPBS?") != 1)
			return -1;
		if (!(ptr = strchr(buf, ',')))
			return -1;
		if (sscanf(++ptr, "%u,%u", &used, &size) != 2)
			return -1;
		if (!used)
			return -1;
	}

	if (at_cmd(fp, buf, "+CPBR=?") != 1)
		return -1;

	if (sscanf(buf, "+CPBR: (%u-%u)", &start, &end) != 2)
		return -1;

	if (manuf == MANUF_ERICSSON) {
		// FIXME
		used = size = end;
	}

	printf("\t<phonebook name=%s size=\"%u\">\n", name, size);

	for (i = start, found = 0; i <= end && found < used; i++)
		if (!at_parse_phonebook_entry(fp, i))
			found++;

	printf("\t</phonebook>\n");

	fflush(stdout);
	return 0;
}

/******************************************************************************/

static int at_parse_brackets(FILE *fp, char *buf, int (*cb)(FILE*, const char*))
{
	char *start, *end, *str;

	if ((!(start = strchr(buf, '('))) || (!(end = strchr(++start, ')'))))
		return -1;

	*end = '\0';

	while ((str = strsep(&start, ",")))
		cb(fp, str);

	return 0;
}

/******************************************************************************/

static int at_parse_phonebook_list(FILE *fp)
{
	char buf[0x1000];

	if (at_cmd(fp, buf, "+CPBS=?") != 1)
		return -1;

	return at_parse_brackets(fp, buf, at_parse_phonebook);
}

/******************************************************************************/

static int at_parse_manufacturer_identification(FILE *fp)
{
	char buf[0x1000];

	if (at_cmd(fp, buf, "+GMI") < 1)
		return -1;

	if (strstr(buf, "Ericsson"))
		manuf = MANUF_ERICSSON;
	else if (strstr(buf, "Nokia"))
		manuf = MANUF_NOKIA;
	else
		manuf = MANUF_UNKNOWN;

	printf("\t<manufacturer>%s</manufacturer>\n", buf);
	return 0;
}

/******************************************************************************/

static int at_parse_model_identification(FILE *fp)
{
	char buf[0x1000];

	if (at_cmd(fp, buf, "+GMM") < 1)
		return -1;

	printf("\t<model>%s</model>\n", buf);
	return 0;
}

/******************************************************************************/

static int at_parse_revision_identification(FILE *fp)
{
	char buf[0x1000];

	if (at_cmd(fp, buf, "+GMR") < 1)
		return -1;

	printf("\t<revision>%s</revision>\n", buf);
	return 0;
}

/******************************************************************************/

static int at_parse_psn_identification(FILE *fp)
{
	char buf[0x1000];

	if (at_cmd(fp, buf, "+GSN") < 1)
		return -1;

	printf("\t<imei>%s</imei>\n", buf);
	return 0;
}

/******************************************************************************/

static int at_parse_identification(FILE *fp)
{
	at_parse_manufacturer_identification(fp);
	at_parse_model_identification(fp);
	at_parse_revision_identification(fp);
	at_parse_psn_identification(fp);

	fflush(stdout);
	return 0;
}

/******************************************************************************/

static int at_parse_message(FILE *fp, size_t num)
{
	char buf[0x1000], *ptr;

	if (at_cmd(fp, buf, "+CMGR=%u", num) < 1)
		return -1;

	ptr = buf;
	if (!strncmp(ptr, "+CMGR: ", 7))
		ptr += 7;

	printf("\t\t<message>%s</message>\n", ptr);

	fflush(stdout);
	return 0;
}

/******************************************************************************/

static int at_parse_message_storage(FILE *fp, const char *name)
{
	char buf[0x1000];
	size_t i, msgnum, size;

	if (at_cmd(fp, buf, "+CPMS=%s", name) != 1)
		return -1;

	if (sscanf(buf, "+CPMS: %u,%u", &msgnum, &size) != 2)
		return -1;

	printf("\t<msgstorage name=%s>\n", name);

	for (i = 1; i <= msgnum; i++)
		at_parse_message(fp, i);

	puts("\t</msgstorage>");

	return 0;
}

/******************************************************************************/

static int at_parse_message_list(FILE *fp)
{
	char buf[0x1000];

	if (at_cmd(fp, NULL, "+CMGF=1") != 0)
		return -1;

	if (at_cmd(fp, buf, "+CPMS=?") != 1)
		return -1;

	return at_parse_brackets(fp, buf, at_parse_message_storage);
}

static int at_disable_echo(FILE *fp)
{
	at_cmd(fp, NULL, "E0");
}

/******************************************************************************/

static int bt_rfcomm_config(int fd)
{
	struct termios t;
	int ret;

	if ((ret = tcgetattr(fd, &t)))
		perror("tcgetattr");
	else {
		t.c_iflag = IGNBRK;
		t.c_oflag = 0;
		t.c_cflag = CLOCAL | CREAD | CS8 | B115200;
		t.c_lflag = 0;
		t.c_line = 0;
		t.c_ispeed = B115200;
		t.c_ospeed = B115200;

		if ((ret = tcsetattr(fd, TCSADRAIN, &t)))
			perror("tcsetattr");
	}

	return ret;
}

/******************************************************************************/

static int bt_rfcomm(int dev_id)
{
	static const char *rfcomm_fmt = "/dev/bluetooth/rfcomm/%u";
	char filename[FILENAME_MAX];
	FILE *fp = NULL;

	snprintf(filename, FILENAME_MAX, rfcomm_fmt, dev_id);

	if (!(fp = fopen(filename, "r+"))) {
		perror(filename);
		return -1;
	}

	if (bt_rfcomm_config(fileno(fp)) == 0) {
		at_disable_echo(fp);
		at_parse_identification(fp);
		at_parse_phonebook_list(fp);
		at_parse_message_list(fp);
		sleep(1);
	}

	fclose(fp);

	return 0;
}

/******************************************************************************/

static int bt_release(int sock, int dev_id)
{
	struct rfcomm_dev_req req;
	int ret;

	req.dev_id = dev_id;
	req.flags = 0;
	bacpy(&req.src, BDADDR_ANY);
	bacpy(&req.dst, BDADDR_ANY);
	req.channel = 0;

	if ((ret = ioctl(sock, RFCOMMRELEASEDEV, &req)))
		perror("RFCOMMRELEASEDEV");

	return ret;
}

/******************************************************************************/

static int bt_bind(int sock, int dev_id, bdaddr_t *bdaddr)
{
	struct rfcomm_dev_req req;
	int ret;

	req.dev_id = dev_id;
	req.flags = 0;
	bacpy(&req.src, BDADDR_ANY);
	bacpy(&req.dst, bdaddr);
	req.channel = 17;	// 18

	if (ioctl(sock, RFCOMMCREATEDEV, &req) == 0)
		return 0;

	if (errno != EADDRINUSE)
		perror("RFCOMMCREATEDEV");
	else if ((ret = bt_release(sock, dev_id)))
		;
	else if ((ret = ioctl(sock, RFCOMMCREATEDEV, &req)))
		perror("RFCOMMCREATEDEV");

	return ret;
}

/******************************************************************************/

static int scan(int dev_id, int s)
{
	inquiry_info *info = NULL;
	int max, len, flags;
	char addr[18], name[256];
	int i, sock;

	len = 4;
	max = 100;
	flags = IREQ_CACHE_FLUSH;

	max = hci_inquiry(dev_id, len, max, NULL, &info, flags);
	if (max == -1) {
		perror("hci_inquiry");
		return -1;
	}

	for (i = 0; i < max; i++) {
		if (bt_cache_find(&info[i].bdaddr))
			continue;

		sock = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_RFCOMM);
		if (sock == -1) {
			perror("socket");
			continue;
		}

		if (bt_bind(sock, dev_id, &info[i].bdaddr))
			continue;

		if (hci_read_remote_name(s, &info[i].bdaddr, sizeof(name), name, 2))
			name[0] = '\0';

		ba2str(&info[i].bdaddr, addr);
		printf("<phone btaddr=\"%s\" name=\"%s\">\n", addr, name);
		fflush(stdout);

		bt_rfcomm(dev_id);
		bt_release(sock, dev_id);
		close(sock);

		puts("</phone>");
		fflush(stdout);

		bt_cache_add(&info[i].bdaddr);
	}

	free(info);

	return 0;
}

/******************************************************************************/

static bool bt_set_auth(int dev_id, int s)
{
	struct hci_dev_req dr;
	int ret;

	dr.dev_id = dev_id;
	dr.dev_opt = AUTH_DISABLED;

	if ((ret = ioctl(s, HCISETAUTH, &dr)))
		perror("HCISETAUTH");

	return (ret == 0);
}

/******************************************************************************/

static bool bt_set_encrypt(int dev_id, int s)
{
	struct hci_dev_req dr;
	int ret;

	dr.dev_id = dev_id;
	dr.dev_opt = ENCRYPT_DISABLED;

	if ((ret = ioctl(s, HCISETENCRYPT, &dr)))
		perror("HCISETENCRYPT");

	return (ret == 0);
}

/******************************************************************************/

static bool bt_set_name(int s)
{
	change_local_name_cp cp;
	int ret;

	memset(cp.name, ' ', CHANGE_LOCAL_NAME_CP_SIZE);

	ret = hci_send_cmd(s, OGF_HOST_CTL, OCF_CHANGE_LOCAL_NAME,
			CHANGE_LOCAL_NAME_CP_SIZE, (void *) &cp);

	if (ret == -1)
		perror("OCF_CHANGE_LOCAL_NAME");

	return (ret == 0);
}

/******************************************************************************/

static bool bt_configure(int dev_id, int s)
{
	return (bt_set_auth(dev_id, s) &&
		bt_set_encrypt(dev_id, s) &&
		bt_set_name(s));
}

/******************************************************************************/

int main(void)
{
	int dev_id, s;
	time_t now, prev;

	if ((dev_id = hci_get_route(NULL)) == -1) {
		perror("hci_get_route");
		return EXIT_FAILURE;
	}

	if ((s = hci_open_dev(dev_id)) == -1) {
		perror("hci_open_dev");
		return -1;
	}

	bt_configure(dev_id, s);

	prev = time(NULL);

	puts("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");

	while (1) {
		scan(dev_id, s);
		now = time(NULL);
		if (now != prev) {
			bt_cache_clear();
			prev = now;
		}
		else {
			usleep(0);
		}
	}

	if (hci_close_dev(s))
		perror("hci_close_dev");

	return EXIT_SUCCESS;
}

