#include "pch.h"

#include "BKDRT11Header.h"
#include "BKParseImage.h"

#pragma warning(disable:4996)

CBKParseImage::CBKParseImage()
    = default;

CBKParseImage::~CBKParseImage()
    = default;

const char *CBKParseImage::strID_Andos = "ANDOS ";
const char *CBKParseImage::strID_DXDOS = "DX-DOS";
const char *CBKParseImage::strID_FAT12 = "FAT12   ";
const char *CBKParseImage::strID_Aodos = " AO-DOS ";
const char *CBKParseImage::strID_Nord = "NORD ";
const char *CBKParseImage::strID_Nord_Voland = "BY VOLAND"; // мультизагрузочные диски - тоже будем опознавать как норд
const char *CBKParseImage::strID_RT11 = "?BOOT-";
const char *CBKParseImage::strID_RT11Q = "?QUBOOT-";

uint16_t CBKParseImage::CalcCRC(std::span<uint8_t> buffer) const
{
	uint32_t crc = 0;

	for (auto i : buffer)
	{
		crc += i;

		if (crc & 0xFFFF0000)
		{
			// если случился перенос в 17 разряд (т.е. бит С для word)
			crc &= 0x0000FFFF; // его обнулим
			crc++; // но прибавим к сумме
		}
	}

	return static_cast<uint16_t>(crc & 0xffff);
}

const std::_tstring CBKParseImage::GetOSName(const IMAGE_TYPE t)
{
	switch (t)
	{
		case IMAGE_TYPE::ERROR_NOIMAGE:
			return { _T("Ошибка чтения") };
		case IMAGE_TYPE::ANDOS:
			return { _T("ANDOS") };
		case IMAGE_TYPE::MKDOS:
			return { _T("MKDOS") };
		case IMAGE_TYPE::AODOS:
			return { _T("AODOS") };
		case IMAGE_TYPE::NORD:
			return { _T("NORD") };
		case IMAGE_TYPE::MIKRODOS:
			return { _T("MicroDOS") };
		case IMAGE_TYPE::CSIDOS3:
			return { _T("CSIDOS3") };
		case IMAGE_TYPE::RT11:
			return { _T("RT-11") };
		case IMAGE_TYPE::NCDOS:
			return { _T("HCDOS") };
		case IMAGE_TYPE::DXDOS:
			return { _T("DXDOS") };
		case IMAGE_TYPE::OPTOK:
			return { _T("OPTOK") };
		case IMAGE_TYPE::HOLOGRAPHY:
			return { _T("Holografy OS") };
		case IMAGE_TYPE::DALE:
			return { _T("Dale OS") };
		case IMAGE_TYPE::MSDOS:
			return { _T("MS-DOS") };
		default:
			return _T("Неизвестно");
	}
}



