Как стать автором
Обновить

Как я парсил БД C-Tree, разработанную 34 года назад

Время на прочтение6 мин
Количество просмотров17K
image

Прилетела мне недавно задача дополнить функционал одной довольно старой програмки (исходного кода программы нет). По сути нужно было просто сканить периодически БД, анализировать информацию и на основе этого совершать рассылки. Вся сложность оказалась в том, что приложение работает с БД c-tree, написанной аж в 1984 году.

Порывшись на сайте производителя данной БД нашёл некий odbc драйвер, однако у меня никак не получалось его подключить. Многочисленные гугления так же не помогли нормально сконнектиться с базой и доставать данные. Позже было решено связаться с техподдержкой и попросить помощи у разработчиков данной базы, однако ребята честно признались что уже прошло 34 года, всё поменялось 100500 раз, нормальных драйверов для подключения на такое старьё у них нет и небось уже тех программистов в живых тоже нету, которые писали сие чудо.
Порывшись в файлах БД и изучив структуру, я понял, что каждая таблица в БД сохраняется в два файла с расширением *.dat и *.idx. Файл idx хранит информацию по id, индексам и т.д. для более быстрого поиска информации в базе. Файл dat содержит саму информацию, которая хранится в табличках.

Решено было парсить эти файлики самостоятельно и как-то добывать эту информацию. В качестве языка использовался Go, т.к. весь остальной проект написан на нём.

Я опишу только самый интересные моменты, с которыми я столкнулся при обработке данных, которые в основном затрагивают не сам язык Go, а именно алгоритмы вычислений и магию математики.

С ходу понять что где в файлах нереально, так как нормальной кодировки там нет, данные записываются в какие-то определённые биты и изначально больше похоже на мусорку данных.

image

Вдохновляющую фразочку я получил от клиента с небольшим описанием, почему так:”**** was a MS-DOS application running on 8086 processors and memory was scarce”.

Логика работы была таковой: назначаю в программе какие-то новые данные, смотрю какие файлы изменялись и затем пытаюсь выщемить биты данных, которые поменяли значения.
В процессе разработки я понял следующие важные правила, которые мне помогли разобраться быстрее:

  • начало и конец файла (размер всегда разный) зарезервированы под табличные данные
  • длина строки в таблице занимает всегда одно и то же количество байт
  • вычисления проводятся в 256ричной системе

Время в расписании


В приложении создаётся расписание с интервалом в 15 минут (Например 12:15 – 14:45). Потыкав в разное время, нашёл область памяти, которая отвечает за часы. Я считываю данные из файла побайтово. Для времени используется 2 байта данных.

Каждый байт может содержать значение от 0 до 255. При добавлении в расписание 15 минут в первый байт добавляется число 15. Плюсуем ещё 15 минут и в байте уже число 30 и т.д.
Например у нас следующие значения:

[0] [245]

Как только значение превышает 255, в следующий байт добавляется 1, а в текущий записывается остаток.

245 + 15 = 260
260 – 256 = 4


Итого имеем значение в файле:

[1] [4]

А теперь внимание! Очень интересная логика. Если это диапазон с 0 минут до 45 минут в часе, то в байты добавляется по 15 минут. НО! Если это последние 15 минут в часе (с 45 до 60), то в байты добавляется число 55.

Получается что 1 час всегда равен числу 100. Если у нас 15 часов 45 минут, то в файле мы увидим такие данные:

[6] [9]

А теперь включаем немного магии и переводим значения из байтов в целое число:

6 * 256 + 9 = 1545

Если разделить это число на 100, то целая часть будет равна часам, а дробная – минутам:

1545/100 = 15.45

Кодяра:

data, err := ioutil.ReadFile(defaultPath + scheduleFileName)
	if err != nil {
		log.Printf("[Error] File %s not found: %v", defaultPath+scheduleFileName, err)
	}

	timeOffset1 := 98
	timeOffset2 := 99
	timeSize := 1

	//first 1613 bytes reserved for table
	//one record use 1598 bytes
	//last 1600 bytes reserved for end of table
	for position := 1613; position < (len(data) - 1600); position += 1598 {
		...
		timeInBytesPart1 := data[(position + timeOffset2):(position + timeOffset2 + timeSize)]
		timeInBytesPart2 := data[(position + timeOffset1):(position + timeOffset1 + timeSize)]
		totalBytes := (int(timeInBytesPart1[0]) * 256) + int(timeInBytesPart2[0])
		hours := totalBytes / 100
		minutes := totalBytes - hours*100
		...
	}

Дата в расписании


Логика работы вычисления значения из байт для дат такая же как и во времени. Байт заполняется до 255, затем обнуляется, а в следующий байт добавляется 1 и т.д. Только для даты уже было выделено не два, а четыре байта памяти. Видимо разработчики решили, что их приложение может прожить ещё несколько миллионов лет. Получается, что максимальное число, которое мы можем получить равно:

[255] [255] [255] [256]
256 * 256 * 256 * 256 + 256 * 256 * 256 + 256 * 256 + 256 = 4311810304


Эталонная стартовая дата в приложении равна 31 декабря 1849. Конкретную дату считаю путём добавления дней. Я изначально знаю, что AddDate из пакета time имеет ограничение и не сможет скушать 4311810304 дней, однако на ближайшие лет 200 хватит).
Проанализировав приличное количество файлов я нашёл три варианта расположения дат в памяти:
  • считывание байт, конкретного участка памяти, необходимо проводить слева направо
  • считывание байт, конкретного участка памяти, необходимо проводить справа налево
  • между каждым «полезным байтом» данных вставляется нулевой байт. Например [1] [0] [101] [0][100] [0] [28]

