Compare commits

...

2 Commits

Author SHA1 Message Date
40e7d88fd0 Add NRes format documentation and decompression algorithms
Some checks failed
Test / cargo test (push) Failing after 40s
- Created `huffman_decompression.md` detailing the Huffman decompression algorithm used in NRes, including context structure, block modes, and decoding methods.
- Created `overview.md` for the NRes format, outlining file structure, header details, file entries, and packing algorithms.
- Updated `mkdocs.yml` to include new documentation files in the navigation structure.
2026-02-05 01:32:24 +04:00
afe6b9a29b feat: remove Rust project 2026-02-05 00:37:59 +04:00
25 changed files with 1612 additions and 2735 deletions

View File

@@ -1,15 +0,0 @@
{
"image": "mcr.microsoft.com/devcontainers/rust:latest",
"customizations": {
"vscode": {
"extensions": [
"rust-lang.rust-analyzer"
]
}
},
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined"
]
}

1
.gitignore vendored
View File

@@ -1 +0,0 @@
/target

1710
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
[workspace]
resolver = "2"
members = ["libs/*", "tools/*", "packer"]
[profile.release]
codegen-units = 1
lto = true
strip = true

View File

@@ -0,0 +1,426 @@
# FRES Декомпрессия
## Обзор
FRES — это гибридный алгоритм сжатия, использующий комбинацию RLE (Run-Length Encoding) и LZ77-подобного сжатия со скользящим окном. Существуют два режима работы: **adaptive Huffman** (флаг `a1 < 0`) и **простой битовый** (флаг `a1 >= 0`).
```c
char __stdcall sub_1001B22E(
char a1, // Флаг режима (< 0 = Huffman, >= 0 = простой)
int a2, // Ключ/seed (не используется напрямую)
_BYTE *a3, // Выходной буфер
int a4, // Размер выходного буфера
_BYTE *a5, // Входные сжатые данные
int a6 // Размер входных данных
)
```
## Структуры данных
### Глобальные переменные
```c
byte_1003A910[4096] // Циклический буфер скользящего окна (12 бит адрес)
dword_1003E09C // Указатель на конец выходного буфера
dword_1003E0A0 // Текущая позиция в циклическом буфере
dword_1003E098 // Состояние Huffman дерева
dword_1003E0A4 // Длина повтора для LZ77
```
### Константы
```c
#define WINDOW_SIZE 4096 // Размер скользящего окна (0x1000)
#define WINDOW_MASK 0x0FFF // Маска для циклического буфера
#define INIT_POS_NEG 4078 // Начальная позиция для Huffman режима
#define INIT_POS_POS 4036 // Начальная позиция для простого режима
```
## Режим 1: Простой битовый режим (a1 >= 0)
Это более простой режим без Huffman кодирования. Работает следующим образом:
### Алгоритм
```
Инициализация:
position = 4036
flags = 0
flagBits = 0
Цикл декомпрессии:
Пока есть входные данные и выходной буфер не заполнен:
1. Прочитать бит флага:
if (flagBits высокий бит == 0):
flags = *input++
flagBits = 127 (0x7F)
flag_bit = flags & 1
flags >>= 1
2. Прочитать второй бит:
if (flagBits низкий бит == 0):
загрузить новый байт флагов
second_bit = flags & 1
flags >>= 1
3. Выбор действия по битам:
a) Если оба бита == 0:
// Литерал - копировать один байт
byte = *input++
window[position] = byte
*output++ = byte
position = (position + 1) & 0xFFF
b) Если второй бит == 0 (первый == 1):
// LZ77 копирование
word = *(uint16*)input
input += 2
offset = (word >> 4) & 0xFFF // 12 бит offset
length = (word & 0xF) + 3 // 4 бита длины + 3
src_pos = offset
Повторить length раз:
byte = window[src_pos]
window[position] = byte
*output++ = byte
src_pos = (src_pos + 1) & 0xFFF
position = (position + 1) & 0xFFF
```
### Формат сжатых данных (простой режим)
```
Битовый поток:
[FLAG_BIT] [SECOND_BIT] [DATA]
Где:
FLAG_BIT = 0, SECOND_BIT = 0: → Литерал (1 байт следует)
FLAG_BIT = 1, SECOND_BIT = 0: → LZ77 копирование (2 байта следуют)
FLAG_BIT = любой, SECOND_BIT = 1: → Литерал (1 байт следует)
Формат LZ77 копирования (2 байта, little-endian):
Байт 0: offset_low (биты 0-7)
Байт 1: [length:4][offset_high:4]
offset = (byte1 >> 4) | (byte0 << 4) // 12 бит
length = (byte1 & 0x0F) + 3 // 4 бита + 3 = 3-18 байт
```
## Режим 2: Adaptive Huffman режим (a1 < 0)
Более сложный режим с динамическим Huffman деревом.
### Инициализация Huffman
```c
Инициализация таблиц:
1. Создание таблицы быстрого декодирования (dword_1003B94C[256])
2. Инициализация длин кодов (byte_1003BD4C[256])
3. Построение начального дерева (627 узлов)
```
### Алгоритм декодирования
```
Инициализация:
position = 4078
bit_buffer = 0
bit_count = 8
Инициализировать окно значением 0x20 (пробел):
for i in range(2039):
window[i] = 0x20
Цикл декомпрессии:
Пока не конец выходного буфера:
1. Декодировать символ через Huffman дерево:
tree_index = dword_1003E098 // начальный узел
Пока tree_index < 627: // внутренний узел
bit = прочитать_бит()
tree_index = tree[tree_index + bit]
symbol = tree_index - 627 // лист дерева
Обновить дерево (sub_1001B0AE)
2. Обработать символ:
if (symbol < 256):
// Литерал
window[position] = symbol
*output++ = symbol
position = (position + 1) & 0xFFF
else:
// LZ77 копирование
length = symbol - 253
// Прочитать offset (закодирован отдельно)
offset_bits = прочитать_биты(таблица длин)
offset = декодировать(offset_bits)
src_pos = (position - 1 - offset) & 0xFFF
Повторить length раз:
byte = window[src_pos]
window[position] = byte
*output++ = byte
src_pos = (src_pos + 1) & 0xFFF
position = (position + 1) & 0xFFF
```
### Обновление дерева
Адаптивное Huffman дерево обновляется после каждого декодированного символа:
```
Алгоритм обновления:
1. Увеличить счетчик частоты символа
2. Если частота превысила порог:
Перестроить узлы дерева (swapping)
3. Если счетчик достиг 0x8000:
Пересчитать все частоты (разделить на 2)
```
## Псевдокод полной реализации
### Декодер (простой режим)
```python
def fres_decompress_simple(input_data, output_size):
"""
FRES декомпрессия в простом режиме
"""
# Инициализация
window = bytearray(4096)
position = 4036
output = bytearray()
input_pos = 0
flags = 0
flag_bits_high = 0
flag_bits_low = 0
while len(output) < output_size and input_pos < len(input_data):
# Читаем флаг высокого бита
if (flag_bits_high & 1) == 0:
if input_pos >= len(input_data):
break
flags = input_data[input_pos]
input_pos += 1
flag_bits_high = 127 # 0x7F
flag_high = flag_bits_high & 1
flag_bits_high >>= 1
# Читаем флаг низкого бита
if input_pos >= len(input_data):
break
if (flag_bits_low & 1) == 0:
flags = input_data[input_pos]
input_pos += 1
flag_bits_low = 127
flag_low = flags & 1
flags >>= 1
# Обработка по флагам
if not flag_low: # Второй бит == 0
if not flag_high: # Оба бита == 0
# Литерал
if input_pos >= len(input_data):
break
byte = input_data[input_pos]
input_pos += 1
window[position] = byte
output.append(byte)
position = (position + 1) & 0xFFF
else: # Первый == 1, второй == 0
# LZ77 копирование
if input_pos + 1 >= len(input_data):
break
word = input_data[input_pos] | (input_data[input_pos + 1] << 8)
input_pos += 2
offset = (word >> 4) & 0xFFF
length = (word & 0xF) + 3
for _ in range(length):
if len(output) >= output_size:
break
byte = window[offset]
window[position] = byte
output.append(byte)
offset = (offset + 1) & 0xFFF
position = (position + 1) & 0xFFF
else: # Второй бит == 1
# Литерал
if input_pos >= len(input_data):
break
byte = input_data[input_pos]
input_pos += 1
window[position] = byte
output.append(byte)
position = (position + 1) & 0xFFF
return bytes(output[:output_size])
```
### Вспомогательные функции
```python
class BitReader:
"""Класс для побитового чтения"""
def __init__(self, data):
self.data = data
self.pos = 0
self.bit_buffer = 0
self.bits_available = 0
def read_bit(self):
"""Прочитать один бит"""
if self.bits_available == 0:
if self.pos >= len(self.data):
return 0
self.bit_buffer = self.data[self.pos]
self.pos += 1
self.bits_available = 8
bit = self.bit_buffer & 1
self.bit_buffer >>= 1
self.bits_available -= 1
return bit
def read_bits(self, count):
"""Прочитать несколько бит"""
result = 0
for i in range(count):
result |= self.read_bit() << i
return result
def initialize_window():
"""Инициализация окна для Huffman режима"""
window = bytearray(4096)
# Заполняем начальным значением
for i in range(4078):
window[i] = 0x20 # Пробел
return window
```
## Ключевые особенности
### 1. Циклический буфер
- Размер: 4096 байт (12 бит адресации)
- Маска: `0xFFF` для циклического доступа
- Начальная позиция зависит от режима
### 2. Dual-режимы
- **Простой**: Быстрее, меньше сжатие, для данных с низкой энтропией
- **Huffman**: Медленнее, лучше сжатие, для данных с высокой энтропией
### 3. LZ77 кодирование
- Offset: 12 бит (0-4095)
- Length: 4 бита + 3 (3-18 байт)
- Максимальное копирование: 18 байт
### 4. Битовые флаги
Используется двойная система флагов для определения типа следующих данных
## Проблемы реализации
### 1. Битовый порядок
Биты читаются справа налево (LSB first), что может вызвать путаницу
### 2. Huffman дерево
Адаптивное дерево требует точного отслеживания частот и правильной перестройки
### 3. Граничные условия
Необходимо тщательно проверять границы буферов
## Примеры данных
### Пример 1: Литералы (простой режим)
```
Входные биты: 00 00 00 ...
Выход: Последовательность литералов
Пример:
Flags: 0x00 (00000000)
Data: 0x41 ('A'), 0x42 ('B'), 0x43 ('C'), ...
Выход: "ABC..."
```
### Пример 2: LZ77 копирование
```
Входные биты: 10 ...
Выход: Копирование из окна
Пример:
Flags: 0x01 (00000001) - первый бит = 1
Word: 0x1234
Разбор:
offset = (0x34 << 4) | (0x12 >> 4) = 0x341
length = (0x12 & 0xF) + 3 = 5
Действие: Скопировать 5 байт с позиции offset
```
## Отладка
Для отладки рекомендуется:
```python
def debug_fres_decompress(input_data, output_size):
"""Версия с отладочным выводом"""
print(f"Input size: {len(input_data)}")
print(f"Output size: {output_size}")
# ... реализация с print на каждом шаге
print(f"Flag: {flag_high}{flag_low}")
if is_literal:
print(f" Literal: 0x{byte:02X}")
else:
print(f" LZ77: offset={offset}, length={length}")
```
## Заключение
FRES — это эффективный гибридный алгоритм, сочетающий:
- RLE для повторяющихся данных
- LZ77 для ссылок на предыдущие данные
- Опциональный Huffman для символов
**Сложность декомпрессии:** O(n) где n — размер выходных данных
**Размер окна:** 4 КБ — хороший баланс между памятью и степенью сжатия

View File

@@ -0,0 +1,598 @@
# Huffman Декомпрессия
## Обзор
Это реализация **DEFLATE-подобного** алгоритма декомпрессии, используемого в [NRes](overview.md). Алгоритм поддерживает три режима блоков и использует два Huffman дерева для кодирования литералов/длин и расстояний.
```c
int __thiscall sub_1001AF10(
unsigned int *this, // Контекст декодера (HuffmanContext)
int *a2 // Выходной параметр (результат операции)
)
```
## Структура контекста (HuffmanContext)
```c
struct HuffmanContext {
uint32_t bitBuffer[0x4000]; // 0x00000-0x0FFFF: Битовый буфер (32KB)
uint32_t compressedSize; // 0x10000: Размер сжатых данных
uint32_t unknown1; // 0x10004: Не используется
uint32_t outputPosition; // 0x10008: Позиция в выходном буфере
uint32_t currentByte; // 0x1000C: Текущий байт
uint8_t* sourceData; // 0x10010: Указатель на сжатые данные
uint8_t* destData; // 0x10014: Указатель на выходной буфер
uint32_t bitPosition; // 0x10018: Позиция бита
uint32_t inputPosition; // 0x1001C: Позиция чтения (this[16389])
uint32_t decodedBytes; // 0x10020: Декодированные байты (this[16386])
uint32_t bitBufferValue; // 0x10024: Значение бит буфера (this[16391])
uint32_t bitsAvailable; // 0x10028: Доступные биты (this[16392])
// ...
};
// Смещения в структуре:
#define CTX_OUTPUT_POS 16385 // this[16385]
#define CTX_DECODED_BYTES 16386 // this[16386]
#define CTX_SOURCE_PTR 16387 // this[16387]
#define CTX_DEST_PTR 16388 // this[16388]
#define CTX_INPUT_POS 16389 // this[16389]
#define CTX_BIT_BUFFER 16391 // this[16391]
#define CTX_BITS_COUNT 16392 // this[16392]
#define CTX_MAX_SYMBOL 16393 // this[16393]
```
## Три режима блоков
Алгоритм определяет тип блока по первым 3 битам:
```
Биты: [TYPE:2] [FINAL:1]
FINAL = 1: Это последний блок
TYPE:
00 = Несжатый блок (сырые данные)
01 = Сжатый с фиксированными Huffman кодами
10 = Сжатый с динамическими Huffman кодами
11 = Зарезервировано (ошибка)
```
### Основной цикл декодирования
```c
int decode_block(HuffmanContext* ctx) {
// Читаем первый бит (FINAL)
int final_bit = read_bit(ctx);
// Читаем 2 бита (TYPE)
int type = read_bits(ctx, 2);
switch (type) {
case 0: // 00 - Несжатый блок
return decode_uncompressed_block(ctx);
case 1: // 01 - Фиксированные Huffman коды
return decode_fixed_huffman_block(ctx);
case 2: // 10 - Динамические Huffman коды
return decode_dynamic_huffman_block(ctx);
case 3: // 11 - Ошибка
return 2; // Неподдерживаемый тип
}
return final_bit ? 0 : 1; // 0 = конец, 1 = есть еще блоки
}
```
## Режим 0: Несжатый блок
Простое копирование байтов без сжатия.
### Алгоритм
```python
def decode_uncompressed_block(ctx):
"""
Формат несжатого блока:
[LEN:16][NLEN:16][DATA:LEN]
Где:
LEN - длина данных (little-endian)
NLEN - инверсия LEN (~LEN)
DATA - сырые данные
"""
# Выравнивание к границе байта
bits_to_skip = ctx.bits_available & 7
ctx.bit_buffer >>= bits_to_skip
ctx.bits_available -= bits_to_skip
# Читаем длину (16 бит)
length = read_bits(ctx, 16)
# Читаем инверсию длины (16 бит)
nlength = read_bits(ctx, 16)
# Проверка целостности
if length != (~nlength & 0xFFFF):
return 1 # Ошибка
# Копируем данные
for i in range(length):
byte = read_byte(ctx)
write_output_byte(ctx, byte)
# Проверка переполнения выходного буфера
if ctx.output_position >= 0x8000:
flush_output_buffer(ctx)
return 0
```
### Детали
- Данные копируются "как есть"
- Используется для несжимаемых данных
- Требует выравнивания по байтам перед чтением длины
## Режим 1: Фиксированные Huffman коды
Использует предопределенные Huffman таблицы.
### Фиксированные таблицы длин кодов
```python
# Таблица для литералов/длин (288 символов)
FIXED_LITERAL_LENGTHS = [
8, 8, 8, 8, ..., 8, # 0-143: коды длины 8 (144 символа)
9, 9, 9, 9, ..., 9, # 144-255: коды длины 9 (112 символов)
7, 7, 7, 7, ..., 7, # 256-279: коды длины 7 (24 символа)
8, 8, 8, 8, ..., 8 # 280-287: коды длины 8 (8 символов)
]
# Таблица для расстояний (30 символов)
FIXED_DISTANCE_LENGTHS = [
5, 5, 5, 5, ..., 5 # 0-29: все коды длины 5
]
```
### Алгоритм декодирования
```python
def decode_fixed_huffman_block(ctx):
"""Декодирование блока с фиксированными Huffman кодами"""
# Инициализация фиксированных таблиц
lit_tree = build_huffman_tree(FIXED_LITERAL_LENGTHS)
dist_tree = build_huffman_tree(FIXED_DISTANCE_LENGTHS)
while True:
# Декодировать символ литерала/длины
symbol = decode_huffman_symbol(ctx, lit_tree)
if symbol < 256:
# Литерал - просто вывести байт
write_output_byte(ctx, symbol)
elif symbol == 256:
# Конец блока
break
else:
# Символ длины (257-285)
length = decode_length(ctx, symbol)
# Декодировать расстояние
dist_symbol = decode_huffman_symbol(ctx, dist_tree)
distance = decode_distance(ctx, dist_symbol)
# Скопировать из истории
copy_from_history(ctx, distance, length)
```
### Таблицы экстра-бит
```python
# Дополнительные биты для длины
LENGTH_EXTRA_BITS = {
257: 0, 258: 0, 259: 0, 260: 0, 261: 0, 262: 0, 263: 0, 264: 0, # 3-10
265: 1, 266: 1, 267: 1, 268: 1, # 11-18
269: 2, 270: 2, 271: 2, 272: 2, # 19-34
273: 3, 274: 3, 275: 3, 276: 3, # 35-66
277: 4, 278: 4, 279: 4, 280: 4, # 67-130
281: 5, 282: 5, 283: 5, 284: 5, # 131-257
285: 0 # 258
}
LENGTH_BASE = {
257: 3, 258: 4, 259: 5, ..., 285: 258
}
# Дополнительные биты для расстояния
DISTANCE_EXTRA_BITS = {
0: 0, 1: 0, 2: 0, 3: 0, # 1-4
4: 1, 5: 1, 6: 2, 7: 2, # 5-12
8: 3, 9: 3, 10: 4, 11: 4, # 13-48
12: 5, 13: 5, 14: 6, 15: 6, # 49-192
16: 7, 17: 7, 18: 8, 19: 8, # 193-768
20: 9, 21: 9, 22: 10, 23: 10, # 769-3072
24: 11, 25: 11, 26: 12, 27: 12, # 3073-12288
28: 13, 29: 13 # 12289-24576
}
DISTANCE_BASE = {
0: 1, 1: 2, 2: 3, 3: 4, ..., 29: 24577
}
```
### Декодирование длины и расстояния
```python
def decode_length(ctx, symbol):
"""Декодировать длину из символа"""
base = LENGTH_BASE[symbol]
extra_bits = LENGTH_EXTRA_BITS[symbol]
if extra_bits > 0:
extra = read_bits(ctx, extra_bits)
return base + extra
return base
def decode_distance(ctx, symbol):
"""Декодировать расстояние из символа"""
base = DISTANCE_BASE[symbol]
extra_bits = DISTANCE_EXTRA_BITS[symbol]
if extra_bits > 0:
extra = read_bits(ctx, extra_bits)
return base + extra
return base
```
## Режим 2: Динамические Huffman коды
Самый сложный режим. Huffman таблицы передаются в начале блока.
### Формат заголовка динамического блока
```
Биты заголовка:
[HLIT:5] - Количество литерал/длина кодов - 257 (значение: 257-286)
[HDIST:5] - Количество расстояние кодов - 1 (значение: 1-30)
[HCLEN:4] - Количество длин кодов для code length алфавита - 4 (значение: 4-19)
Далее идут длины кодов для code length алфавита:
[CL0:3] [CL1:3] ... [CL(HCLEN-1):3]
Порядок code length кодов:
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
```
### Алгоритм декодирования
```python
def decode_dynamic_huffman_block(ctx):
"""Декодирование блока с динамическими Huffman кодами"""
# 1. Читаем заголовок
hlit = read_bits(ctx, 5) + 257 # Количество литерал/длина кодов
hdist = read_bits(ctx, 5) + 1 # Количество расстояние кодов
hclen = read_bits(ctx, 4) + 4 # Количество code length кодов
if hlit > 286 or hdist > 30:
return 1 # Ошибка
# 2. Читаем длины для code length алфавита
CODE_LENGTH_ORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5,
11, 4, 12, 3, 13, 2, 14, 1, 15]
code_length_lengths = [0] * 19
for i in range(hclen):
code_length_lengths[CODE_LENGTH_ORDER[i]] = read_bits(ctx, 3)
# 3. Строим дерево для code length
cl_tree = build_huffman_tree(code_length_lengths)
# 4. Декодируем длины литерал/длина и расстояние кодов
lengths = decode_code_lengths(ctx, cl_tree, hlit + hdist)
# 5. Разделяем на два алфавита
literal_lengths = lengths[:hlit]
distance_lengths = lengths[hlit:]
# 6. Строим деревья для декодирования
lit_tree = build_huffman_tree(literal_lengths)
dist_tree = build_huffman_tree(distance_lengths)
# 7. Декодируем данные (аналогично фиксированному режиму)
return decode_huffman_data(ctx, lit_tree, dist_tree)
```
### Декодирование длин кодов
Используется специальный алфавит с тремя специальными символами:
```python
def decode_code_lengths(ctx, cl_tree, total_count):
"""
Декодирование последовательности длин кодов
Специальные символы:
16 - Повторить предыдущую длину 3-6 раз (2 доп. бита)
17 - Повторить 0 длину 3-10 раз (3 доп. бита)
18 - Повторить 0 длину 11-138 раз (7 доп. бит)
"""
lengths = []
last_length = 0
while len(lengths) < total_count:
symbol = decode_huffman_symbol(ctx, cl_tree)
if symbol < 16:
# Обычная длина (0-15)
lengths.append(symbol)
last_length = symbol
elif symbol == 16:
# Повторить предыдущую длину
repeat = read_bits(ctx, 2) + 3
lengths.extend([last_length] * repeat)
elif symbol == 17:
# Повторить ноль (короткий)
repeat = read_bits(ctx, 3) + 3
lengths.extend([0] * repeat)
last_length = 0
elif symbol == 18:
# Повторить ноль (длинный)
repeat = read_bits(ctx, 7) + 11
lengths.extend([0] * repeat)
last_length = 0
return lengths[:total_count]
```
## Построение Huffman дерева
```python
def build_huffman_tree(code_lengths):
"""
Построить Huffman дерево из длин кодов
Использует алгоритм "canonical Huffman codes"
"""
max_length = max(code_lengths) if code_lengths else 0
# 1. Подсчитать количество кодов каждой длины
bl_count = [0] * (max_length + 1)
for length in code_lengths:
if length > 0:
bl_count[length] += 1
# 2. Вычислить первый код для каждой длины
code = 0
next_code = [0] * (max_length + 1)
for bits in range(1, max_length + 1):
code = (code + bl_count[bits - 1]) << 1
next_code[bits] = code
# 3. Присвоить числовые коды символам
tree = {}
for symbol, length in enumerate(code_lengths):
if length > 0:
tree[symbol] = {
'code': next_code[length],
'length': length
}
next_code[length] += 1
# 4. Создать структуру быстрого поиска
lookup_table = create_lookup_table(tree)
return lookup_table
def decode_huffman_symbol(ctx, tree):
"""Декодировать один символ из Huffman дерева"""
code = 0
length = 0
for length in range(1, 16):
bit = read_bit(ctx)
code = (code << 1) | bit
# Проверить в таблице быстрого поиска
if (code, length) in tree:
return tree[(code, length)]
return -1 # Ошибка декодирования
```
## Управление выходным буфером
```python
def write_output_byte(ctx, byte):
"""Записать байт в выходной буфер"""
# Записываем в bitBuffer (используется как циклический буфер)
ctx.bitBuffer[ctx.decodedBytes] = byte
ctx.decodedBytes += 1
# Если буфер заполнен (32KB)
if ctx.decodedBytes >= 0x8000:
flush_output_buffer(ctx)
def flush_output_buffer(ctx):
"""Сбросить выходной буфер в финальный выход"""
# Копируем данные в финальный выходной буфер
dest_offset = ctx.outputPosition + ctx.destData
memcpy(dest_offset, ctx.bitBuffer, ctx.decodedBytes)
# Обновляем счетчики
ctx.outputPosition += ctx.decodedBytes
ctx.decodedBytes = 0
def copy_from_history(ctx, distance, length):
"""Скопировать данные из истории (LZ77)"""
# Позиция источника в циклическом буфере
src_pos = (ctx.decodedBytes - distance) & 0x7FFF
for i in range(length):
byte = ctx.bitBuffer[src_pos]
write_output_byte(ctx, byte)
src_pos = (src_pos + 1) & 0x7FFF
```
## Полная реализация на Python
```python
class HuffmanDecoder:
"""Полный DEFLATE-подобный декодер"""
def __init__(self, input_data, output_size):
self.input_data = input_data
self.output_size = output_size
self.input_pos = 0
self.bit_buffer = 0
self.bits_available = 0
self.output = bytearray()
self.history = bytearray(32768) # 32KB циклический буфер
self.history_pos = 0
def read_bit(self):
"""Прочитать один бит"""
if self.bits_available == 0:
if self.input_pos >= len(self.input_data):
return 0
self.bit_buffer = self.input_data[self.input_pos]
self.input_pos += 1
self.bits_available = 8
bit = self.bit_buffer & 1
self.bit_buffer >>= 1
self.bits_available -= 1
return bit
def read_bits(self, count):
"""Прочитать несколько бит (LSB first)"""
result = 0
for i in range(count):
result |= self.read_bit() << i
return result
def write_byte(self, byte):
"""Записать байт в выход и историю"""
self.output.append(byte)
self.history[self.history_pos] = byte
self.history_pos = (self.history_pos + 1) & 0x7FFF
def copy_from_history(self, distance, length):
"""Скопировать из истории"""
src_pos = (self.history_pos - distance) & 0x7FFF
for _ in range(length):
byte = self.history[src_pos]
self.write_byte(byte)
src_pos = (src_pos + 1) & 0x7FFF
def decompress(self):
"""Основной цикл декомпрессии"""
while len(self.output) < self.output_size:
# Читаем заголовок блока
final = self.read_bit()
block_type = self.read_bits(2)
if block_type == 0:
# Несжатый блок
if not self.decode_uncompressed_block():
break
elif block_type == 1:
# Фиксированные Huffman коды
if not self.decode_fixed_huffman_block():
break
elif block_type == 2:
# Динамические Huffman коды
if not self.decode_dynamic_huffman_block():
break
else:
# Ошибка
raise ValueError("Invalid block type")
if final:
break
return bytes(self.output[:self.output_size])
# ... реализации decode_*_block методов ...
```
## Оптимизации
### 1. Таблица быстрого поиска
```python
# Предвычисленная таблица для 9 бит (первый уровень)
FAST_LOOKUP_BITS = 9
fast_table = [None] * (1 << FAST_LOOKUP_BITS)
# Заполнение таблицы при построении дерева
for symbol, info in tree.items():
if info['length'] <= FAST_LOOKUP_BITS:
# Все возможные префиксы для этого кода
code = info['code']
for i in range(1 << (FAST_LOOKUP_BITS - info['length'])):
lookup_code = code | (i << info['length'])
fast_table[lookup_code] = symbol
```
### 2. Буферизация битов
```python
# Читать по 32 бита за раз вместо побитового чтения
def refill_bits(self):
"""Пополнить битовый буфер"""
while self.bits_available < 24 and self.input_pos < len(self.input_data):
byte = self.input_data[self.input_pos]
self.input_pos += 1
self.bit_buffer |= byte << self.bits_available
self.bits_available += 8
```
## Отладка и тестирование
```python
def debug_huffman_decode(data):
"""Декодирование с отладочной информацией"""
decoder = HuffmanDecoder(data, len(data) * 10)
original_read_bits = decoder.read_bits
def debug_read_bits(count):
result = original_read_bits(count)
print(f"Read {count} bits: 0x{result:0{count//4}X} ({result})")
return result
decoder.read_bits = debug_read_bits
return decoder.decompress()
```
## Заключение
Этот Huffman декодер реализует **DEFLATE**-совместимый алгоритм с тремя режимами блоков:
1. **Несжатый** - для несжимаемых данных
2. **Фиксированный Huffman** - быстрое декодирование с предопределенными таблицами
3. **Динамический Huffman** - максимальное сжатие с пользовательскими таблицами
**Ключевые особенности:**
- Поддержка LZ77 для повторяющихся последовательностей
- Канонические Huffman коды для эффективного декодирования
- Циклический буфер 32KB для истории
- Оптимизации через таблицы быстрого поиска
**Сложность:** O(n) где n - размер выходных данных