/* определение типа образа, на выходе номер, соответствующий определённой ОС
    0 - образ не опознан, -1 - ошибка
*/
PARSE_RESULT CBKParseImage::ParseImage(const fs::path &fname, unsigned long nBaseOffset) const
{
	Sector_t Sector;
	CBKImgFile BKFloppyImgFile;
	PARSE_RESULT ret;
	ret.strName = fname;
	ret.nBaseOffset = nBaseOffset;
	ret.imageOSType = IMAGE_TYPE::ERROR_NOIMAGE; // предполагаем изначально такое

	if (!BKFloppyImgFile.Open(fname, false))
	{
		return ret;
	}

	ret.nImageSize = BKFloppyImgFile.GetFileSize(); // получим размер файла
	BKFloppyImgFile.SeekTo00();
	nBaseOffset /= BLOCK_SIZE; // смещение в LBA

	if (BKFloppyImgFile.ReadLBA(Sector.b.data(), nBaseOffset + 0, 1))
	{
		// Получим признак системного диска
		if (Sector.w[0] == 0240 /*NOP*/ && Sector.w[1] != 5 /*RESET*/)
		{
			ret.bImageBootable = true;
		}

		// Узнаем формат образа
		// проверка на формат FAT12, если надпись "FAT12"
		// однако, в некоторых андосах, нету идентификатора фс, зато есть метка андос,
		// но бывает, что и метки нету, зато параметры BPB специфические
		// проверим тут, а то потом, прямо в условии присваивания не работают из-за оптимизации проверки условий
		// условие андос 1 (метка диска)
		const bool bAC1 = (memcmp(reinterpret_cast<char *>(Sector.b.data() + 04), strID_Andos, strlen(strID_Andos)) == 0);
		// условие андос 2 (параметры диска)
		//                секторов в кластере                   число файлов в корне                                    медиадескриптор         число секторов в одной фат
		const bool bAC2 = ((Sector.b[015] == 4) && (*(reinterpret_cast<uint16_t *>(Sector.b.data() + 021)) == 112) && (Sector.b[025] == 0371) && (Sector.w[026 / 2] == 2));

		if ( // если хоть что-то выполняется, то скорее всего это FAT12, но возможны ложные срабатывания из-за мусора в этой области
		    bAC1 || bAC2 ||
		    (memcmp(reinterpret_cast<char *>(Sector.b.data() + 066), strID_FAT12, strlen(strID_FAT12)) == 0)
		)
		{
			const uint8_t nMediaDescriptor = Sector.b[025];
			const int nBootSize = Sector.w[016 / 2] * (*(reinterpret_cast<uint16_t *>(Sector.b.data() + 013)));
			std::array<uint8_t, BLOCK_SIZE> b;
			BKFloppyImgFile.ReadLBA(b.data(), nBaseOffset + nBootSize / BLOCK_SIZE, 1);

			if (b[1] == 0xff && b[0] == nMediaDescriptor) // если медиадескриптор в BPB и фат совпадают, то это точно FAT12
			{
				ret.nPartLen = *(reinterpret_cast<uint16_t *>(Sector.b.data() + 023));

				if (bAC1 || bAC2) // если одно из этих условий - то это скорее всего ANDOS
				{
					ret.imageOSType = IMAGE_TYPE::ANDOS;
				}
				else if (memcmp(reinterpret_cast<char *>(Sector.b.data() + 04), strID_DXDOS, strlen(strID_DXDOS)) == 0) // если метка диска DX-DOS, то это скорее всего DX-DOS, других вариантов пока нет.
				{
					ret.imageOSType = IMAGE_TYPE::DXDOS;
				}
				else
				{
					ret.imageOSType = IMAGE_TYPE::MSDOS; // тут только MS DOS остался
				}
			}
			else
			{
				ret.imageOSType = IMAGE_TYPE::UNKNOWN;
			}
		}
		else if (Sector.w[0400 / 2] == 0123456) // если микродос
		{
			// определяем с определённой долей вероятности систему аодос
			if (Sector.b[4] == 032 && (Sector.w[6 / 2] == 256 || Sector.w[6 / 2] == 512))
			{
				// либо расширенный формат
				ret.nPartLen = Sector.w[0466 / 2];
				ret.imageOSType = IMAGE_TYPE::AODOS;
			}
			else if (memcmp(reinterpret_cast<char *>(Sector.b.data() + 4), strID_Aodos, strlen(strID_Aodos)) == 0)
			{
				// либо по метке диска
				// аодос 1.77 так не распознаётся. его будем считать просто микродосом. т.к. там нету директорий
				ret.nPartLen = Sector.w[0466 / 2];
				ret.imageOSType = IMAGE_TYPE::AODOS;
			}
			else if (memcmp(reinterpret_cast<char *>(Sector.b.data() + 4), strID_Nord, strlen(strID_Nord)) == 0)
			{
				// предполагаем, что это метка для системы
				ret.nPartLen = Sector.w[0466 / 2];
				ret.imageOSType = IMAGE_TYPE::NORD;
			}
			else if (memcmp(reinterpret_cast<char *>(Sector.b.data() + 0xa0), strID_Nord_Voland, strlen(strID_Nord_Voland)) == 0)
			{
				// мультизагрузочные диски - тоже будем опознавать как норд
				ret.nPartLen = Sector.w[0466 / 2];
				ret.imageOSType = IMAGE_TYPE::NORD;
			}
			/* обнаружилась нехорошая привычка AODOS и возможно NORD мимикрировать под MKDOS,
			так что проверка на него - последней, при этом, т.к. MKDOSу похрен на ячейки 04..012
			то иногда, если там были признаки аодоса, мкдосный диск принимается за аодосный.
			но это лучше, чем наоборот, ибо в первом случае просто в имени директории будет глючный символ,
			а во втором - вообще все директории пропадают.
			*/
			else if (Sector.w[0402 / 2] == 051414)
			{
				ret.nPartLen = Sector.w[0466 / 2];
				ret.imageOSType = IMAGE_TYPE::MKDOS;
			}
			// тут надо найти способ как определить прочую экзотику
			else
			{
				const int nRet = AnalyzeMicrodos(&BKFloppyImgFile, nBaseOffset);

				switch (nRet)
				{
					case 0:
						ret.nPartLen = Sector.w[0466 / 2];
						ret.imageOSType = IMAGE_TYPE::MIKRODOS;
						break;

					case 1:
						ret.nPartLen = Sector.w[0466 / 2];
						ret.imageOSType = IMAGE_TYPE::AODOS;
						break;

					case 2:
						ret.nPartLen = Sector.w[0466 / 2];
						ret.imageOSType = IMAGE_TYPE::NORD;
						break;
				}
			}
		}
		// проверка на предположительно возможно НС-ДОС
		else if (Sector.w[2 / 2] == 012700 && Sector.w[4 / 2] == 0404 && Sector.w[6 / 2] == 0104012)
		{
			// нс-дос никак себя не выделяет среди других ос, поэтому будем её определять по коду.
			ret.nPartLen = 1600;
			ret.imageOSType = IMAGE_TYPE::NCDOS;
		}
		else
		{
			// дальше сложные проверки
			// выход из функций: 1 - да, это проверяемая ОС
			//                   0 - нет, это какая-то другая ОС
			//                  -1 - ошибка ввода вывода
			// проверим, а не рт11 ли у нас
			int t = IsRT11_old(&BKFloppyImgFile, nBaseOffset, &Sector);

			switch (t)
			{
				case 0:
					t = IsRT11(&BKFloppyImgFile, nBaseOffset, &Sector);

					switch (t)
					{
						case 0:
						{
							// нет, не рт11
							// проверим, а не ксидос ли у нас
							t = IsCSIDOS3(&BKFloppyImgFile, nBaseOffset, &Sector);

							switch (t)
							{
								case 0:
									// нет, не ксидос
									// проверим на ОПТОК
									t = IsOPTOK(&BKFloppyImgFile, nBaseOffset, &Sector);

									switch (t)
									{
										case 0:
											// нет, не опток
											// проверим на Holography
											t = IsHolography(&BKFloppyImgFile, nBaseOffset, &Sector);

											switch (t)
											{
												case 0:
													// нет, не Holography
													// проверим на Dale OS
													t = IsDaleOS(&BKFloppyImgFile, nBaseOffset, &Sector);

													switch (t)
													{
														case 0:
															ret.imageOSType = IMAGE_TYPE::UNKNOWN;
															break;

														case 1:
															ret.nPartLen = 1600;
															ret.imageOSType = IMAGE_TYPE::DALE;
															break;
													}

													break;

												case 1:
													ret.nPartLen = 800;
													ret.imageOSType = IMAGE_TYPE::HOLOGRAPHY;
													break;
											}

											break;

										case 1:
											ret.nPartLen = 720;
											ret.imageOSType = IMAGE_TYPE::OPTOK;
											break;
									}

									break;

								case 1:
									ret.nPartLen = Sector.w[2 / 2];
									ret.imageOSType = IMAGE_TYPE::CSIDOS3;
									break;
							}

							break;
						}

						case 1:
							ret.nPartLen = (ret.nImageSize + (BLOCK_SIZE - 1)) / BLOCK_SIZE;
							ret.imageOSType = IMAGE_TYPE::RT11;
							break;
					}

					break;

				case 1:
					ret.nPartLen = (ret.nImageSize + (BLOCK_SIZE - 1)) / BLOCK_SIZE;
					ret.imageOSType = IMAGE_TYPE::RT11;
					break;
			}
		}
	}

	BKFloppyImgFile.Close();
	return ret;
}