Мне так и не удалось понять, зачем использовалась разная логика для расположения данных, но принцип подсчёта даты везде одинаковый, главное правильно доставать данные.

Кодяра:

func getDate(data []uint8) time.Time {
	startDate := time.Date(1849, 12, 31, 0, 00, 00, 0, time.UTC)
	var result int

	for i := 0; i < len(data)-1; i++ {
		var sqr = 1
		for j := 0; j < i; j++ {
			sqr = sqr * 256
		}

		result = result + (int(data[i]) * sqr)
	}

	return startDate.AddDate(0, 0, result)
}

Расписание доступности


У сотрудников есть расписание доступности. В день можно назначить до трёх интервалов времени. Например:

8:00 – 13:00
14:00 – 16:30
17:00 – 19:00


Расписание может быть назначено на любой день недели. Мне необходимо было сгенерировать расписание на ближайшие 3 месяца.

Вот примерная схема хранения данных в файле:

image

Я вырезаю из файла каждую запись по отдельности, затем смещаюсь на определённое количество байт в зависимости от дня недели и там уже обрабатываю время.

Кодяра:

type Schedule struct {
	ProviderID string                  `json:"provider_id"`
	Date       time.Time               `json:"date"`
	DayStart   string                  `json:"day_start"`
	DayEnd     string                  `json:"day_end"`
	Breaks     *ScheduleBreaks         `json:"breaks"`
}

type SheduleBreaks []*cheduleBreak

type ScheduleBreak struct {
	Start time.Time `json:"start"`
	End   time.Time `json:"end"`
}

func ScanSchedule(config Config) (schedules []Schedule, err error) {
	dataFromFile, err := ioutil.ReadFile(config.DBPath + providersFileName)
	if err != nil {
		return schedules, err
	}

	scheduleOffset := 774
	weeklyDayOffset := map[string]int{
		"Sunday":    0,
		"Monday":    12,
		"Tuesday":   24,
		"Wednesday": 36,
		"Thursday":  48,
		"Friday":    60,
		"Saturday":  72,
	}

	//first 1158 bytes reserved for table
	//one record with contact information use 1147 bytes
	//last 4494 bytes reserved for end of table
	for position := 1158; position < (len(dataFromFile) - 4494); position += 1147 {
		id := getIDFromSliceByte(dataFromFile[position:(position + idSize)])
		//if table border (id equal "255|255"), then finish parse file
		if id == "255|255" {
			break
		}
		position := position + scheduleOffset
		date := time.Now()
		//create schedule on 3 future month (90 days)
		for dayNumber := 1; dayNumber < 90; dayNumber++ {
			schedule := Schedule{}
			offset := weeklyDayOffset[date.Weekday().String()]
			from1, to1 := getScheduleTimeFromBytes((dataFromFile[(position + offset):(position + offset + 4)]), date)
			from2, to2 := getScheduleTimeFromBytes((dataFromFile[(position + offset + 1):(position + offset + 4 + 1)]), date)
			from3, to3 := getScheduleTimeFromBytes((dataFromFile[(position + offset + 2):(position + offset + 4 + 2)]), date)

			//no schedule on this day
			if from1.IsZero() {
				continue
			}

			schedule.Date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC)
			schedule.DayStart = from1.Format(time.RFC3339)

			switch {
			case to3.IsZero() == false:
				schedule.DayEnd = to3.Format(time.RFC3339)
			case to2.IsZero() == false:
				schedule.DayEnd = to2.Format(time.RFC3339)
			case to1.IsZero() == false:
				schedule.DayEnd = to1.Format(time.RFC3339)
			}

			if from2.IsZero() == false {
				scheduleBreaks := ScheduleBreaks{}
				scheduleBreak := ScheduleBreak{}
				scheduleBreak.Start = to1
				scheduleBreak.End = from2
				scheduleBreaks = append(scheduleBreaks, &scheduleBreak)
				if from3.IsZero() == false {
					scheduleBreak.Start = to2
					scheduleBreak.End = from3
					scheduleBreaks = append(scheduleBreaks, &scheduleBreak)
				}
				schedule.Breaks = &scheduleBreaks
			}
			date = date.AddDate(0, 0, 1)
			schedules = append(schedules, &schedule)
		}
	}
	return schedules, err
}

//getScheduleTimeFromBytes calculate bytes in time range
func getScheduleTimeFromBytes(data []uint8, date time.Time) (from, to time.Time) {
	totalTimeFrom := int(data[0])
	totalTimeTo := int(data[3])

	//no schedule
	if totalTimeFrom == 0 && totalTimeTo == 0 {
		return from, to
	}

	hoursFrom := totalTimeFrom / 4
	hoursTo := totalTimeTo / 4
	minutesFrom := (totalTimeFrom*25 - hoursFrom*100) * 6 / 10
	minutesTo := (totalTimeTo*25 - hoursTo*100) * 6 / 10
	from = time.Date(date.Year(), date.Month(), date.Day(), hoursFrom, minutesFrom, 0, 0, time.UTC)
	to = time.Date(date.Year(), date.Month(), date.Day(), hoursTo, minutesTo, 0, 0, time.UTC)

	return from, to
}

В целом хотелось просто поделиться знаниями о том, какими алгоритмами вычислений пользовались раньше.
Теги:
Хабы:
+44
Комментарии65

Публикации

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн