/*
 * @(#CDWverify.cpp
 *
 * This file is part of webCDwriter - Network CD Writing.
 *
 * Copyright (C) 2001-2004 Jörg P. M. Haeger
 *
 * webCDwriter is free software. See CDWserver.cpp for details.
 *
 * Author: Jörg P. M. Haeger, 28.02.2001
 */

#define _FILE_OFFSET_BITS 64

#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef FreeBSD
#include <sys/param.h>
#endif
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>
#include "Error.h"
#include "Version.h"

typedef unsigned short uint16;
typedef unsigned long uint32;
typedef unsigned long long uint64;

/**
 * Compare a directory or an image file to the contents of a CD
 *
 * @version 20041029
 * @author Jörg P. M. Haeger
 */
class Verify {
public:
	static bool cancel;

private:
	char *buf1, *buf2;
	uint64 bytesCounter, bufferSize;
	int checkTooLong;
	int MBytes0;
	uint64 time0;
	int newLine;
	int numOfFiles, numOfErrors;
	int existing, missing;
	int maxSpeed;

public:
	Verify(int maxSpeedArg) {
		bufferSize = 64 * 1024;
		buf1 = new char[bufferSize];
		buf2 = new char[bufferSize];
		bytesCounter = 0;
		checkTooLong = 1;
		MBytes0 = 0;
		time0 = 0;
		newLine = 1;
		numOfFiles = numOfErrors = 0;
		maxSpeed = maxSpeedArg;
	}

	~Verify() {
		delete[] buf1;
		delete[] buf2;
	}

	void compare(const char *orig, const char *copy);
	void compareFile(const char *orig, const char *copy, uint64 size = 0);
	void compareTracks(const char *orig, const char *copy);

	Error *createActionError(const char *action) {
		return new Error("%s, action: %s",
			strerror(errno), action);
	}

	int getExisting() { return existing; }
	int getMissing() { return missing; }
	int getNumOfErrors() { return numOfErrors; }
	int getNumOfFiles() { return numOfFiles; }

	static uint64 getTime() {
		struct timeval tv;
		gettimeofday(&tv, NULL);
		uint64 t = tv.tv_sec;
		return 1000 * 1000 * t + tv.tv_usec;
	}

	public: static int mount(const char *specialfile, const char *dir) {
#ifdef FreeBSD
//		return ::mount(specialfile, dir, MNT_RDONLY, NULL);
		char command[1024];
		snprintf(command, sizeof command,
			"mount -t cd9660 -o ro %s %s",
			specialfile, dir);
		return system(command);
#else
		return ::mount(specialfile, dir, "iso9660", MS_RDONLY, NULL);
#endif
	}

	/**
	 * Print an ID of the ISO 9660 primary volume descriptor.
	 */
	static void printID(unsigned char *buf,
			const char *name, int start, int length) {
		int i = start + length - 1;
		while (i >= start && buf[i] <= 32)
			buf[i--] = 0;

		printf("%s = %s\n", name, &buf[start]);
	}

	static uint64 readISOsize(const char *imageFile, int sector0 = 0);

	public: void scan(const char *orig, const char *copy) {
		existing = missing = 0;
		scan2(orig, copy);
	}

	private: void scan2(const char *orig, const char *copy) {
		struct stat statBuf;
		if (stat(orig, &statBuf) != 0)
			return;

		if (!S_ISDIR(statBuf.st_mode)) {
			if (stat(copy, &statBuf) == 0)
				existing++;
			else
				missing++;
			return;
		}

		DIR *dir = opendir(orig);
		if (dir == NULL)
			return;

		struct dirent *dirEntry;
		while ((dirEntry = readdir(dir)) != NULL) {
			if (strcmp(dirEntry->d_name, ".") == 0
				|| strcmp(dirEntry->d_name, "..") == 0)
				continue;

			char tmpOrig[strlen(orig) + 1
				+ strlen(dirEntry->d_name) + 1];
			sprintf(tmpOrig, "%s/%s",
				orig, dirEntry->d_name);

			char tmpCopy[strlen(copy) + 1
				+ strlen(dirEntry->d_name) + 1];
			sprintf(tmpCopy, "%s/%s",
				copy, dirEntry->d_name);

			scan2(tmpOrig, tmpCopy);
			if (cancel)
				break;
		}

		closedir(dir);
	}