/*
анализатор каталога микродос, для уточнения формата.
2 - Nord
1 - Aodos
0 - Microdos
-1 - ошибка чтения файла
*/
int CBKParseImage::AnalyzeMicrodos(const CBKImgFile *f, unsigned long nBaseOffsetLBA) const
{
#pragma pack(push)
#pragma pack(1)
	struct MikrodosFileRecord
	{
		uint8_t dir_dir{ 0 };    // статус записи: бит 7 - бэд блок, остальные значения - неизвестно
		uint8_t dir_num{ 0 };    // статус записи: значения неизвестны
		std::array<uint8_t, 14> name{ 0, };  // имя файла, если начинается с 0177 - каталог. и значит это не микродос
		uint16_t start_block{ 0 };// начальный блок
		uint16_t len_blk{ 0 };   // длина в блоках, если len_blk == 0 то это тоже с большой долей вероятности каталог
		uint16_t address{ 0 };   // стартовый адрес
		uint16_t length{ 0 };    // длина, или остаток длины от длины в блоках, если размер файла > 64кб
	};
#pragma pack(pop)
	int nRet = -1;
	constexpr int nCatSize = 10; // размер каталога - одна сторона нулевой дорожки.
	auto pCatBuffer = std::vector<uint8_t>(nCatSize * BLOCK_SIZE);

	if (pCatBuffer.data())
	{
		// перемещаемся к нулевому сектору, + 0 - для наглядности
		if (f->ReadLBA(pCatBuffer.data(), nBaseOffsetLBA + 0, nCatSize))
		{
			const int nRecNum = *(reinterpret_cast<uint16_t *>(&pCatBuffer[030])); // читаем общее кол-во файлов.
			const auto pDiskCat = reinterpret_cast<MikrodosFileRecord *>(pCatBuffer.data() + 0500); // каталог диска
			int nMKDirFlag = 0;
			int nAODirFlag = 0;

			// сканируем каталог и ищем там директории
			for (int i = 0; i < nRecNum; ++i)
			{
				if (pDiskCat[i].name[0] == 0177)
				{
					nMKDirFlag++;
				}
				else if (pDiskCat[i].name[0] != 0 && pDiskCat[i].len_blk == 0)
				{
					nAODirFlag++;
				}
			}

			if (nMKDirFlag && nAODirFlag)
			{
				nRet = 0;
			}
			else if (nMKDirFlag)
			{
				nRet = 2;
			}
			else if (nAODirFlag)
			{
				nRet = 1;
			}
			else
			{
				nRet = 0;
			}
		}
	}

	return nRet;
}



