Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1bd98083b |
15
.devcontainer/devcontainer.json
Normal file
15
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/rust:latest",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"rust-lang.rust-analyzer"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runArgs": [
|
||||||
|
"--cap-add=SYS_PTRACE",
|
||||||
|
"--security-opt",
|
||||||
|
"seccomp=unconfined"
|
||||||
|
]
|
||||||
|
}
|
||||||
73
.gitignore
vendored
73
.gitignore
vendored
@@ -1,72 +1 @@
|
|||||||
*~
|
/target
|
||||||
|
|
||||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
|
||||||
.fuse_hidden*
|
|
||||||
|
|
||||||
# KDE directory preferences
|
|
||||||
.directory
|
|
||||||
|
|
||||||
# Linux trash folder which might appear on any partition or disk
|
|
||||||
.Trash-*
|
|
||||||
|
|
||||||
# .nfs files are created when an open file is removed but is still being accessed
|
|
||||||
.nfs*
|
|
||||||
|
|
||||||
# General
|
|
||||||
.DS_Store
|
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
|
|
||||||
# Icon must end with two \r
|
|
||||||
Icon
|
|
||||||
|
|
||||||
# Thumbnails
|
|
||||||
._*
|
|
||||||
|
|
||||||
# Files that might appear in the root of a volume
|
|
||||||
.DocumentRevisions-V100
|
|
||||||
.fseventsd
|
|
||||||
.Spotlight-V100
|
|
||||||
.TemporaryItems
|
|
||||||
.Trashes
|
|
||||||
.VolumeIcon.icns
|
|
||||||
.com.apple.timemachine.donotpresent
|
|
||||||
|
|
||||||
# Directories potentially created on remote AFP share
|
|
||||||
.AppleDB
|
|
||||||
.AppleDesktop
|
|
||||||
Network Trash Folder
|
|
||||||
Temporary Items
|
|
||||||
.apdisk
|
|
||||||
|
|
||||||
# Windows thumbnail cache files
|
|
||||||
Thumbs.db
|
|
||||||
Thumbs.db:encryptable
|
|
||||||
ehthumbs.db
|
|
||||||
ehthumbs_vista.db
|
|
||||||
|
|
||||||
# Dump file
|
|
||||||
*.stackdump
|
|
||||||
|
|
||||||
# Folder config file
|
|
||||||
[Dd]esktop.ini
|
|
||||||
|
|
||||||
# Recycle Bin used on file shares
|
|
||||||
$RECYCLE.BIN/
|
|
||||||
|
|
||||||
# Windows Installer files
|
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msix
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
|
|
||||||
# Windows shortcuts
|
|
||||||
*.lnk
|
|
||||||
|
|
||||||
# Zig programming language
|
|
||||||
zig-cache/
|
|
||||||
zig-out/
|
|
||||||
build/
|
|
||||||
build-*/
|
|
||||||
docgen_tmp/
|
|
||||||
|
|||||||
1710
Cargo.lock
generated
Normal file
1710
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = ["libs/*", "tools/*", "packer"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = true
|
||||||
|
strip = true
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
|
||||||
_ = b; // stub
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
.{
|
|
||||||
.name = .fparkan,
|
|
||||||
.version = "0.0.1",
|
|
||||||
.minimum_zig_version = "0.15.2",
|
|
||||||
.paths = .{""},
|
|
||||||
.fingerprint = 0x8922aff25cf1dd39,
|
|
||||||
}
|
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
# 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 КБ — хороший баланс между памятью и степенью сжатия
|
|
||||||
@@ -1,598 +0,0 @@
|
|||||||
# 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 - размер выходных данных
|
|
||||||
@@ -1,578 +0,0 @@
|
|||||||
# Документация по формату 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 представляет собой эффективный архив с поддержкой множества методов сжатия.
|
|
||||||
10
libs/nres/Cargo.toml
Normal file
10
libs/nres/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "libnres"
|
||||||
|
version = "0.1.4"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
byteorder = "1.4"
|
||||||
|
log = "0.4"
|
||||||
|
miette = "7.0"
|
||||||
|
thiserror = "2.0"
|
||||||
30
libs/nres/src/converter.rs
Normal file
30
libs/nres/src/converter.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
45
libs/nres/src/error.rs
Normal file
45
libs/nres/src/error.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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 },
|
||||||
|
}
|
||||||
24
libs/nres/src/lib.rs
Normal file
24
libs/nres/src/lib.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/// 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)
|
||||||
|
}
|
||||||
227
libs/nres/src/reader.rs
Normal file
227
libs/nres/src/reader.rs
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
10
mkdocs.yml
10
mkdocs.yml
@@ -19,16 +19,6 @@ theme:
|
|||||||
palette:
|
palette:
|
||||||
scheme: slate
|
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
|
# Additional configuration
|
||||||
extra:
|
extra:
|
||||||
social:
|
social:
|
||||||
|
|||||||
9
packer/Cargo.toml
Normal file
9
packer/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[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"
|
||||||
27
packer/README.md
Normal file
27
packer/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# 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.
|
||||||
175
packer/src/main.rs
Normal file
175
packer/src/main.rs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
14
tools/nres-cli/Cargo.toml
Normal file
14
tools/nres-cli/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[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"
|
||||||
6
tools/nres-cli/README.md
Normal file
6
tools/nres-cli/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 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.
|
||||||
198
tools/nres-cli/src/main.rs
Normal file
198
tools/nres-cli/src/main.rs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
8
tools/texture-decoder/Cargo.toml
Normal file
8
tools/texture-decoder/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "texture-decoder"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
byteorder = "1.4.3"
|
||||||
|
image = "0.25.0"
|
||||||
13
tools/texture-decoder/README.md
Normal file
13
tools/texture-decoder/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Декодировщик текстур
|
||||||
|
|
||||||
|
Сборка:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./target/release/texture-decoder ./out/AIM_02.0 ./out/AIM_02.0.png
|
||||||
|
```
|
||||||
41
tools/texture-decoder/src/main.rs
Normal file
41
tools/texture-decoder/src/main.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
tools/unpacker/Cargo.toml
Normal file
9
tools/unpacker/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[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"
|
||||||
41
tools/unpacker/README.md
Normal file
41
tools/unpacker/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 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.
|
||||||
124
tools/unpacker/src/main.rs
Normal file
124
tools/unpacker/src/main.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user