	public: static int unmount(const char *dir) {
		seteuid(0);
/*		char command[1024];
		snprintf(command, sizeof command,
			"umount %s", dir);
		return system(command);
*/
		int res;
#ifdef FreeBSD
		res = ::unmount(dir, MNT_FORCE);
#else
		res = ::umount(dir);
#endif
		if (res != 0)
			printf("\nCannot unmount %s: %s\n", dir, strerror(errno));
		seteuid(getuid());
		return res;
	}

	public: static void unmountDevice(const char *device) {
		FILE *in = fopen("/proc/mounts", "r");
		if (in == NULL)
			return;

		while (true) {
			char line[1024 + 1];
			if (fgets(line, sizeof line, in) == NULL)
				break;
			char dev[sizeof line], dir[sizeof line];
			if (sscanf(line, "%s %s", dev, dir) != 2)
				continue;
			if (strcmp(dev, device) == 0) {
				printf("Unmounting %s...", dir);
				fflush(stdout);
				unmount(dir);
				printf(" done\n");
			}
		}

		fclose(in);
	}

	class Speedometer {
		uint64 t0, t0Next;
		uint64 counter, counterNext;
		float speed;

		public: Speedometer() {
			t0 = t0Next = getTime();
			counter = counterNext = 0;
			speed = 0.0;
		}

		private: void calibrate(uint64 t) {
			if (t - t0Next > 1 * 1000 * 1000) {
				t0 = t0Next;
				counter = counterNext;
				t0Next = t;
				counterNext = 0;
			}
		}

		public: float getSpeed() {
			uint64 t = getTime();
			calibrate(t);
			if (t - t0 > 1 * 1000 * 1000)
				speed = 1000 * 1000 * counter
					/ (t - t0) / (150.0 * 1024);

			return speed;
		}

		public: void inc(int n) {
			counter += n;
			counterNext += n;
		}

		public: void slowDown(int maxSpeed) {
			while (true) {
				uint64 t = getTime();
				calibrate(t);
				uint64 limit = (t - t0) * (maxSpeed * 150 * 1024)
					/ 1000 / 1000;
				if (counter <= limit)
					break;
				usleep(1000 * 1000 * (counter - limit)
					/ (maxSpeed * 150 * 1024));
			}
		}
	};

	private: Speedometer speedometer;
};

void Verify::compare(const char *orig, const char *copy) {
	struct stat statBuf;
	if (stat(orig, &statBuf) != 0)
		return;

	if (!S_ISDIR(statBuf.st_mode)) {
		compareFile(orig, copy);
		return;
	}

	DIR *dir = opendir(orig);
	if (dir == NULL)
		return;

	struct dirent *dirEntry;
	while ((dirEntry = readdir(dir)) != NULL) {
		if (strcmp(dirEntry->d_name, ".") == 0
			|| strcmp(dirEntry->d_name, "..") == 0)
			continue;

		char tmpOrig[strlen(orig) + 1
			+ strlen(dirEntry->d_name) + 1];
		sprintf(tmpOrig, "%s/%s",
			orig, dirEntry->d_name);

		char tmpCopy[strlen(copy) + 1
			+ strlen(dirEntry->d_name) + 1];
		sprintf(tmpCopy, "%s/%s",
			copy, dirEntry->d_name);

		compare(tmpOrig, tmpCopy);
		if (cancel)
			break;
	}

	closedir(dir);
}