// сравнение двух символов, без учёта регистра, русских букв не сравниваем, т.к. пока не нужно
// выход: true - равны
//      false - не равны
bool CBKParseImage::charcompare(uint8_t a, uint8_t b) const
{
	if ('a' <= a && a <= 'z')
	{
		a -= ' '; // делаем из маленькой буквы большую
	}

	if ('a' <= b && b <= 'z')
	{
		b -= ' '; // делаем из маленькой буквы большую
	}

	if (a == b)
	{
		return true;
	}

	return false;
}

bool CBKParseImage::substrfind(const uint8_t *bufs, size_t len_bufs, const uint8_t *buff, size_t len_buff) const
{
	size_t b_pos = 0; // позиция в буфере.
	size_t s_pos = 0; // позиция в строке, которую ищем
	size_t t_pos = 0;
	bool bOk = false;

	while (b_pos < len_bufs)
	{
		// пока есть где искать
		// ищем совпадение с первым символом
		if (charcompare(bufs[b_pos++], buff[s_pos]))
		{
			// нашли. теперь проверим на совпадения с остальными символами
			t_pos = b_pos;
			s_pos++;    // первый символ совпал, начинаем проверять со второго
			bOk = true; // флаг выхода из цикла поиска на совпадение строки

			while (s_pos < len_buff)
			{
				// пока строка не кончится
				if (!charcompare(buff[s_pos++], bufs[t_pos++]))
				{
					// если символ не совпал
					bOk = false; // то не ОК
					break;
				}
			}

			b_pos = t_pos;
			s_pos = 0; // вернёмся на начало строки

			if (bOk)
			{
				// если таки вся строка совпала
				break; // незачем дальше искать
			}
		}
	}

	// на выходе отсюда bOk == true если строка нашлась, иначе - false
	return bOk;
}

