243 lines
9.6 KiB
Python
Executable File
243 lines
9.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Упаковщик FAT12-дискеты Sprinter. Образ принадлежит вызывающему проекту."""
|
|
|
|
import struct
|
|
import sys
|
|
import os
|
|
|
|
def create_floppy_image(output_file, files_to_add):
|
|
"""Создаёт FAT12 образ дискеты 1.44MB и добавляет файлы"""
|
|
|
|
# Размер дискеты 1.44MB
|
|
SECTOR_SIZE = 512
|
|
TOTAL_SECTORS = 2880
|
|
FAT_SIZE = 9
|
|
ROOT_DIR_ENTRIES = 224
|
|
|
|
# Проверяем весь набор до открытия выходного файла: ошибочная упаковка
|
|
# не должна уничтожать предыдущий пригодный носитель приложения.
|
|
capacity = TOTAL_SECTORS - 1 - 2 * FAT_SIZE - ROOT_DIR_ENTRIES * 32 // SECTOR_SIZE
|
|
needed = 0
|
|
names = set()
|
|
if len(files_to_add) > ROOT_DIR_ENTRIES:
|
|
raise ValueError("слишком много файлов для корневого каталога FAT12")
|
|
for filename, src_path in files_to_add:
|
|
name, ext = os.path.splitext(filename)
|
|
short_name = (name[:8].upper(), ext[1:4].upper())
|
|
# Исторические тесты SDK могут иметь длинное имя; на носителе оно
|
|
# сокращается до 8.3. Коллизия после сокращения — настоящая ошибка.
|
|
if not name or short_name in names:
|
|
raise ValueError(f"пустое или повторное имя FAT 8.3: {filename}")
|
|
short_name[0].encode('ascii')
|
|
short_name[1].encode('ascii')
|
|
names.add(short_name)
|
|
needed += max(1, (os.path.getsize(src_path) + SECTOR_SIZE - 1) // SECTOR_SIZE)
|
|
if needed > capacity:
|
|
raise ValueError(f"данные не помещаются на дискету: {needed} > {capacity} секторов")
|
|
|
|
os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True)
|
|
|
|
# Создаём пустой образ
|
|
with open(output_file, 'wb') as f:
|
|
f.write(b'\x00' * (TOTAL_SECTORS * SECTOR_SIZE))
|
|
|
|
# Читаем и модифицируем образ
|
|
with open(output_file, 'r+b') as f:
|
|
# BPB (BIOS Parameter Block) - начинаем с смещения 0
|
|
# Jump instruction
|
|
f.seek(0)
|
|
f.write(b'\xEB\x3C\x90')
|
|
# OEM name
|
|
f.write(b'MSDOS5.0')
|
|
# Bytes per sector
|
|
f.write(struct.pack('<H', SECTOR_SIZE))
|
|
# Sectors per cluster
|
|
f.write(struct.pack('B', 1))
|
|
# Reserved sectors
|
|
f.write(struct.pack('<H', 1))
|
|
# Number of FATs
|
|
f.write(struct.pack('B', 2))
|
|
# Root directory entries
|
|
f.write(struct.pack('<H', ROOT_DIR_ENTRIES))
|
|
# Total sectors (16-bit)
|
|
f.write(struct.pack('<H', 0))
|
|
# Media descriptor
|
|
f.write(struct.pack('B', 0xF0))
|
|
# Sectors per FAT
|
|
f.write(struct.pack('<H', FAT_SIZE))
|
|
# Sectors per track
|
|
f.write(struct.pack('<H', 18))
|
|
# Heads
|
|
f.write(struct.pack('<H', 2))
|
|
# Hidden sectors
|
|
f.write(struct.pack('<I', 0))
|
|
# Total sectors (32-bit)
|
|
f.write(struct.pack('<I', TOTAL_SECTORS))
|
|
|
|
# Extended BPB
|
|
# Drive number
|
|
f.write(struct.pack('B', 0))
|
|
# Reserved
|
|
f.write(struct.pack('B', 0))
|
|
# Extended boot signature
|
|
f.write(struct.pack('B', 0x29))
|
|
# Volume serial number
|
|
f.write(struct.pack('<I', 0x12345678))
|
|
# Volume label
|
|
f.write(b'NO NAME ')
|
|
# File system type
|
|
f.write(b'FAT12 ')
|
|
|
|
# Boot signature
|
|
f.seek(510)
|
|
f.write(struct.pack('<H', 0xAA55))
|
|
|
|
# FAT таблицы
|
|
fat_start = 1 * SECTOR_SIZE
|
|
fat1_start = fat_start
|
|
fat2_start = fat_start + FAT_SIZE * SECTOR_SIZE
|
|
|
|
# Инициализация FAT
|
|
# Первые две записи зарезервированы
|
|
fat = [0xFFF8, 0xFFFF]
|
|
|
|
# Добавляем файлы
|
|
root_dir_start = (1 + 2 * FAT_SIZE) * SECTOR_SIZE
|
|
data_start = root_dir_start + ROOT_DIR_ENTRIES * 32
|
|
|
|
cluster = 2 # Первый кластер данных
|
|
|
|
for filename, src_path in files_to_add:
|
|
# Читаем файл
|
|
with open(src_path, 'rb') as src:
|
|
file_data = src.read()
|
|
|
|
file_size = len(file_data)
|
|
print(f"Добавляем файл: {filename}, размер: {file_size} байт (0x{file_size:04X})")
|
|
|
|
# Вычисляем количество кластеров
|
|
clusters_needed = max(1, (file_size + SECTOR_SIZE - 1) // SECTOR_SIZE)
|
|
|
|
# Добавляем записи в FAT
|
|
first_cluster = cluster
|
|
for i in range(clusters_needed):
|
|
if i == clusters_needed - 1:
|
|
fat.append(0xFFFF) # Последний кластер
|
|
else:
|
|
fat.append(cluster + 1)
|
|
cluster += 1
|
|
|
|
# Записываем данные файла
|
|
file_offset = (first_cluster - 2) * SECTOR_SIZE
|
|
f.seek(data_start + file_offset)
|
|
f.write(file_data)
|
|
|
|
# Создаём запись в корневом каталоге
|
|
# Имя файла (8.3 формат)
|
|
name, ext = os.path.splitext(filename)
|
|
name = name.upper().ljust(8, ' ')[:8]
|
|
# ext comes from splitext as ".XYZ" (with leading dot) or "".
|
|
# Strip the dot first, then space-pad to 3 chars. The old form
|
|
# `ext.upper().ljust(3, ' ')[1:4]` lost padding for 2-char
|
|
# extensions like ".MD" → "MD" (2 bytes), corrupting the dir
|
|
# entry layout.
|
|
ext = ext[1:].upper().ljust(3, ' ')[:3] if ext else ' '
|
|
|
|
# Атрибуты
|
|
attr = 0x20 # Archive
|
|
print(f"Атрибуты: 0x{attr:02X}")
|
|
|
|
# Время и дата (упрощённо)
|
|
time_val = 0x0000
|
|
date_val = 0x0021
|
|
|
|
# Первый кластер (FAT12: младший байт по смещению 26, старший по 27)
|
|
first_cluster_lo = first_cluster & 0xFF
|
|
first_cluster_hi = (first_cluster >> 8) & 0xFF
|
|
|
|
# Размер файла (пробуем big-endian для Sprinter)
|
|
file_size_bytes = struct.pack('<I', file_size)
|
|
print(f"Размер файла в записи: {file_size_bytes.hex()}")
|
|
|
|
# Записываем в корневой каталог (32 байта)
|
|
dir_entry = bytearray(32)
|
|
dir_entry[0:8] = name.encode('ascii')
|
|
dir_entry[8:11] = ext.encode('ascii')
|
|
dir_entry[11] = attr
|
|
dir_entry[12:22] = b'\x00' * 10 # Reserved
|
|
dir_entry[22:24] = struct.pack('<H', time_val)
|
|
dir_entry[24:26] = struct.pack('<H', date_val)
|
|
dir_entry[26] = first_cluster_lo
|
|
dir_entry[27] = first_cluster_hi
|
|
dir_entry[28:32] = file_size_bytes
|
|
|
|
print(f"Атрибуты в записи: 0x{dir_entry[11]:02X}")
|
|
print(f"Запись каталога: {dir_entry.hex()}")
|
|
|
|
# Находим свободное место в корневом каталоге
|
|
f.seek(root_dir_start)
|
|
found = False
|
|
for i in range(ROOT_DIR_ENTRIES):
|
|
entry_pos = root_dir_start + i * 32
|
|
f.seek(entry_pos)
|
|
first_byte = f.read(1)
|
|
if first_byte == b'\x00' or first_byte == b'\xE5':
|
|
f.seek(entry_pos)
|
|
f.write(dir_entry)
|
|
found = True
|
|
print(f"Записан в корневой каталог, запись #{i}, позиция 0x{entry_pos:04X}")
|
|
break
|
|
|
|
if not found:
|
|
print(f"Ошибка: нет места в корневом каталоге для {filename}")
|
|
return False
|
|
|
|
# Записываем FAT
|
|
# FAT12 кодирование: 3 байта на 2 записи
|
|
fat_bytes = bytearray()
|
|
for i in range(0, len(fat), 2):
|
|
if i + 1 < len(fat):
|
|
val1 = fat[i] & 0xFFF
|
|
val2 = fat[i + 1] & 0xFFF
|
|
fat_bytes.extend([
|
|
val1 & 0xFF,
|
|
((val2 & 0xF) << 4) | ((val1 >> 8) & 0xF),
|
|
(val2 >> 4) & 0xFF
|
|
])
|
|
else:
|
|
val1 = fat[i] & 0xFFF
|
|
fat_bytes.extend([
|
|
val1 & 0xFF,
|
|
(val1 >> 8) & 0xF,
|
|
0x00
|
|
])
|
|
|
|
# Записываем FAT1
|
|
f.seek(fat1_start)
|
|
f.write(fat_bytes[:FAT_SIZE * SECTOR_SIZE])
|
|
|
|
# Записываем FAT2
|
|
f.seek(fat2_start)
|
|
f.write(fat_bytes[:FAT_SIZE * SECTOR_SIZE])
|
|
|
|
print(f"Создан образ: {output_file}")
|
|
print(f"Использовано кластеров: {cluster - 2}")
|
|
return True
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) < 3:
|
|
print("Использование: python3 make_disk.py <output.img> <file1> [file2 ...]")
|
|
sys.exit(1)
|
|
|
|
output_file = sys.argv[1]
|
|
files = []
|
|
for i in range(2, len(sys.argv)):
|
|
files.append((os.path.basename(sys.argv[i]), sys.argv[i]))
|
|
|
|
try:
|
|
ok = create_floppy_image(output_file, files)
|
|
except (OSError, ValueError, UnicodeEncodeError) as error:
|
|
print(f"make_disk: {error}", file=sys.stderr)
|
|
sys.exit(1)
|
|
sys.exit(0 if ok else 1)
|