void Verify::compareFile(const char *orig, const char *copy, uint64 size) {
	int in1 = -1, in2 = -1;

	try {
		struct stat statBuf;
		if (stat(copy, &statBuf) != 0)
			throw new Error("missing");

		in1 = open(orig, O_RDONLY);
		if (in1 == -1)
			throw createActionError("open original");

		in2 = open(copy, O_RDONLY);
		if (in2 == -1)
			throw createActionError("open copy");

		uint64 perFileBytesCounter = 0;
		for (int i = 0; !cancel; ) {
			{
				char cancelFile[1024];
				sprintf(cancelFile, "/tmp/CDWcancel-%d", getpid());
				struct stat statBuf;
				if (stat(cancelFile, &statBuf) == 0) {
					cancel = true;
					break;
				}
			}

			int MBytes = bytesCounter / 1024 / 1024;
			if (MBytes > MBytes0 && getTime() > time0 + 1000 * 1000) {
				if (!newLine)
					printf("\r");
				float speed = speedometer.getSpeed();
				printf("%d MB, %d files, %d errors, %4.1fx (%4.0f KB/s)",
					MBytes, numOfFiles, numOfErrors,
					speed, 150.0 * speed);
				fflush(stdout);
				MBytes0 = MBytes;
				time0 = getTime();
				newLine = 0;
			}

			int nMax = bufferSize;
			if (size > 0
				&& bytesCounter + nMax > size)
				// just compare the first size bytes
				nMax = size - bytesCounter;

			int n = 0;
			while (n < nMax) {
				int blockSize = nMax - n;
				if (blockSize > 2 * 1024)
					blockSize = 2 * 1024;
				int l = read(in1, &buf1[n], blockSize);
				if (l == -1)
					if (errno == EINTR)
						continue;
					else
						throw createActionError("read original");
				if (l == 0)
					break;
				n += l;
			}
			if (n == 0)
				if (checkTooLong
					&& read(in2, buf2, 1) > 0)
					throw new Error(
						"too long (> %llu bytes)", bytesCounter);
				else
					break;

			int m = 0;
			while (1) {
				int l = read(in2, &buf2[m], n - m);
				if (l == -1 && errno != EINTR)
					throw new Error("%s while reading byte %llu",
						strerror(errno), bytesCounter + m);
				if (l == 0)
					break;
				m += l;
			}

			if (memcmp(buf1, buf2, m) != 0) {
				int i = 0;
				while (buf1[i] == buf2[i])
					i++;
				throw new Error(
					"byte %llu differs", perFileBytesCounter + i + 1);
			}

			if (m < n)
				throw new Error(
					"too short (%llu bytes)", bytesCounter + m);

			bytesCounter += n;
			perFileBytesCounter += n;
			speedometer.inc(n);
			speedometer.slowDown(maxSpeed);
		}
	}
	catch (Error *err) {
		if (!newLine) {
			printf("\n");
			newLine = 1;
		}
		const char *name = copy;
		if (strstr(name, "/tmp/cdrom") == name)
			name += 4 + 6 + 5;
		printf("error: %s, file: %s\n", err->toString(), name);
		delete err;
		numOfErrors++;
	}
	numOfFiles++;

	if (in2 != -1)
		close(in2);
	if (in1 != -1)
		close(in1);
}

void Verify::compareTracks(const char *orig, const char *copy) {
	checkTooLong = 0;
	compareFile(orig, copy, readISOsize(orig));
}

uint64 Verify::readISOsize(const char *imageFile, int sector0) {
	uint64 size = 0;
	struct stat statBuf;
	if (stat(imageFile, &statBuf) == 0)
		size = statBuf.st_size;

	FILE *in = fopen(imageFile, "r");
	if (in != NULL) {
		fseek(in, 2048 * (sector0 + 16), SEEK_SET);
		unsigned char buf[2048];
		for (int i = 0; i < 32; i++) {
			int n = fread(buf, 1, sizeof buf, in);
			if (n < sizeof buf || buf[0] == 255)
				break;
			if (buf[0] == 1) {
				uint64 size = *(uint32 *)&buf[80];
				size *= *(uint16 *)&buf[128];
				printf("ISO image size = %llu\n", size);
				printID(buf, "volume ID", 40, 32);
				printID(buf, "publisher ID", 318, 128);
				printID(buf, "preparer ID", 446, 128);
			}
		}
		fclose(in);
	}
	return size;
}

bool Verify::cancel = false;

void sigTermHandler(int sig) {
	Verify::cancel = true;
	printf("\nCancelled\n");
	fflush(stdout);
}