/*
Проверим, а не rt11 ли это старым методом. Поскольку непонятно как узнать, что это у нас УКНЦшный АДОС.
выход:
1 - rt11
0 - не rt11
-1 - ошибка чтения файла
*/
int CBKParseImage::IsRT11_old(const CBKImgFile *f, unsigned long nBaseOffsetLBA, Sector_t *buf) const
{
	int nRet = 0;

	// перемещаемся к нулевому сектору, + 0 - для наглядности

	if (!f->ReadLBA(buf->b.data(), nBaseOffsetLBA + 0, 1))
	{
		return -1;
	}

	// надо найти строку strID_RT11 где-то в нулевом секторе.
	size_t s_len = strlen(strID_RT11);
	size_t b_len = BLOCK_SIZE - s_len;
	bool bOk = substrfind(buf->b.data(), b_len, (uint8_t *)strID_RT11, s_len);

	if (bOk)
	{
		nRet = 1;
	}
	else
	{
		// если не нашли строку первого вида, поищем второго
		s_len = strlen(strID_RT11Q);
		b_len = BLOCK_SIZE - s_len;
		bOk = substrfind(buf->b.data(), b_len, (uint8_t *)strID_RT11Q, s_len);

		if (bOk)
		{
			nRet = 1;
		}
	}

	return nRet;
}

/*
Проверим, а не rt11 ли это. выход:
1 - rt11
0 - не rt11
-1 - ошибка чтения файла
*/
int CBKParseImage::IsRT11(const CBKImgFile *f, unsigned long nBaseOffsetLBA, Sector_t *buf) const
{
	int nRet = 0;
	Sector_t bufsh{};
	// проверку будем осуществлять методом эмуляции чтения каталога.
	const unsigned int nImgSize = f->GetFileSize() / BLOCK_SIZE; // размер образа в блоках.

	// перемещаемся к первому сектору
	if (!f->ReadLBA(buf->b.data(), nBaseOffsetLBA + 1, 1))
	{
		return -1;
	}

	unsigned int nBeginBlock = buf->w[0724 / 2]; // получим блок, с которого начинается сектор.

	// !!! фикс для АДОС
	if ((nBeginBlock == 0) || (nBeginBlock > 255))
	{
		nBeginBlock = 6;
	}

	if (nBeginBlock < nImgSize)
	{
		unsigned int nCurrentSegment = 1;   // номер текущего сегмента
		unsigned int nTotalSegments = 0;    // всего возможных сегментов в каталоге
		unsigned int nUsedSegments = -1;    // число занятых сегментов, не должно совпадать с nCount
		unsigned int nAddBytes = 0;
		unsigned int nCount = 0;            // счётчик пройденных сегментов

		while (nCurrentSegment > 0)
		{
			const int offs = ((nCurrentSegment - 1) * 2 + nBeginBlock);

			if (!f->ReadLBA(bufsh.b.data(), nBaseOffsetLBA + offs, 1)) // перемещаемся к началу текущего сегмента каталога и читаем
			{
				return -1;
			}

			const auto CurrentSegmentHeader = reinterpret_cast<const RT11SegmentHdr *>(bufsh.b.data());

			if (nTotalSegments == 0)
			{
				nTotalSegments = CurrentSegmentHeader->segments_num; // если в первый раз, запомним число сегментов.

				if (nTotalSegments > 31) // если число сегментов больше 31, то не RT-11
				{
					break;
				}

				nUsedSegments = CurrentSegmentHeader->used_segments;

				// если в первом сегменте счётчик использованных сегментов больше общего числа сегментов
				if (nUsedSegments > nTotalSegments)
				{
					break;
				}

				nAddBytes = CurrentSegmentHeader->filerecord_add_bytes;

				if (nAddBytes > 252) // если число доп слов больше размера сегмента - то это не RT-11
				{
					// т.к. в сегменте обязательно должен быть маркер конца сегмента, то в нём должны умещаться как минимум 2 записи.
					// поэтому 72*7 делим пополам
					break;
				}
			}
			else
			{
				// если не в первый - сравним старое кол-во с новым, если не равно - значит не RT-11
				if (nTotalSegments != CurrentSegmentHeader->segments_num)
				{
					break;
				}

				if (nAddBytes != CurrentSegmentHeader->filerecord_add_bytes)
				{
					break;
				}
			}

			// если начальный блок в сегменте выходит за пределы образа - то это не RT-11
			if (CurrentSegmentHeader->file_block > nImgSize)
			{
				break;
			}

			nCurrentSegment = CurrentSegmentHeader->next_segment;

			if (nCurrentSegment > 31) // если номер следующего сегмента больше 31, то не RT-11
			{
				break;
			}

			if (nCurrentSegment > nTotalSegments) // если номер следующего сегмента больше общего кол-ва сегментов, то не RT-11
			{
				break;
			}

			nCount++; // чтобы не зациклиться, считаем пройденные сегменты

			if (nCount > nTotalSegments) // если их было больше, чем общее число сегментов - то это не RT-11
			{
				break;
			}
		}

		// когда выходим из цикла
		// если соблюдаются следующие условия, то очень вероятно, что это всё-таки RT-11
		// третье условие - избыточно
		if ((nCurrentSegment == 0) && (nCount == nUsedSegments) && (nCount <= nTotalSegments))
		{
			nRet = 1;
		}
	}

	return nRet;
}