View File

@@ -0,0 +1,578 @@
# Документация по формату NRes
## Обзор
NRes — это формат контейнера ресурсов, используемый в игровом движке Nikita. Файл представляет собой архив, содержащий несколько упакованных файлов с метаданными и поддержкой различных методов сжатия.
## Структура файла NRes
### 1. Заголовок файла (16 байт)
```c
struct NResHeader {
uint32_t signature; // +0x00: Сигнатура "NRes" (0x7365526E в little-endian)
uint32_t version; // +0x04: Версия формата (0x00000100 = версия 1.0)
uint32_t fileCount; // +0x08: Количество файлов в архиве
uint32_t fileSize; // +0x0C: Общий размер файла в байтах
};
```
**Детали:**
- `signature`: Константа `0x7365526E` (1936020046 в десятичном виде). Это ASCII строка "nRes" в обратном порядке байт
- `version`: Всегда должна быть `0x00000100` (256 в десятичном виде) для версии 1.0
- `fileCount`: Общее количество файлов в архиве (используется для валидации)
- `fileSize`: Полный размер NRes файла, включая заголовок
### 2. Данные файлов
Сразу после заголовка (с offset 0x10) начинаются данные упакованных файлов. Они хранятся последовательно, один за другим. Точное расположение каждого файла определяется записью в каталоге (см. раздел 3).
**⚠️ ВАЖНО: Выравнивание данных**
Данные каждого файла **выравниваются по границе 8 байт**. После записи данных файла добавляется padding (нулевые байты) до ближайшего кратного 8 адреса.
**Формула выравнивания:**
```
aligned_size = (packed_size + 7) & ~7
padding_bytes = aligned_size - packed_size
```
**Пример:**
- Файл размером 100 байт → padding 4 байта (до 104)
- Файл размером 104 байт → padding 0 байт (уже выровнен)
- Файл размером 105 байт → padding 3 байта (до 108)
Это означает, что:
1. `dataOffset` следующего файла всегда кратен 8
2. Между данными файлов могут быть 0-7 байт нулевого padding
3. При чтении нужно использовать `packedSize`, а не выравнивать вручную
### 3. Каталог файлов (Directory)
Каталог находится в **конце файла**. Его расположение вычисляется по формуле:
```
DirectoryOffset = FileSize - (FileCount * 64)
```
Каждая запись в каталоге имеет **фиксированный размер 64 байта (0x40)**:
```c
struct NResFileEntry {
char name[16]; // +0x00: Имя файла (NULL-terminated, uppercase)
uint32_t crc32; // +0x10: CRC32 хеш упакованных данных
uint32_t packMethod; // +0x14: Флаги метода упаковки и опции
uint32_t unpackedSize; // +0x18: Размер файла после распаковки
uint32_t packedSize; // +0x1C: Размер упакованных данных
uint32_t dataOffset; // +0x20: Смещение данных от начала файла
uint32_t fastDataPtr; // +0x24: Указатель для быстрого доступа (в памяти)
uint32_t xorSize; // +0x28: Размер данных для XOR-шифрования
uint32_t sortIndex; // +0x2C: Индекс для сортировки по имени
uint32_t reserved[4]; // +0x30: Зарезервировано (обычно нули)
};
```
## Подробное описание полей каталога
### Поле: name (смещение +0x00, 16 байт)
- **Назначение**: Имя файла в архиве
- **Формат**: NULL-terminated строка, максимум 15 символов + NULL
- **Особенности**:
- Все символы хранятся в **UPPERCASE** (заглавными буквами)
- При поиске файлов используется регистронезависимое сравнение (`_strcmpi`)
- Если имя короче 16 байт, остаток заполняется нулями
### Поле: crc32 (смещение +0x10, 4 байта)
- **Назначение**: Контрольная сумма CRC32 упакованных данных
- **Использование**: Проверка целостности данных при чтении
### Поле: packMethod (смещение +0x14, 4 байта)
**Критически важное поле!** Содержит битовые флаги, определяющие метод обработки данных:
```c
// Маски для извлечения метода упаковки
#define PACK_METHOD_MASK 0x1E0 // Биты 5-8 (основной метод)
#define PACK_METHOD_MASK2 0x1C0 // Биты 6-7 (альтернативная маска)
// Методы упаковки (биты 5-8)
#define PACK_NONE 0x000 // Нет упаковки (копирование)
#define PACK_XOR 0x020 // XOR-шифрование
#define PACK_FRES 0x040 // FRES компрессия (устаревшая)
#define PACK_FRES_XOR 0x060 // FRES + XOR (два прохода)
#define PACK_ZLIB 0x080 // Zlib сжатие (устаревшее)
#define PACK_ZLIB_XOR 0x0A0 // Zlib + XOR (два прохода)
#define PACK_HUFFMAN 0x0E0 // Huffman кодирование (основной метод)
// Дополнительные флаги
#define FLAG_ENCRYPTED 0x040 // Файл зашифрован/требует декодирования
```
**Алгоритм определения метода:**
1. Извлечь биты `packMethod & 0x1E0`
2. Проверить конкретные значения:
- `0x000`: Данные не сжаты, простое копирование
- `0x020`: XOR-шифрование с двухбайтовым ключом
- `0x040` или `0x060`: FRES компрессия (может быть + XOR)
- `0x080` или `0x0A0`: Zlib компрессия (может быть + XOR)
- `0x0E0`: Huffman кодирование (наиболее распространенный)
### Поле: unpackedSize (смещение +0x18, 4 байта)
- **Назначение**: Размер файла после полной распаковки
- **Использование**:
- Для выделения памяти под распакованные данные
- Для проверки корректности распаковки
### Поле: packedSize (смещение +0x1C, 4 байта)
- **Назначение**: Размер сжатых данных в архиве
- **Особенности**:
- Если `packedSize == 0`, файл пустой или является указателем
- Для несжатых файлов: `packedSize == unpackedSize`
### Поле: dataOffset (смещение +0x20, 4 байта)
- **Назначение**: Абсолютное смещение данных файла от начала NRes файла
- **Формула вычисления**: `BaseAddress + dataOffset = начало данных`
- **Диапазон**: Обычно от 0x10 (после заголовка) до начала каталога
### Поле: fastDataPtr (смещение +0x24, 4 байта)
- **Назначение**: Указатель на данные в памяти для быстрого доступа
- **Использование**: Только во время выполнения (runtime)
- **В файле**: Обычно равно 0 или содержит относительный offset
- **Особенность**: Используется функцией `rsLoadFast()` для файлов без упаковки
### Поле: xorSize (смещение +0x28, 4 байта)
- **Назначение**: Размер данных для XOR-шифрования при комбинированных методах
- **Использование**:
- Когда `packMethod & 0x60 == 0x60` (FRES + XOR)
- Сначала применяется XOR к этому количеству байт, затем FRES к результату
- **Значение**: Может отличаться от `packedSize` при многоэтапной упаковке
### Поле: sortIndex (смещение +0x2C, 4 байта)
- **Назначение**: Индекс для быстрого поиска по отсортированному каталогу
- **Использование**:
- Каталог сортируется по алфавиту (имени файлов)
- `sortIndex` хранит оригинальный порядковый номер файла
- Позволяет использовать бинарный поиск для функции `rsFind()`
### Поле: reserved (смещение +0x30, 16 байт)
- **Назначение**: Зарезервировано для будущих расширений
- **В файле**: Обычно заполнено нулями
- **Может содержать**: Дополнительные метаданные в новых версиях формата
## Алгоритмы упаковки
### 1. Без упаковки (PACK_NONE = 0x000)
```
Простое копирование данных:
memcpy(destination, source, packedSize);
```
### 2. XOR-шифрование (PACK_XOR = 0x020)
```c
// Ключ берется из поля crc32
uint16_t key = (uint16_t)(crc32 & 0xFFFF);
for (int i = 0; i < packedSize; i++) {
uint8_t byte = source[i];
destination[i] = byte ^ (key >> 8) ^ (key << 1);
// Обновление ключа
uint8_t newByte = (key >> 8) ^ (key << 1);
key = (newByte ^ ((key >> 8) >> 1)) | (newByte << 8);
}
```
**Ключевые особенности:**
- Используется 16-битный ключ из младших байт CRC32
- Ключ изменяется после каждого байта по специальному алгоритму
- Операции: XOR с старшим байтом ключа и со сдвинутым значением
### 3. [FRES компрессия](fres_decompression.md) (PACK_FRES = 0x040, 0x060)
Алгоритм FRES — это RLE-подобное сжатие с особой кодировкой повторов:
```
sub_1001B22E() - функция декомпрессии FRES
- Читает управляющие байты
- Декодирует литералы и повторы
- Использует скользящее окно для ссылок
```
### 4. [Huffman кодирование](huffman_decompression.md) (PACK_HUFFMAN = 0x0E0)
Наиболее сложный и эффективный метод:
```c
// Структура декодера
struct HuffmanDecoder {
uint32_t bitBuffer[0x4000]; // Буфер для битов
uint32_t compressedSize; // Размер сжатых данных
uint32_t outputPosition; // Текущая позиция в выходном буфере
uint32_t inputPosition; // Позиция в входных данных
uint8_t* sourceData; // Указатель на сжатые данные
uint8_t* destData; // Указатель на выходной буфер
uint32_t bitPosition; // Позиция бита в буфере
// ... дополнительные поля
};
```
**Процесс декодирования:**
1. Инициализация структуры декодера
2. Чтение битов и построение дерева Huffman
3. Декодирование символов по дереву
4. Запись в выходной буфер
## Высокоуровневая инструкция по реализации
### Этап 1: Открытие файла
```python
def open_nres_file(filepath):
with open(filepath, 'rb') as f:
# 1. Читаем заголовок (16 байт)
header_data = f.read(16)
signature, version, file_count, file_size = struct.unpack('<4I', header_data)
# 2. Проверяем сигнатуру
if signature != 0x7365526E: # "nRes"
raise ValueError("Неверная сигнатура файла")
# 3. Проверяем версию
if version != 0x100:
raise ValueError(f"Неподдерживаемая версия: {version}")
# 4. Вычисляем расположение каталога
directory_offset = file_size - (file_count * 64)
# 5. Читаем весь файл в память (или используем memory mapping)
f.seek(0)
file_data = f.read()
return {
'file_count': file_count,
'file_size': file_size,
'directory_offset': directory_offset,
'data': file_data
}
```
### Этап 2: Чтение каталога
```python
def read_directory(nres_file):
data = nres_file['data']
offset = nres_file['directory_offset']
file_count = nres_file['file_count']
entries = []
for i in range(file_count):
entry_offset = offset + (i * 64)
entry_data = data[entry_offset:entry_offset + 64]
# Парсим 64-байтовую запись
name = entry_data[0:16].decode('ascii').rstrip('\x00')
crc32, pack_method, unpacked_size, packed_size, data_offset, \
fast_ptr, xor_size, sort_index = struct.unpack('<8I', entry_data[16:48])
entry = {
'name': name,
'crc32': crc32,
'pack_method': pack_method,
'unpacked_size': unpacked_size,
'packed_size': packed_size,
'data_offset': data_offset,
'fast_data_ptr': fast_ptr,
'xor_size': xor_size,
'sort_index': sort_index
}
entries.append(entry)
return entries
```
### Этап 3: Поиск файла по имени
```python
def find_file(entries, filename):
# Имена в архиве хранятся в UPPERCASE
search_name = filename.upper()
# Используем бинарный поиск, так как каталог отсортирован
# Сортировка по sort_index восстанавливает алфавитный порядок
sorted_entries = sorted(entries, key=lambda e: e['sort_index'])
left, right = 0, len(sorted_entries) - 1
while left <= right:
mid = (left + right) // 2
mid_name = sorted_entries[mid]['name']
if mid_name == search_name:
return sorted_entries[mid]
elif mid_name < search_name:
left = mid + 1
else:
right = mid - 1
return None
```
### Этап 4: Извлечение данных файла
```python
def extract_file(nres_file, entry):
data = nres_file['data']
# 1. Получаем упакованные данные
packed_data = data[entry['data_offset']:
entry['data_offset'] + entry['packed_size']]
# 2. Определяем метод упаковки
pack_method = entry['pack_method'] & 0x1E0
# 3. Распаковываем в зависимости от метода
if pack_method == 0x000:
# Без упаковки
return unpack_none(packed_data)
elif pack_method == 0x020:
# XOR-шифрование
return unpack_xor(packed_data, entry['crc32'], entry['unpacked_size'])
elif pack_method == 0x040 or pack_method == 0x060:
# FRES компрессия (может быть с XOR)
if pack_method == 0x060:
# Сначала XOR
temp_data = unpack_xor(packed_data, entry['crc32'], entry['xor_size'])
return unpack_fres(temp_data, entry['unpacked_size'])
else:
return unpack_fres(packed_data, entry['unpacked_size'])
elif pack_method == 0x0E0:
# Huffman кодирование
return unpack_huffman(packed_data, entry['unpacked_size'])
else:
raise ValueError(f"Неподдерживаемый метод упаковки: 0x{pack_method:X}")
```
### Этап 5: Реализация алгоритмов распаковки
```python
def unpack_none(data):
"""Без упаковки - просто возвращаем данные"""
return data
def unpack_xor(data, crc32, size):
"""XOR-дешифрование с изменяющимся ключом"""
result = bytearray(size)
key = crc32 & 0xFFFF # Берем младшие 16 бит
for i in range(min(size, len(data))):
byte = data[i]
# XOR операция
high_byte = (key >> 8) & 0xFF
shifted = (key << 1) & 0xFFFF
result[i] = byte ^ high_byte ^ (shifted & 0xFF)
# Обновление ключа
new_byte = high_byte ^ (key << 1)
key = (new_byte ^ (high_byte >> 1)) | ((new_byte & 0xFF) << 8)
key &= 0xFFFF
return bytes(result)
def unpack_fres(data, unpacked_size):
"""
FRES декомпрессия - гибридный RLE+LZ77 алгоритм
Полная реализация в nres_decompression.py (класс FRESDecoder)
"""
from nres_decompression import FRESDecoder
decoder = FRESDecoder()
return decoder.decompress(data, unpacked_size)
def unpack_huffman(data, unpacked_size):
"""
Huffman декодирование (DEFLATE-подобный)
Полная реализация в nres_decompression.py (класс HuffmanDecoder)
"""
from nres_decompression import HuffmanDecoder
decoder = HuffmanDecoder()
return decoder.decompress(data, unpacked_size)
```
### Этап 6: Извлечение всех файлов
```python
def extract_all(nres_filepath, output_dir):
import os
# 1. Открываем NRes файл
nres_file = open_nres_file(nres_filepath)
# 2. Читаем каталог
entries = read_directory(nres_file)
# 3. Создаем выходную директорию
os.makedirs(output_dir, exist_ok=True)
# 4. Извлекаем каждый файл
for entry in entries:
print(f"Извлечение: {entry['name']}")
try:
# Извлекаем данные
unpacked_data = extract_file(nres_file, entry)
# Сохраняем в файл
output_path = os.path.join(output_dir, entry['name'])
with open(output_path, 'wb') as f:
f.write(unpacked_data)
print(f" ✓ Успешно ({len(unpacked_data)} байт)")
except Exception as e:
print(f" ✗ Ошибка: {e}")
```
## Особенности и важные замечания
### 1. Порядок байт (Endianness)
- **Все многобайтовые значения хранятся в Little-Endian порядке**
- При чтении используйте `struct.unpack('<...')`
### 2. Сортировка каталога
- Каталог файлов **отсортирован по имени файла** (алфавитный порядок)
- Поле `sortIndex` хранит оригинальный индекс до сортировки
- Это позволяет использовать бинарный поиск
### 3. Регистр символов
- Все имена файлов конвертируются в **UPPERCASE** (заглавные буквы)
- При поиске используйте регистронезависимое сравнение
### 4. Memory Mapping
- Оригинальный код использует `MapViewOfFile` для эффективной работы с большими файлами
- Рекомендуется использовать memory-mapped файлы для больших архивов
### 5. Валидация данных
- **Всегда проверяйте сигнатуру** перед обработкой
- **Проверяйте версию** формата
- **Проверяйте CRC32** после распаковки
- **Проверяйте размеры** (unpacked_size должен совпадать с результатом)
### 6. Обработка ошибок
- Файл может быть поврежден
- Метод упаковки может быть неподдерживаемым
- Данные могут быть частично зашифрованы
### 7. Производительность
- Для несжатых файлов (`packMethod & 0x1E0 == 0`) можно использовать прямое чтение
- Поле `fastDataPtr` может содержать кешированный указатель
- Используйте буферизацию при последовательном чтении
### 8. Выравнивание данных
- **Все данные файлов выравниваются по 8 байт**
- После каждого файла может быть 0-7 байт нулевого padding
- `dataOffset` следующего файла всегда кратен 8
- При чтении используйте `packedSize` из записи, не вычисляйте выравнивание
- При создании архива добавляйте padding: `padding = ((size + 7) & ~7) - size`
## Пример использования
```python
# Открыть архив
nres = open_nres_file("resources.nres")
# Прочитать каталог
entries = read_directory(nres)
# Вывести список файлов
for entry in entries:
print(f"{entry['name']:20s} - {entry['unpacked_size']:8d} байт")
# Найти конкретный файл
entry = find_file(entries, "texture.bmp")
if entry:
data = extract_file(nres, entry)
with open("extracted_texture.bmp", "wb") as f:
f.write(data)
# Извлечь все файлы
extract_all("resources.nres", "./extracted/")
```
## Дополнительные функции
### Проверка формата файла
```python
def is_nres_file(filepath):
try:
with open(filepath, 'rb') as f:
signature = struct.unpack('<I', f.read(4))[0]
return signature == 0x7365526E
except:
return False
```
### Получение информации о файле
```python
def get_file_info(entry):
pack_names = {
0x000: "Без сжатия",
0x020: "XOR",
0x040: "FRES",
0x060: "FRES+XOR",
0x080: "Zlib",
0x0A0: "Zlib+XOR",
0x0E0: "Huffman"
}
pack_method = entry['pack_method'] & 0x1E0
pack_name = pack_names.get(pack_method, f"Неизвестный (0x{pack_method:X})")
ratio = 100.0 * entry['packed_size'] / entry['unpacked_size'] if entry['unpacked_size'] > 0 else 0
return {
'name': entry['name'],
'size': entry['unpacked_size'],
'packed': entry['packed_size'],
'compression': pack_name,
'ratio': f"{ratio:.1f}%",
'crc32': f"0x{entry['crc32']:08X}"
}
```
## Заключение
Формат NRes представляет собой эффективный архив с поддержкой множества методов сжатия.