int main(int argc, char *args[]) {
	printf("CDWverify %s (part of %s)\n", version, project);
	fflush(stdout);

	signal(SIGTERM, sigTermHandler);
	seteuid(getuid());

	if (argc == 4 && strcmp(args[1], "-readISOHeader") == 0) {
		int n = 0;
		sscanf(args[2], "%d", &n);
		Verify::readISOsize(args[3], n);
		return 0;
	}

	int invalidArg = 0;
	int maxSpeed = 64;

	for (int i = 1; i < argc - 2; i++)
		if (sscanf(args[i], "maxSpeed=%d", &maxSpeed) == 1)
			;
		else
			invalidArg = 1;

	if (argc < 3 || invalidArg) {
		printf("%s compares a directory "
			"or an image file to the contents of a CD\n\n"
			"Usage:\n"
			"\t\t%s [options] directory rawDevice\n"
			"\t\t%s [options] imageFile rawDevice\n"
			"\t\t%s -readISOHeader sector0 rawDevice\n\n"
			"Options:\n"
			"\t\tmaxSpeed=%dx"
				"\tdo not read faster than %dx (default)\n\n",
			args[0], args[0], args[0], args[0], maxSpeed, maxSpeed);
		return 1;
	}

	const char *orig = args[argc - 2];
	const char *copy = args[argc - 1];

	struct stat statBuf;
	if (stat(orig, &statBuf) != 0) {
		printf("Cannot stat %s (%s)\n", orig, strerror(errno));
		return 1;
	}

	// make sure the device isn't already mounted
	Verify::unmountDevice(copy);

/*	const char *readcd = "/usr/bin/readcd";
	const char *dev = "0,0,0";
	if (stat(readcd, &statBuf) != 0)
		readcd = "/usr/local/bin/readcd";
	if (stat(readcd, &statBuf) == 0) {
		printf("Trying to set drive to %dx speed...", maxSpeed);
		fflush(stdout);
		char cmd[4 * 1024];
		sprintf(cmd, "%s dev=%s speed=%d sectors=0-1 f=/tmp/cdrom%05dS0",
			readcd, dev, maxSpeed, getpid());
		if (system(cmd) == 0)
			printf(" done\n");
		else
			printf(" failed\n");
	}
*/

	Verify verify(maxSpeed);
	if (S_ISDIR(statBuf.st_mode)) {
		char dir[1024];
		sprintf(dir, "/tmp/cdrom%05d", getpid());
		mkdir(dir, S_IRWXU);

		// mounting the CD/DVD
		seteuid(0);
		for (int i = 1; ; i++) {
			if (i > 1)
				printf("\r");
			printf("Mounting the CD/DVD on %s...", dir);
			fflush(stdout);
			if (i > 1) {
				if (i == 2)
					printf(" 2nd try");
				else
				if (i == 3)
					printf(" 3rd try");
				else
				if (i == 5) {
					printf(" error: %s\n", strerror(errno));
					return 1;
				}
				else
				if (i > 101) {
					if (i > 102)
						printf("   ");
					break;
				}
				else
					printf(" %dth try", i);
				fflush(stdout);
				sleep(2);
			}

			if (Verify::mount(copy, dir) == 0)
				i += 100;
		}
		seteuid(getuid());
		printf(" done\n");

		printf("Scanning the directories...");
		fflush(stdout);
		verify.scan(orig, dir);
		printf(" %d of %d files found\n",
			verify.getExisting(),
			verify.getExisting() + verify.getMissing());
		fflush(stdout);
		if (verify.getExisting() == 0
				&& verify.getMissing() > 0) {
			Verify::unmount(dir);
			return 1;
		}

		printf("Comparing the files...\n");
		fflush(stdout);
		verify.compare(orig, dir);
		Verify::unmount(dir);
		printf("\n%d files are identical, %d files differ\n",
			verify.getNumOfFiles() - verify.getNumOfErrors(),
			verify.getNumOfErrors());
		if (verify.getNumOfErrors() == 0)
			return 0;
		else
			return 1;
	}
	else {
		// opening the raw device of the CD/DVD
		for (int i = 1; ; i++) {
			if (i > 1)
				printf("\r");
			printf("Opening %s...", copy);
			fflush(stdout);
			if (i > 1) {
				if (i == 2)
					printf(" 2nd try");
				else
				if (i == 3)
					printf(" 3rd try");
				else
				if (i == 5) {
					printf(" error: %s\n", strerror(errno));
					return 1;
				}
				else
				if (i > 101) {
					if (i > 102)
						printf("   ");
					break;
				}
				else
					printf(" %dth try", i);
				fflush(stdout);
				sleep(2);
			}

			if (open(copy, O_RDONLY) != -1)
				i += 100;
		}
		printf(" done\n");

		verify.compareTracks(orig, copy);
		printf("\n");
		if (verify.getNumOfErrors() == 0) {
			printf("OK\n");
			return 0;
		}
		else
			return 1;
	}
}