/*
Проверим, а не ксидос ли это. выход:
1 - ксидос
0 - не ксидос
-1 - ошибка чтения
*/
int CBKParseImage::IsCSIDOS3(const CBKImgFile *f, unsigned long nBaseOffsetLBA, Sector_t *buf) const
{
	// проверим, а не ксидос ли у нас
	if (f->ReadLBA(buf->b.data(), nBaseOffsetLBA + 2, 1))
	{
		if ((buf->w[8 / 2] == 0123123) || (buf->w[8 / 2] == 0123124))
		{
			// если ксидос или особый ксидос
			if ((buf->w[4 / 2] == 0123123) && (buf->w[6 / 2] == 0123123)) // если ксидос3
			{
				return 1;
			}
		}

		// про ксидос других версий ничего неизвестно
		return 0; // вообще не ксидос
	}

	return -1;
}

/*
Проверим, вдруг это ОПТОК. выход:
1 - Опток
0 - не Опток
-1 - ошибка чтения
*/
int CBKParseImage::IsOPTOK(const CBKImgFile *f, unsigned long nBaseOffsetLBA, Sector_t *buf) const
{
	// перемещаемся ко второму блоку
	if (!f->ReadLBA(buf->b.data(), nBaseOffsetLBA + 2, 1))
	{
		return -1;
	}

	// первое слово - сигнатура "**", затем 13 нулевых слов.
	uint16_t s = 0;

	for (int i = 0; i < 14; ++i)
	{
		s += buf->w[i]; // проверяем
	}

	if (s == 0x2a2a) // если всё так и есть
	{
		return 1;   // то это скорее всего Опток
	}

	return 0;
}

int CBKParseImage::IsHolography(const CBKImgFile *f, unsigned long nBaseOffsetLBA, Sector_t *buf) const
{
	// читаем бут сектор
	if (!f->ReadLBA(buf->b.data(), nBaseOffsetLBA + 0, 1))
	{
		return -1;
	}

	// будем проверять по КС, потому что существует всего 2 дискеты с этой, так сказать, ОС
	const uint16_t crc = CalcCRC(std::span<uint8_t>(buf->b.data(), 0162));
	return (crc == 8384);
}

int CBKParseImage::IsDaleOS(const CBKImgFile *f, unsigned long nBaseOffsetLBA, Sector_t *buf) const
{
	static const char *OSSignature = "Da";

	// читаем бут сектор
	if (!f->ReadLBA(buf->b.data(), nBaseOffsetLBA + 0, 1))
	{
		return -1;
	}

	if (buf->w[1] == 0240)
	{
		// если второе слово у загрузчика тоже NOP
		// читаем блок 1
		if (!f->ReadLBA(buf->b.data(), nBaseOffsetLBA + 1, 1))
		{
			return -1;
		}

		if (buf->w[0] == *(reinterpret_cast<const uint16_t *>(&OSSignature[0])))
		{
			// если первое слово второго блока - сигнатура ОС
			// читаем блок 2
			if (!f->ReadLBA(buf->b.data(), nBaseOffsetLBA + 2, 1))
			{
				return -1;
			}

			// если последнее слово второго блока - сигнатура каталога
			if (buf->w[0776 / 2] == 041125)
			{
				return 1; // то это оно
			}
		}
	}

	return 0; // иначе - это не оно
}