View File

@@ -1,10 +0,0 @@
[package]
name = "libnres"
version = "0.1.4"
edition = "2021"
[dependencies]
byteorder = "1.4"
log = "0.4"
miette = "7.0"
thiserror = "2.0"

View File

@@ -1,30 +0,0 @@
use crate::error::ConverterError;
/// Method for converting u32 to u64.
pub fn u32_to_u64(value: u32) -> Result<u64, ConverterError> {
Ok(u64::from(value))
}
/// Method for converting u32 to usize.
pub fn u32_to_usize(value: u32) -> Result<usize, ConverterError> {
match usize::try_from(value) {
Err(error) => Err(ConverterError::TryFromIntError(error)),
Ok(result) => Ok(result),
}
}
/// Method for converting u64 to u32.
pub fn u64_to_u32(value: u64) -> Result<u32, ConverterError> {
match u32::try_from(value) {
Err(error) => Err(ConverterError::TryFromIntError(error)),
Ok(result) => Ok(result),
}
}
/// Method for converting usize to u32.
pub fn usize_to_u32(value: usize) -> Result<u32, ConverterError> {
match u32::try_from(value) {
Err(error) => Err(ConverterError::TryFromIntError(error)),
Ok(result) => Ok(result),
}
}

View File

@@ -1,45 +0,0 @@
extern crate miette;
extern crate thiserror;
use miette::Diagnostic;
use thiserror::Error;
#[derive(Error, Diagnostic, Debug)]
pub enum ConverterError {
#[error("error converting an value")]
#[diagnostic(code(libnres::infallible))]
Infallible(#[from] std::convert::Infallible),
#[error("error converting an value")]
#[diagnostic(code(libnres::try_from_int_error))]
TryFromIntError(#[from] std::num::TryFromIntError),
}
#[derive(Error, Diagnostic, Debug)]
pub enum ReaderError {
#[error(transparent)]
#[diagnostic(code(libnres::convert_error))]
ConvertValue(#[from] ConverterError),
#[error("incorrect header format")]
#[diagnostic(code(libnres::list_type_error))]
IncorrectHeader,
#[error("incorrect file size (expected {expected:?} bytes, received {received:?} bytes)")]
#[diagnostic(code(libnres::file_size_error))]
IncorrectSizeFile { expected: u32, received: u32 },
#[error(
"incorrect size of the file list (not a multiple of {expected:?}, received {received:?})"
)]
#[diagnostic(code(libnres::list_size_error))]
IncorrectSizeList { expected: u32, received: u32 },
#[error("resource file reading error")]
#[diagnostic(code(libnres::io_error))]
ReadFile(#[from] std::io::Error),
#[error("file is too small (must be at least {expected:?} bytes, received {received:?} byte)")]
#[diagnostic(code(libnres::file_size_error))]
SmallFile { expected: u32, received: u32 },
}

View File

@@ -1,24 +0,0 @@
/// First constant value of the NRes file ("NRes" characters in numeric)
pub const FILE_TYPE_1: u32 = 1936020046;
/// Second constant value of the NRes file
pub const FILE_TYPE_2: u32 = 256;
/// Size of the element item (in bytes)
pub const LIST_ELEMENT_SIZE: u32 = 64;
/// Minimum allowed file size (in bytes)
pub const MINIMUM_FILE_SIZE: u32 = 16;
static DEBUG: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
mod converter;
mod error;
pub mod reader;
/// Get debug status value
pub fn get_debug() -> bool {
DEBUG.load(std::sync::atomic::Ordering::Relaxed)
}
/// Change debug status value
pub fn set_debug(value: bool) {
DEBUG.store(value, std::sync::atomic::Ordering::Relaxed)
}

View File

@@ -1,227 +0,0 @@
use std::io::{Read, Seek};
use byteorder::ByteOrder;
use crate::error::ReaderError;
use crate::{converter, FILE_TYPE_1, FILE_TYPE_2, LIST_ELEMENT_SIZE, MINIMUM_FILE_SIZE};
#[derive(Debug)]
pub struct ListElement {
/// Unknown parameter
_unknown0: i32,
/// Unknown parameter
_unknown1: i32,
/// Unknown parameter
_unknown2: i32,
/// File extension
pub extension: String,
/// Identifier or sequence number
pub index: u32,
/// File name
pub name: String,
/// Position in the file
pub position: u32,
/// File size (in bytes)
pub size: u32,
}
impl ListElement {
/// Get full name of the file
pub fn get_filename(&self) -> String {
format!("{}.{}", self.name, self.extension)
}
}
#[derive(Debug)]
pub struct FileHeader {
/// File size
size: u32,
/// Number of files
total: u32,
/// First constant value
type1: u32,
/// Second constant value
type2: u32,
}
/// Get a packed file data
pub fn get_file(file: &std::fs::File, element: &ListElement) -> Result<Vec<u8>, ReaderError> {
let size = get_file_size(file)?;
check_file_size(size)?;
let header = get_file_header(file)?;
check_file_header(&header, size)?;
let data = get_element_data(file, element)?;
Ok(data)
}
/// Get a list of packed files
pub fn get_list(file: &std::fs::File) -> Result<Vec<ListElement>, ReaderError> {
let mut list: Vec<ListElement> = Vec::new();
let size = get_file_size(file)?;
check_file_size(size)?;
let header = get_file_header(file)?;
check_file_header(&header, size)?;
get_file_list(file, &header, &mut list)?;
Ok(list)
}
fn check_file_header(header: &FileHeader, size: u32) -> Result<(), ReaderError> {
if header.type1 != FILE_TYPE_1 || header.type2 != FILE_TYPE_2 {
return Err(ReaderError::IncorrectHeader);
}
if header.size != size {
return Err(ReaderError::IncorrectSizeFile {
expected: size,
received: header.size,
});
}
Ok(())
}
fn check_file_size(size: u32) -> Result<(), ReaderError> {
if size < MINIMUM_FILE_SIZE {
return Err(ReaderError::SmallFile {
expected: MINIMUM_FILE_SIZE,
received: size,
});
}
Ok(())
}
fn get_element_data(file: &std::fs::File, element: &ListElement) -> Result<Vec<u8>, ReaderError> {
let position = converter::u32_to_u64(element.position)?;
let size = converter::u32_to_usize(element.size)?;
let mut reader = std::io::BufReader::new(file);
let mut buffer = vec![0u8; size];
if let Err(error) = reader.seek(std::io::SeekFrom::Start(position)) {
return Err(ReaderError::ReadFile(error));
};
if let Err(error) = reader.read_exact(&mut buffer) {
return Err(ReaderError::ReadFile(error));
};
Ok(buffer)
}
fn get_element_position(index: u32) -> Result<(usize, usize), ReaderError> {
let from = converter::u32_to_usize(index * LIST_ELEMENT_SIZE)?;
let to = converter::u32_to_usize((index * LIST_ELEMENT_SIZE) + LIST_ELEMENT_SIZE)?;
Ok((from, to))
}
fn get_file_header(file: &std::fs::File) -> Result<FileHeader, ReaderError> {
let mut reader = std::io::BufReader::new(file);
let mut buffer = vec![0u8; MINIMUM_FILE_SIZE as usize];
if let Err(error) = reader.seek(std::io::SeekFrom::Start(0)) {
return Err(ReaderError::ReadFile(error));
};
if let Err(error) = reader.read_exact(&mut buffer) {
return Err(ReaderError::ReadFile(error));
};
let header = FileHeader {
size: byteorder::LittleEndian::read_u32(&buffer[12..16]),
total: byteorder::LittleEndian::read_u32(&buffer[8..12]),
type1: byteorder::LittleEndian::read_u32(&buffer[0..4]),
type2: byteorder::LittleEndian::read_u32(&buffer[4..8]),
};
buffer.clear();
Ok(header)
}
fn get_file_list(
file: &std::fs::File,
header: &FileHeader,
list: &mut Vec<ListElement>,
) -> Result<(), ReaderError> {
let (start_position, list_size) = get_list_position(header)?;
let mut reader = std::io::BufReader::new(file);
let mut buffer = vec![0u8; list_size];
if let Err(error) = reader.seek(std::io::SeekFrom::Start(start_position)) {
return Err(ReaderError::ReadFile(error));
};
if let Err(error) = reader.read_exact(&mut buffer) {
return Err(ReaderError::ReadFile(error));
}
let buffer_size = converter::usize_to_u32(buffer.len())?;
if buffer_size % LIST_ELEMENT_SIZE != 0 {
return Err(ReaderError::IncorrectSizeList {
expected: LIST_ELEMENT_SIZE,
received: buffer_size,
});
}
for i in 0..(buffer_size / LIST_ELEMENT_SIZE) {
let (from, to) = get_element_position(i)?;
let chunk: &[u8] = &buffer[from..to];
let element = get_list_element(chunk)?;
list.push(element);
}
buffer.clear();
Ok(())
}
fn get_file_size(file: &std::fs::File) -> Result<u32, ReaderError> {
let metadata = match file.metadata() {
Err(error) => return Err(ReaderError::ReadFile(error)),
Ok(value) => value,
};
let result = converter::u64_to_u32(metadata.len())?;
Ok(result)
}
fn get_list_element(buffer: &[u8]) -> Result<ListElement, ReaderError> {
let index = byteorder::LittleEndian::read_u32(&buffer[60..64]);
let position = byteorder::LittleEndian::read_u32(&buffer[56..60]);
let size = byteorder::LittleEndian::read_u32(&buffer[12..16]);
let unknown0 = byteorder::LittleEndian::read_i32(&buffer[4..8]);
let unknown1 = byteorder::LittleEndian::read_i32(&buffer[8..12]);
let unknown2 = byteorder::LittleEndian::read_i32(&buffer[16..20]);
let extension = String::from_utf8_lossy(&buffer[0..4])
.trim_matches(char::from(0))
.to_string();
let name = String::from_utf8_lossy(&buffer[20..56])
.trim_matches(char::from(0))
.to_string();
Ok(ListElement {
_unknown0: unknown0,
_unknown1: unknown1,
_unknown2: unknown2,
extension,
index,
name,
position,
size,
})
}
fn get_list_position(header: &FileHeader) -> Result<(u64, usize), ReaderError> {
let position = converter::u32_to_u64(header.size - (header.total * LIST_ELEMENT_SIZE))?;
let size = converter::u32_to_usize(header.total * LIST_ELEMENT_SIZE)?;
Ok((position, size))
}

View File

@@ -19,6 +19,16 @@ theme:
palette:
scheme: slate
# Navigation
nav:
- Home: index.md
- Specs:
- Assets:
- NRes:
- Документация по формату: specs/assets/nres/overview.md
- FRES Декомпрессия: specs/assets/nres/fres_decompression.md
- Huffman Декомпрессия: specs/assets/nres/huffman_decompression.md
# Additional configuration
extra:
social:

View File

@@ -1,9 +0,0 @@
[package]
name = "packer"
version = "0.1.0"
edition = "2021"
[dependencies]
byteorder = "1.4.3"
serde = { version = "1.0.160", features = ["derive"] }
serde_json = "1.0.96"

View File

@@ -1,27 +0,0 @@
# NRes Game Resource Packer
At the moment, this is a demonstration of the NRes game resource packing algorithm in action.
It packs 100% of the NRes game resources for the game "Parkan: Iron Strategy".
The hash sums of the resulting files match the original game files.
__Attention!__
This is a test version of the utility. It overwrites the specified final file without asking.
## Building
To build the tools, you need to run the following command in the root directory:
```bash
cargo build --release
```
## Running
You can run the utility with the following command:
```bash
./target/release/packer /path/to/unpack /path/to/file.ex
```
- `/path/to/unpack`: This is the directory with the resources unpacked by the [unpacker](../unpacker) utility.
- `/path/to/file.ex`: This is the final file that will be created.

View File

@@ -1,175 +0,0 @@
use std::env;
use std::{
fs::{self, File},
io::{BufReader, Read},
};
use byteorder::{ByteOrder, LittleEndian};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct ImportListElement {
pub extension: String,
pub index: u32,
pub name: String,
pub unknown0: u32,
pub unknown1: u32,
pub unknown2: u32,
}
#[derive(Debug)]
pub struct ListElement {
pub extension: String,
pub index: u32,
pub name: String,
pub position: u32,
pub size: u32,
pub unknown0: u32,
pub unknown1: u32,
pub unknown2: u32,
}
fn main() {
let args: Vec<String> = env::args().collect();
let input = &args[1];
let output = &args[2];
pack(String::from(input), String::from(output));
}
fn pack(input: String, output: String) {
// Загружаем индекс-файл
let index_file = format!("{}/{}", input, "index.json");
let data = fs::read_to_string(index_file).unwrap();
let list: Vec<ImportListElement> = serde_json::from_str(&data).unwrap();
// Общий буфер хранения файлов
let mut content_buffer: Vec<u8> = Vec::new();
let mut list_buffer: Vec<u8> = Vec::new();
// Общее количество файлов
let total_files: u32 = list.len() as u32;
for (index, item) in list.iter().enumerate() {
// Открываем дескриптор файла
let path = format!("{}/{}.{}", input, item.name, item.index);
let file = File::open(path).unwrap();
let metadata = file.metadata().unwrap();
// Считываем файл в буфер
let mut reader = BufReader::new(file);
let mut file_buffer: Vec<u8> = Vec::new();
reader.read_to_end(&mut file_buffer).unwrap();
// Выравнивание буфера
if index != 0 {
while !content_buffer.len().is_multiple_of(8) {
content_buffer.push(0);
}
}
// Получение позиции файла
let position = content_buffer.len() + 16;
// Записываем файл в буфер
content_buffer.extend(file_buffer);
// Формируем элемент
let element = ListElement {
extension: item.extension.to_string(),
index: item.index,
name: item.name.to_string(),
position: position as u32,
size: metadata.len() as u32,
unknown0: item.unknown0,
unknown1: item.unknown1,
unknown2: item.unknown2,
};
// Создаем буфер из элемента
let mut element_buffer: Vec<u8> = Vec::new();
// Пишем тип файла
let mut extension_buffer: [u8; 4] = [0; 4];
let mut file_extension_buffer = element.extension.into_bytes();
file_extension_buffer.resize(4, 0);
extension_buffer.copy_from_slice(&file_extension_buffer);
element_buffer.extend(extension_buffer);
// Пишем неизвестное значение #1
let mut unknown0_buffer: [u8; 4] = [0; 4];
LittleEndian::write_u32(&mut unknown0_buffer, element.unknown0);
element_buffer.extend(unknown0_buffer);
// Пишем неизвестное значение #2
let mut unknown1_buffer: [u8; 4] = [0; 4];
LittleEndian::write_u32(&mut unknown1_buffer, element.unknown1);
element_buffer.extend(unknown1_buffer);
// Пишем размер файла
let mut file_size_buffer: [u8; 4] = [0; 4];
LittleEndian::write_u32(&mut file_size_buffer, element.size);
element_buffer.extend(file_size_buffer);
// Пишем неизвестное значение #3
let mut unknown2_buffer: [u8; 4] = [0; 4];
LittleEndian::write_u32(&mut unknown2_buffer, element.unknown2);
element_buffer.extend(unknown2_buffer);
// Пишем название файла
let mut name_buffer: [u8; 36] = [0; 36];
let mut file_name_buffer = element.name.into_bytes();
file_name_buffer.resize(36, 0);
name_buffer.copy_from_slice(&file_name_buffer);
element_buffer.extend(name_buffer);
// Пишем позицию файла
let mut position_buffer: [u8; 4] = [0; 4];
LittleEndian::write_u32(&mut position_buffer, element.position);
element_buffer.extend(position_buffer);
// Пишем индекс файла
let mut index_buffer: [u8; 4] = [0; 4];
LittleEndian::write_u32(&mut index_buffer, element.index);
element_buffer.extend(index_buffer);
// Добавляем итоговый буфер в буфер элементов списка
list_buffer.extend(element_buffer);
}
// Выравнивание буфера
while !content_buffer.len().is_multiple_of(8) {
content_buffer.push(0);
}
let mut header_buffer: Vec<u8> = Vec::new();
// Пишем первый тип файла
let mut header_type_1 = [0; 4];
LittleEndian::write_u32(&mut header_type_1, 1936020046_u32);
header_buffer.extend(header_type_1);
// Пишем второй тип файла
let mut header_type_2 = [0; 4];
LittleEndian::write_u32(&mut header_type_2, 256_u32);
header_buffer.extend(header_type_2);
// Пишем количество файлов
let mut header_total_files = [0; 4];
LittleEndian::write_u32(&mut header_total_files, total_files);
header_buffer.extend(header_total_files);
// Пишем общий размер файла
let mut header_total_size = [0; 4];
let total_size: u32 = ((content_buffer.len() + 16) as u32) + (total_files * 64);
LittleEndian::write_u32(&mut header_total_size, total_size);
header_buffer.extend(header_total_size);
let mut result_buffer: Vec<u8> = Vec::new();
result_buffer.extend(header_buffer);
result_buffer.extend(content_buffer);
result_buffer.extend(list_buffer);
fs::write(output, result_buffer).unwrap();
}

View File

@@ -1,14 +0,0 @@
[package]
name = "nres-cli"
version = "0.2.3"
edition = "2021"
[dependencies]
byteorder = "1.4"
clap = { version = "4.2", features = ["derive"] }
console = "0.16"
dialoguer = { version = "0.12", features = ["completion"] }
indicatif = "0.18"
libnres = { version = "0.1", path = "../../libs/nres" }
miette = { version = "7.0", features = ["fancy"] }
tempdir = "0.3"

View File

@@ -1,6 +0,0 @@
# Console tool for NRes files (Deprecated)
## Commands
- `extract` - Extract game resources from a "NRes" file.
- `ls` - Get a list of files in a "NRes" file.

View File

@@ -1,198 +0,0 @@
extern crate core;
extern crate libnres;
use std::io::Write;
use clap::{Parser, Subcommand};
use miette::{IntoDiagnostic, Result};
#[derive(Parser, Debug)]
#[command(name = "NRes CLI")]
#[command(about, author, version, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// Check if the "NRes" file can be extract
Check {
/// "NRes" file
file: String,
},
/// Print debugging information on the "NRes" file
#[command(arg_required_else_help = true)]
Debug {
/// "NRes" file
file: String,
/// Filter results by file name
#[arg(long)]
name: Option<String>,
},
/// Extract files or a file from the "NRes" file
#[command(arg_required_else_help = true)]
Extract {
/// "NRes" file
file: String,
/// Overwrite files
#[arg(short, long, default_value_t = false, value_name = "TRUE|FALSE")]
force: bool,
/// Outbound directory
#[arg(short, long, value_name = "DIR")]
out: String,
},
/// Print a list of files in the "NRes" file
#[command(arg_required_else_help = true)]
Ls {
/// "NRes" file
file: String,
},
}
pub fn main() -> Result<()> {
let stdout = console::Term::stdout();
let cli = Cli::parse();
match cli.command {
Commands::Check { file } => command_check(stdout, file)?,
Commands::Debug { file, name } => command_debug(stdout, file, name)?,
Commands::Extract { file, force, out } => command_extract(stdout, file, out, force)?,
Commands::Ls { file } => command_ls(stdout, file)?,
}
Ok(())
}
fn command_check(_stdout: console::Term, file: String) -> Result<()> {
let file = std::fs::File::open(file).into_diagnostic()?;
let list = libnres::reader::get_list(&file).into_diagnostic()?;
let tmp = tempdir::TempDir::new("nres").into_diagnostic()?;
let bar = indicatif::ProgressBar::new(list.len() as u64);
bar.set_style(get_bar_style()?);
for element in list {
bar.set_message(element.get_filename());
let path = tmp.path().join(element.get_filename());
let mut output = std::fs::File::create(path).into_diagnostic()?;
let mut buffer = libnres::reader::get_file(&file, &element).into_diagnostic()?;
output.write_all(&buffer).into_diagnostic()?;
buffer.clear();
bar.inc(1);
}
bar.finish();
Ok(())
}
fn command_debug(stdout: console::Term, file: String, name: Option<String>) -> Result<()> {
let file = std::fs::File::open(file).into_diagnostic()?;
let mut list = libnres::reader::get_list(&file).into_diagnostic()?;
let mut total_files_size: u32 = 0;
let mut total_files_gap: u32 = 0;
let mut total_files: u32 = 0;
for (index, item) in list.iter().enumerate() {
total_files_size += item.size;
total_files += 1;
let mut gap = 0;
if index > 1 {
let previous_item = &list[index - 1];
gap = item.position - (previous_item.position + previous_item.size);
}
total_files_gap += gap;
}
if let Some(name) = name {
list.retain(|item| item.name.contains(&name));
};
for (index, item) in list.iter().enumerate() {
let mut gap = 0;
if index > 1 {
let previous_item = &list[index - 1];
gap = item.position - (previous_item.position + previous_item.size);
}
let text = format!("Index: {};\nGap: {};\nItem: {:#?};\n", index, gap, item);
stdout.write_line(&text).into_diagnostic()?;
}
let text = format!(
"Total files: {};\nTotal files gap: {} (bytes);\nTotal files size: {} (bytes);",
total_files, total_files_gap, total_files_size
);
stdout.write_line(&text).into_diagnostic()?;
Ok(())
}
fn command_extract(_stdout: console::Term, file: String, out: String, force: bool) -> Result<()> {
let file = std::fs::File::open(file).into_diagnostic()?;
let list = libnres::reader::get_list(&file).into_diagnostic()?;
let bar = indicatif::ProgressBar::new(list.len() as u64);
bar.set_style(get_bar_style()?);
for element in list {
bar.set_message(element.get_filename());
let path = format!("{}/{}", out, element.get_filename());
if !force && is_exist_file(&path) {
let message = format!("File \"{}\" exists. Overwrite it?", path);
if !dialoguer::Confirm::new()
.with_prompt(message)
.interact()
.into_diagnostic()?
{
continue;
}
}
let mut output = std::fs::File::create(path).into_diagnostic()?;
let mut buffer = libnres::reader::get_file(&file, &element).into_diagnostic()?;
output.write_all(&buffer).into_diagnostic()?;
buffer.clear();
bar.inc(1);
}
bar.finish();
Ok(())
}
fn command_ls(stdout: console::Term, file: String) -> Result<()> {
let file = std::fs::File::open(file).into_diagnostic()?;
let list = libnres::reader::get_list(&file).into_diagnostic()?;
for element in list {
stdout.write_line(&element.name).into_diagnostic()?;
}
Ok(())
}
fn get_bar_style() -> Result<indicatif::ProgressStyle> {
Ok(
indicatif::ProgressStyle::with_template("[{bar:32}] {pos:>7}/{len:7} {msg}")
.into_diagnostic()?
.progress_chars("=>-"),
)
}
fn is_exist_file(path: &String) -> bool {
let metadata = std::path::Path::new(path);
metadata.exists()
}

View File

@@ -1,8 +0,0 @@
[package]
name = "texture-decoder"
version = "0.1.0"
edition = "2021"
[dependencies]
byteorder = "1.4.3"
image = "0.25.0"

View File

@@ -1,13 +0,0 @@
# Декодировщик текстур
Сборка:
```bash
cargo build --release
```
Запуск:
```bash
./target/release/texture-decoder ./out/AIM_02.0 ./out/AIM_02.0.png
```

View File

@@ -1,41 +0,0 @@
use std::io::Read;
use byteorder::ReadBytesExt;
use image::Rgba;
fn decode_texture(file_path: &str, output_path: &str) -> Result<(), std::io::Error> {
// Читаем файл
let mut file = std::fs::File::open(file_path)?;
let mut buffer: Vec<u8> = Vec::new();
file.read_to_end(&mut buffer)?;
// Декодируем метаданные
let mut cursor = std::io::Cursor::new(&buffer[4..]);
let img_width = cursor.read_u32::<byteorder::LittleEndian>()?;
let img_height = cursor.read_u32::<byteorder::LittleEndian>()?;
// Пропустить оставшиеся байты метаданных
cursor.set_position(20);
// Извлекаем данные изображения
let image_data = buffer[cursor.position() as usize..].to_vec();
let img =
image::ImageBuffer::<Rgba<u8>, _>::from_raw(img_width, img_height, image_data.to_vec())
.expect("Failed to decode image");
// Сохраняем изображение
img.save(output_path).unwrap();
Ok(())
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let input = &args[1];
let output = &args[2];
if let Err(err) = decode_texture(input, output) {
eprintln!("Error: {}", err)
}
}

View File

@@ -1,9 +0,0 @@
[package]
name = "unpacker"
version = "0.1.1"
edition = "2021"
[dependencies]
byteorder = "1.4.3"
serde = { version = "1.0.160", features = ["derive"] }
serde_json = "1.0.96"

View File

@@ -1,41 +0,0 @@
# NRes Game Resource Unpacker
At the moment, this is a demonstration of the NRes game resource unpacking algorithm in action.
It unpacks 100% of the NRes game resources for the game "Parkan: Iron Strategy".
The unpacked resources can be packed again using the [packer](../packer) utility and replace the original game files.
__Attention!__
This is a test version of the utility.
It overwrites existing files without asking.
## Building
To build the tools, you need to run the following command in the root directory:
```bash
cargo build --release
```
## Running
You can run the utility with the following command:
```bash
./target/release/unpacker /path/to/file.ex /path/to/output
```
- `/path/to/file.ex`: This is the file containing the game resources that will be unpacked.
- `/path/to/output`: This is the directory where the unpacked files will be placed.
## How it Works
The structure describing the packed game resources is not fully understood yet.
Therefore, the utility saves unpacked files in the format `file_name.file_index` because some files have the same name.
Additionally, an `index.json` file is created, which is important for re-packing the files.
This file lists all the fields that game resources have in their packed form.
It is essential to preserve the file index for the game to function correctly, as the game engine looks for the necessary files by index.
Files can be replaced and packed back using the [packer](../packer).
The newly obtained game resource files are correctly processed by the game engine.
For example, sounds and 3D models of warbots' weapons were successfully replaced.

View File

@@ -1,124 +0,0 @@
use std::env;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use byteorder::{ByteOrder, LittleEndian};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct FileHeader {
pub size: u32,
pub total: u32,
pub type1: u32,
pub type2: u32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ListElement {
pub extension: String,
pub index: u32,
pub name: String,
#[serde(skip_serializing)]
pub position: u32,
#[serde(skip_serializing)]
pub size: u32,
pub unknown0: u32,
pub unknown1: u32,
pub unknown2: u32,
}
fn main() {
let args: Vec<String> = env::args().collect();
let input = &args[1];
let output = &args[2];
unpack(String::from(input), String::from(output));
}
fn unpack(input: String, output: String) {
let file = File::open(input).unwrap();
let metadata = file.metadata().unwrap();
let mut reader = BufReader::new(file);
let mut list: Vec<ListElement> = Vec::new();
// Считываем заголовок файла
let mut header_buffer = [0u8; 16];
reader.seek(SeekFrom::Start(0)).unwrap();
reader.read_exact(&mut header_buffer).unwrap();
let file_header = FileHeader {
size: LittleEndian::read_u32(&header_buffer[12..16]),
total: LittleEndian::read_u32(&header_buffer[8..12]),
type1: LittleEndian::read_u32(&header_buffer[0..4]),
type2: LittleEndian::read_u32(&header_buffer[4..8]),
};
if file_header.type1 != 1936020046 || file_header.type2 != 256 {
panic!("this isn't NRes file");
}
if metadata.len() != file_header.size as u64 {
panic!("incorrect size")
}
// Считываем список файлов
let list_files_start_position = file_header.size - (file_header.total * 64);
let list_files_size = file_header.total * 64;
let mut list_buffer = vec![0u8; list_files_size as usize];
reader
.seek(SeekFrom::Start(list_files_start_position as u64))
.unwrap();
reader.read_exact(&mut list_buffer).unwrap();
if !list_buffer.len().is_multiple_of(64) {
panic!("invalid files list")
}
for i in 0..(list_buffer.len() / 64) {
let from = i * 64;
let to = (i * 64) + 64;
let chunk: &[u8] = &list_buffer[from..to];
let element_list = ListElement {
extension: String::from_utf8_lossy(&chunk[0..4])
.trim_matches(char::from(0))
.to_string(),
index: LittleEndian::read_u32(&chunk[60..64]),
name: String::from_utf8_lossy(&chunk[20..56])
.trim_matches(char::from(0))
.to_string(),
position: LittleEndian::read_u32(&chunk[56..60]),
size: LittleEndian::read_u32(&chunk[12..16]),
unknown0: LittleEndian::read_u32(&chunk[4..8]),
unknown1: LittleEndian::read_u32(&chunk[8..12]),
unknown2: LittleEndian::read_u32(&chunk[16..20]),
};
list.push(element_list)
}
// Распаковываем файлы в директорию
for element in &list {
let path = format!("{}/{}.{}", output, element.name, element.index);
let mut file = File::create(path).unwrap();
let mut file_buffer = vec![0u8; element.size as usize];
reader
.seek(SeekFrom::Start(element.position as u64))
.unwrap();
reader.read_exact(&mut file_buffer).unwrap();
file.write_all(&file_buffer).unwrap();
file_buffer.clear();
}
// Выгрузка списка файлов в JSON
let path = format!("{}/{}", output, "index.json");
let file = File::create(path).unwrap();
let mut writer = BufWriter::new(file);
serde_json::to_writer_pretty(&mut writer, &list).unwrap();
writer.flush().unwrap();
}