feat: remove Rust project
This commit is contained in:
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"image": "mcr.microsoft.com/devcontainers/rust:latest",
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": [
|
|
||||||
"rust-lang.rust-analyzer"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runArgs": [
|
|
||||||
"--cap-add=SYS_PTRACE",
|
|
||||||
"--security-opt",
|
|
||||||
"seccomp=unconfined"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/target
|
|
||||||
|
|||||||
1710
Cargo.lock
generated
1710
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
|||||||
[workspace]
|
|
||||||
resolver = "2"
|
|
||||||
members = ["libs/*", "tools/*", "packer"]
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
codegen-units = 1
|
|
||||||
lto = true
|
|
||||||
strip = true
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "libnres"
|
|
||||||
version = "0.1.4"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
byteorder = "1.4"
|
|
||||||
log = "0.4"
|
|
||||||
miette = "7.0"
|
|
||||||
thiserror = "2.0"
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
use crate::error::ConverterError;
|
|
||||||
|
|
||||||
/// Method for converting u32 to u64.
|
|
||||||
pub fn u32_to_u64(value: u32) -> Result<u64, ConverterError> {
|
|
||||||
Ok(u64::from(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Method for converting u32 to usize.
|
|
||||||
pub fn u32_to_usize(value: u32) -> Result<usize, ConverterError> {
|
|
||||||
match usize::try_from(value) {
|
|
||||||
Err(error) => Err(ConverterError::TryFromIntError(error)),
|
|
||||||
Ok(result) => Ok(result),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Method for converting u64 to u32.
|
|
||||||
pub fn u64_to_u32(value: u64) -> Result<u32, ConverterError> {
|
|
||||||
match u32::try_from(value) {
|
|
||||||
Err(error) => Err(ConverterError::TryFromIntError(error)),
|
|
||||||
Ok(result) => Ok(result),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Method for converting usize to u32.
|
|
||||||
pub fn usize_to_u32(value: usize) -> Result<u32, ConverterError> {
|
|
||||||
match u32::try_from(value) {
|
|
||||||
Err(error) => Err(ConverterError::TryFromIntError(error)),
|
|
||||||
Ok(result) => Ok(result),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
extern crate miette;
|
|
||||||
extern crate thiserror;
|
|
||||||
|
|
||||||
use miette::Diagnostic;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Diagnostic, Debug)]
|
|
||||||
pub enum ConverterError {
|
|
||||||
#[error("error converting an value")]
|
|
||||||
#[diagnostic(code(libnres::infallible))]
|
|
||||||
Infallible(#[from] std::convert::Infallible),
|
|
||||||
|
|
||||||
#[error("error converting an value")]
|
|
||||||
#[diagnostic(code(libnres::try_from_int_error))]
|
|
||||||
TryFromIntError(#[from] std::num::TryFromIntError),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Error, Diagnostic, Debug)]
|
|
||||||
pub enum ReaderError {
|
|
||||||
#[error(transparent)]
|
|
||||||
#[diagnostic(code(libnres::convert_error))]
|
|
||||||
ConvertValue(#[from] ConverterError),
|
|
||||||
|
|
||||||
#[error("incorrect header format")]
|
|
||||||
#[diagnostic(code(libnres::list_type_error))]
|
|
||||||
IncorrectHeader,
|
|
||||||
|
|
||||||
#[error("incorrect file size (expected {expected:?} bytes, received {received:?} bytes)")]
|
|
||||||
#[diagnostic(code(libnres::file_size_error))]
|
|
||||||
IncorrectSizeFile { expected: u32, received: u32 },
|
|
||||||
|
|
||||||
#[error(
|
|
||||||
"incorrect size of the file list (not a multiple of {expected:?}, received {received:?})"
|
|
||||||
)]
|
|
||||||
#[diagnostic(code(libnres::list_size_error))]
|
|
||||||
IncorrectSizeList { expected: u32, received: u32 },
|
|
||||||
|
|
||||||
#[error("resource file reading error")]
|
|
||||||
#[diagnostic(code(libnres::io_error))]
|
|
||||||
ReadFile(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("file is too small (must be at least {expected:?} bytes, received {received:?} byte)")]
|
|
||||||
#[diagnostic(code(libnres::file_size_error))]
|
|
||||||
SmallFile { expected: u32, received: u32 },
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/// First constant value of the NRes file ("NRes" characters in numeric)
|
|
||||||
pub const FILE_TYPE_1: u32 = 1936020046;
|
|
||||||
/// Second constant value of the NRes file
|
|
||||||
pub const FILE_TYPE_2: u32 = 256;
|
|
||||||
/// Size of the element item (in bytes)
|
|
||||||
pub const LIST_ELEMENT_SIZE: u32 = 64;
|
|
||||||
/// Minimum allowed file size (in bytes)
|
|
||||||
pub const MINIMUM_FILE_SIZE: u32 = 16;
|
|
||||||
|
|
||||||
static DEBUG: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
|
||||||
|
|
||||||
mod converter;
|
|
||||||
mod error;
|
|
||||||
pub mod reader;
|
|
||||||
|
|
||||||
/// Get debug status value
|
|
||||||
pub fn get_debug() -> bool {
|
|
||||||
DEBUG.load(std::sync::atomic::Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change debug status value
|
|
||||||
pub fn set_debug(value: bool) {
|
|
||||||
DEBUG.store(value, std::sync::atomic::Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
use std::io::{Read, Seek};
|
|
||||||
|
|
||||||
use byteorder::ByteOrder;
|
|
||||||
|
|
||||||
use crate::error::ReaderError;
|
|
||||||
use crate::{converter, FILE_TYPE_1, FILE_TYPE_2, LIST_ELEMENT_SIZE, MINIMUM_FILE_SIZE};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ListElement {
|
|
||||||
/// Unknown parameter
|
|
||||||
_unknown0: i32,
|
|
||||||
/// Unknown parameter
|
|
||||||
_unknown1: i32,
|
|
||||||
/// Unknown parameter
|
|
||||||
_unknown2: i32,
|
|
||||||
/// File extension
|
|
||||||
pub extension: String,
|
|
||||||
/// Identifier or sequence number
|
|
||||||
pub index: u32,
|
|
||||||
/// File name
|
|
||||||
pub name: String,
|
|
||||||
/// Position in the file
|
|
||||||
pub position: u32,
|
|
||||||
/// File size (in bytes)
|
|
||||||
pub size: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ListElement {
|
|
||||||
/// Get full name of the file
|
|
||||||
pub fn get_filename(&self) -> String {
|
|
||||||
format!("{}.{}", self.name, self.extension)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FileHeader {
|
|
||||||
/// File size
|
|
||||||
size: u32,
|
|
||||||
/// Number of files
|
|
||||||
total: u32,
|
|
||||||
/// First constant value
|
|
||||||
type1: u32,
|
|
||||||
/// Second constant value
|
|
||||||
type2: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a packed file data
|
|
||||||
pub fn get_file(file: &std::fs::File, element: &ListElement) -> Result<Vec<u8>, ReaderError> {
|
|
||||||
let size = get_file_size(file)?;
|
|
||||||
check_file_size(size)?;
|
|
||||||
|
|
||||||
let header = get_file_header(file)?;
|
|
||||||
check_file_header(&header, size)?;
|
|
||||||
|
|
||||||
let data = get_element_data(file, element)?;
|
|
||||||
Ok(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a list of packed files
|
|
||||||
pub fn get_list(file: &std::fs::File) -> Result<Vec<ListElement>, ReaderError> {
|
|
||||||
let mut list: Vec<ListElement> = Vec::new();
|
|
||||||
|
|
||||||
let size = get_file_size(file)?;
|
|
||||||
check_file_size(size)?;
|
|
||||||
|
|
||||||
let header = get_file_header(file)?;
|
|
||||||
check_file_header(&header, size)?;
|
|
||||||
|
|
||||||
get_file_list(file, &header, &mut list)?;
|
|
||||||
|
|
||||||
Ok(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_file_header(header: &FileHeader, size: u32) -> Result<(), ReaderError> {
|
|
||||||
if header.type1 != FILE_TYPE_1 || header.type2 != FILE_TYPE_2 {
|
|
||||||
return Err(ReaderError::IncorrectHeader);
|
|
||||||
}
|
|
||||||
|
|
||||||
if header.size != size {
|
|
||||||
return Err(ReaderError::IncorrectSizeFile {
|
|
||||||
expected: size,
|
|
||||||
received: header.size,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_file_size(size: u32) -> Result<(), ReaderError> {
|
|
||||||
if size < MINIMUM_FILE_SIZE {
|
|
||||||
return Err(ReaderError::SmallFile {
|
|
||||||
expected: MINIMUM_FILE_SIZE,
|
|
||||||
received: size,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_element_data(file: &std::fs::File, element: &ListElement) -> Result<Vec<u8>, ReaderError> {
|
|
||||||
let position = converter::u32_to_u64(element.position)?;
|
|
||||||
let size = converter::u32_to_usize(element.size)?;
|
|
||||||
|
|
||||||
let mut reader = std::io::BufReader::new(file);
|
|
||||||
let mut buffer = vec![0u8; size];
|
|
||||||
|
|
||||||
if let Err(error) = reader.seek(std::io::SeekFrom::Start(position)) {
|
|
||||||
return Err(ReaderError::ReadFile(error));
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(error) = reader.read_exact(&mut buffer) {
|
|
||||||
return Err(ReaderError::ReadFile(error));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_element_position(index: u32) -> Result<(usize, usize), ReaderError> {
|
|
||||||
let from = converter::u32_to_usize(index * LIST_ELEMENT_SIZE)?;
|
|
||||||
let to = converter::u32_to_usize((index * LIST_ELEMENT_SIZE) + LIST_ELEMENT_SIZE)?;
|
|
||||||
Ok((from, to))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_file_header(file: &std::fs::File) -> Result<FileHeader, ReaderError> {
|
|
||||||
let mut reader = std::io::BufReader::new(file);
|
|
||||||
let mut buffer = vec![0u8; MINIMUM_FILE_SIZE as usize];
|
|
||||||
|
|
||||||
if let Err(error) = reader.seek(std::io::SeekFrom::Start(0)) {
|
|
||||||
return Err(ReaderError::ReadFile(error));
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(error) = reader.read_exact(&mut buffer) {
|
|
||||||
return Err(ReaderError::ReadFile(error));
|
|
||||||
};
|
|
||||||
|
|
||||||
let header = FileHeader {
|
|
||||||
size: byteorder::LittleEndian::read_u32(&buffer[12..16]),
|
|
||||||
total: byteorder::LittleEndian::read_u32(&buffer[8..12]),
|
|
||||||
type1: byteorder::LittleEndian::read_u32(&buffer[0..4]),
|
|
||||||
type2: byteorder::LittleEndian::read_u32(&buffer[4..8]),
|
|
||||||
};
|
|
||||||
|
|
||||||
buffer.clear();
|
|
||||||
Ok(header)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_file_list(
|
|
||||||
file: &std::fs::File,
|
|
||||||
header: &FileHeader,
|
|
||||||
list: &mut Vec<ListElement>,
|
|
||||||
) -> Result<(), ReaderError> {
|
|
||||||
let (start_position, list_size) = get_list_position(header)?;
|
|
||||||
let mut reader = std::io::BufReader::new(file);
|
|
||||||
let mut buffer = vec![0u8; list_size];
|
|
||||||
|
|
||||||
if let Err(error) = reader.seek(std::io::SeekFrom::Start(start_position)) {
|
|
||||||
return Err(ReaderError::ReadFile(error));
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(error) = reader.read_exact(&mut buffer) {
|
|
||||||
return Err(ReaderError::ReadFile(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
let buffer_size = converter::usize_to_u32(buffer.len())?;
|
|
||||||
|
|
||||||
if buffer_size % LIST_ELEMENT_SIZE != 0 {
|
|
||||||
return Err(ReaderError::IncorrectSizeList {
|
|
||||||
expected: LIST_ELEMENT_SIZE,
|
|
||||||
received: buffer_size,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0..(buffer_size / LIST_ELEMENT_SIZE) {
|
|
||||||
let (from, to) = get_element_position(i)?;
|
|
||||||
let chunk: &[u8] = &buffer[from..to];
|
|
||||||
|
|
||||||
let element = get_list_element(chunk)?;
|
|
||||||
list.push(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.clear();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_file_size(file: &std::fs::File) -> Result<u32, ReaderError> {
|
|
||||||
let metadata = match file.metadata() {
|
|
||||||
Err(error) => return Err(ReaderError::ReadFile(error)),
|
|
||||||
Ok(value) => value,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = converter::u64_to_u32(metadata.len())?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_list_element(buffer: &[u8]) -> Result<ListElement, ReaderError> {
|
|
||||||
let index = byteorder::LittleEndian::read_u32(&buffer[60..64]);
|
|
||||||
let position = byteorder::LittleEndian::read_u32(&buffer[56..60]);
|
|
||||||
let size = byteorder::LittleEndian::read_u32(&buffer[12..16]);
|
|
||||||
let unknown0 = byteorder::LittleEndian::read_i32(&buffer[4..8]);
|
|
||||||
let unknown1 = byteorder::LittleEndian::read_i32(&buffer[8..12]);
|
|
||||||
let unknown2 = byteorder::LittleEndian::read_i32(&buffer[16..20]);
|
|
||||||
|
|
||||||
let extension = String::from_utf8_lossy(&buffer[0..4])
|
|
||||||
.trim_matches(char::from(0))
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let name = String::from_utf8_lossy(&buffer[20..56])
|
|
||||||
.trim_matches(char::from(0))
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Ok(ListElement {
|
|
||||||
_unknown0: unknown0,
|
|
||||||
_unknown1: unknown1,
|
|
||||||
_unknown2: unknown2,
|
|
||||||
extension,
|
|
||||||
index,
|
|
||||||
name,
|
|
||||||
position,
|
|
||||||
size,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_list_position(header: &FileHeader) -> Result<(u64, usize), ReaderError> {
|
|
||||||
let position = converter::u32_to_u64(header.size - (header.total * LIST_ELEMENT_SIZE))?;
|
|
||||||
let size = converter::u32_to_usize(header.total * LIST_ELEMENT_SIZE)?;
|
|
||||||
Ok((position, size))
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "packer"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
byteorder = "1.4.3"
|
|
||||||
serde = { version = "1.0.160", features = ["derive"] }
|
|
||||||
serde_json = "1.0.96"
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# NRes Game Resource Packer
|
|
||||||
|
|
||||||
At the moment, this is a demonstration of the NRes game resource packing algorithm in action.
|
|
||||||
It packs 100% of the NRes game resources for the game "Parkan: Iron Strategy".
|
|
||||||
The hash sums of the resulting files match the original game files.
|
|
||||||
|
|
||||||
__Attention!__
|
|
||||||
This is a test version of the utility. It overwrites the specified final file without asking.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
To build the tools, you need to run the following command in the root directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
You can run the utility with the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./target/release/packer /path/to/unpack /path/to/file.ex
|
|
||||||
```
|
|
||||||
|
|
||||||
- `/path/to/unpack`: This is the directory with the resources unpacked by the [unpacker](../unpacker) utility.
|
|
||||||
- `/path/to/file.ex`: This is the final file that will be created.
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::{
|
|
||||||
fs::{self, File},
|
|
||||||
io::{BufReader, Read},
|
|
||||||
};
|
|
||||||
|
|
||||||
use byteorder::{ByteOrder, LittleEndian};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct ImportListElement {
|
|
||||||
pub extension: String,
|
|
||||||
pub index: u32,
|
|
||||||
pub name: String,
|
|
||||||
pub unknown0: u32,
|
|
||||||
pub unknown1: u32,
|
|
||||||
pub unknown2: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ListElement {
|
|
||||||
pub extension: String,
|
|
||||||
pub index: u32,
|
|
||||||
pub name: String,
|
|
||||||
pub position: u32,
|
|
||||||
pub size: u32,
|
|
||||||
pub unknown0: u32,
|
|
||||||
pub unknown1: u32,
|
|
||||||
pub unknown2: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
|
|
||||||
let input = &args[1];
|
|
||||||
let output = &args[2];
|
|
||||||
|
|
||||||
pack(String::from(input), String::from(output));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pack(input: String, output: String) {
|
|
||||||
// Загружаем индекс-файл
|
|
||||||
let index_file = format!("{}/{}", input, "index.json");
|
|
||||||
let data = fs::read_to_string(index_file).unwrap();
|
|
||||||
let list: Vec<ImportListElement> = serde_json::from_str(&data).unwrap();
|
|
||||||
|
|
||||||
// Общий буфер хранения файлов
|
|
||||||
let mut content_buffer: Vec<u8> = Vec::new();
|
|
||||||
let mut list_buffer: Vec<u8> = Vec::new();
|
|
||||||
|
|
||||||
// Общее количество файлов
|
|
||||||
let total_files: u32 = list.len() as u32;
|
|
||||||
|
|
||||||
for (index, item) in list.iter().enumerate() {
|
|
||||||
// Открываем дескриптор файла
|
|
||||||
let path = format!("{}/{}.{}", input, item.name, item.index);
|
|
||||||
let file = File::open(path).unwrap();
|
|
||||||
let metadata = file.metadata().unwrap();
|
|
||||||
|
|
||||||
// Считываем файл в буфер
|
|
||||||
let mut reader = BufReader::new(file);
|
|
||||||
let mut file_buffer: Vec<u8> = Vec::new();
|
|
||||||
reader.read_to_end(&mut file_buffer).unwrap();
|
|
||||||
|
|
||||||
// Выравнивание буфера
|
|
||||||
if index != 0 {
|
|
||||||
while !content_buffer.len().is_multiple_of(8) {
|
|
||||||
content_buffer.push(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получение позиции файла
|
|
||||||
let position = content_buffer.len() + 16;
|
|
||||||
|
|
||||||
// Записываем файл в буфер
|
|
||||||
content_buffer.extend(file_buffer);
|
|
||||||
|
|
||||||
// Формируем элемент
|
|
||||||
let element = ListElement {
|
|
||||||
extension: item.extension.to_string(),
|
|
||||||
index: item.index,
|
|
||||||
name: item.name.to_string(),
|
|
||||||
position: position as u32,
|
|
||||||
size: metadata.len() as u32,
|
|
||||||
unknown0: item.unknown0,
|
|
||||||
unknown1: item.unknown1,
|
|
||||||
unknown2: item.unknown2,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Создаем буфер из элемента
|
|
||||||
let mut element_buffer: Vec<u8> = Vec::new();
|
|
||||||
|
|
||||||
// Пишем тип файла
|
|
||||||
let mut extension_buffer: [u8; 4] = [0; 4];
|
|
||||||
let mut file_extension_buffer = element.extension.into_bytes();
|
|
||||||
file_extension_buffer.resize(4, 0);
|
|
||||||
extension_buffer.copy_from_slice(&file_extension_buffer);
|
|
||||||
element_buffer.extend(extension_buffer);
|
|
||||||
|
|
||||||
// Пишем неизвестное значение #1
|
|
||||||
let mut unknown0_buffer: [u8; 4] = [0; 4];
|
|
||||||
LittleEndian::write_u32(&mut unknown0_buffer, element.unknown0);
|
|
||||||
element_buffer.extend(unknown0_buffer);
|
|
||||||
|
|
||||||
// Пишем неизвестное значение #2
|
|
||||||
let mut unknown1_buffer: [u8; 4] = [0; 4];
|
|
||||||
LittleEndian::write_u32(&mut unknown1_buffer, element.unknown1);
|
|
||||||
element_buffer.extend(unknown1_buffer);
|
|
||||||
|
|
||||||
// Пишем размер файла
|
|
||||||
let mut file_size_buffer: [u8; 4] = [0; 4];
|
|
||||||
LittleEndian::write_u32(&mut file_size_buffer, element.size);
|
|
||||||
element_buffer.extend(file_size_buffer);
|
|
||||||
|
|
||||||
// Пишем неизвестное значение #3
|
|
||||||
let mut unknown2_buffer: [u8; 4] = [0; 4];
|
|
||||||
LittleEndian::write_u32(&mut unknown2_buffer, element.unknown2);
|
|
||||||
element_buffer.extend(unknown2_buffer);
|
|
||||||
|
|
||||||
// Пишем название файла
|
|
||||||
let mut name_buffer: [u8; 36] = [0; 36];
|
|
||||||
let mut file_name_buffer = element.name.into_bytes();
|
|
||||||
file_name_buffer.resize(36, 0);
|
|
||||||
name_buffer.copy_from_slice(&file_name_buffer);
|
|
||||||
element_buffer.extend(name_buffer);
|
|
||||||
|
|
||||||
// Пишем позицию файла
|
|
||||||
let mut position_buffer: [u8; 4] = [0; 4];
|
|
||||||
LittleEndian::write_u32(&mut position_buffer, element.position);
|
|
||||||
element_buffer.extend(position_buffer);
|
|
||||||
|
|
||||||
// Пишем индекс файла
|
|
||||||
let mut index_buffer: [u8; 4] = [0; 4];
|
|
||||||
LittleEndian::write_u32(&mut index_buffer, element.index);
|
|
||||||
element_buffer.extend(index_buffer);
|
|
||||||
|
|
||||||
// Добавляем итоговый буфер в буфер элементов списка
|
|
||||||
list_buffer.extend(element_buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выравнивание буфера
|
|
||||||
while !content_buffer.len().is_multiple_of(8) {
|
|
||||||
content_buffer.push(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut header_buffer: Vec<u8> = Vec::new();
|
|
||||||
|
|
||||||
// Пишем первый тип файла
|
|
||||||
let mut header_type_1 = [0; 4];
|
|
||||||
LittleEndian::write_u32(&mut header_type_1, 1936020046_u32);
|
|
||||||
header_buffer.extend(header_type_1);
|
|
||||||
|
|
||||||
// Пишем второй тип файла
|
|
||||||
let mut header_type_2 = [0; 4];
|
|
||||||
LittleEndian::write_u32(&mut header_type_2, 256_u32);
|
|
||||||
header_buffer.extend(header_type_2);
|
|
||||||
|
|
||||||
// Пишем количество файлов
|
|
||||||
let mut header_total_files = [0; 4];
|
|
||||||
LittleEndian::write_u32(&mut header_total_files, total_files);
|
|
||||||
header_buffer.extend(header_total_files);
|
|
||||||
|
|
||||||
// Пишем общий размер файла
|
|
||||||
let mut header_total_size = [0; 4];
|
|
||||||
let total_size: u32 = ((content_buffer.len() + 16) as u32) + (total_files * 64);
|
|
||||||
LittleEndian::write_u32(&mut header_total_size, total_size);
|
|
||||||
header_buffer.extend(header_total_size);
|
|
||||||
|
|
||||||
let mut result_buffer: Vec<u8> = Vec::new();
|
|
||||||
result_buffer.extend(header_buffer);
|
|
||||||
result_buffer.extend(content_buffer);
|
|
||||||
result_buffer.extend(list_buffer);
|
|
||||||
|
|
||||||
fs::write(output, result_buffer).unwrap();
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "nres-cli"
|
|
||||||
version = "0.2.3"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
byteorder = "1.4"
|
|
||||||
clap = { version = "4.2", features = ["derive"] }
|
|
||||||
console = "0.16"
|
|
||||||
dialoguer = { version = "0.12", features = ["completion"] }
|
|
||||||
indicatif = "0.18"
|
|
||||||
libnres = { version = "0.1", path = "../../libs/nres" }
|
|
||||||
miette = { version = "7.0", features = ["fancy"] }
|
|
||||||
tempdir = "0.3"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# Console tool for NRes files (Deprecated)
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
- `extract` - Extract game resources from a "NRes" file.
|
|
||||||
- `ls` - Get a list of files in a "NRes" file.
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
extern crate core;
|
|
||||||
extern crate libnres;
|
|
||||||
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
use miette::{IntoDiagnostic, Result};
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(name = "NRes CLI")]
|
|
||||||
#[command(about, author, version, long_about = None)]
|
|
||||||
struct Cli {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Commands,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
|
||||||
enum Commands {
|
|
||||||
/// Check if the "NRes" file can be extract
|
|
||||||
Check {
|
|
||||||
/// "NRes" file
|
|
||||||
file: String,
|
|
||||||
},
|
|
||||||
/// Print debugging information on the "NRes" file
|
|
||||||
#[command(arg_required_else_help = true)]
|
|
||||||
Debug {
|
|
||||||
/// "NRes" file
|
|
||||||
file: String,
|
|
||||||
/// Filter results by file name
|
|
||||||
#[arg(long)]
|
|
||||||
name: Option<String>,
|
|
||||||
},
|
|
||||||
/// Extract files or a file from the "NRes" file
|
|
||||||
#[command(arg_required_else_help = true)]
|
|
||||||
Extract {
|
|
||||||
/// "NRes" file
|
|
||||||
file: String,
|
|
||||||
/// Overwrite files
|
|
||||||
#[arg(short, long, default_value_t = false, value_name = "TRUE|FALSE")]
|
|
||||||
force: bool,
|
|
||||||
/// Outbound directory
|
|
||||||
#[arg(short, long, value_name = "DIR")]
|
|
||||||
out: String,
|
|
||||||
},
|
|
||||||
/// Print a list of files in the "NRes" file
|
|
||||||
#[command(arg_required_else_help = true)]
|
|
||||||
Ls {
|
|
||||||
/// "NRes" file
|
|
||||||
file: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main() -> Result<()> {
|
|
||||||
let stdout = console::Term::stdout();
|
|
||||||
let cli = Cli::parse();
|
|
||||||
|
|
||||||
match cli.command {
|
|
||||||
Commands::Check { file } => command_check(stdout, file)?,
|
|
||||||
Commands::Debug { file, name } => command_debug(stdout, file, name)?,
|
|
||||||
Commands::Extract { file, force, out } => command_extract(stdout, file, out, force)?,
|
|
||||||
Commands::Ls { file } => command_ls(stdout, file)?,
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_check(_stdout: console::Term, file: String) -> Result<()> {
|
|
||||||
let file = std::fs::File::open(file).into_diagnostic()?;
|
|
||||||
let list = libnres::reader::get_list(&file).into_diagnostic()?;
|
|
||||||
let tmp = tempdir::TempDir::new("nres").into_diagnostic()?;
|
|
||||||
let bar = indicatif::ProgressBar::new(list.len() as u64);
|
|
||||||
|
|
||||||
bar.set_style(get_bar_style()?);
|
|
||||||
|
|
||||||
for element in list {
|
|
||||||
bar.set_message(element.get_filename());
|
|
||||||
|
|
||||||
let path = tmp.path().join(element.get_filename());
|
|
||||||
let mut output = std::fs::File::create(path).into_diagnostic()?;
|
|
||||||
let mut buffer = libnres::reader::get_file(&file, &element).into_diagnostic()?;
|
|
||||||
|
|
||||||
output.write_all(&buffer).into_diagnostic()?;
|
|
||||||
buffer.clear();
|
|
||||||
bar.inc(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
bar.finish();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_debug(stdout: console::Term, file: String, name: Option<String>) -> Result<()> {
|
|
||||||
let file = std::fs::File::open(file).into_diagnostic()?;
|
|
||||||
let mut list = libnres::reader::get_list(&file).into_diagnostic()?;
|
|
||||||
|
|
||||||
let mut total_files_size: u32 = 0;
|
|
||||||
let mut total_files_gap: u32 = 0;
|
|
||||||
let mut total_files: u32 = 0;
|
|
||||||
|
|
||||||
for (index, item) in list.iter().enumerate() {
|
|
||||||
total_files_size += item.size;
|
|
||||||
total_files += 1;
|
|
||||||
let mut gap = 0;
|
|
||||||
|
|
||||||
if index > 1 {
|
|
||||||
let previous_item = &list[index - 1];
|
|
||||||
gap = item.position - (previous_item.position + previous_item.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
total_files_gap += gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(name) = name {
|
|
||||||
list.retain(|item| item.name.contains(&name));
|
|
||||||
};
|
|
||||||
|
|
||||||
for (index, item) in list.iter().enumerate() {
|
|
||||||
let mut gap = 0;
|
|
||||||
|
|
||||||
if index > 1 {
|
|
||||||
let previous_item = &list[index - 1];
|
|
||||||
gap = item.position - (previous_item.position + previous_item.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = format!("Index: {};\nGap: {};\nItem: {:#?};\n", index, gap, item);
|
|
||||||
stdout.write_line(&text).into_diagnostic()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = format!(
|
|
||||||
"Total files: {};\nTotal files gap: {} (bytes);\nTotal files size: {} (bytes);",
|
|
||||||
total_files, total_files_gap, total_files_size
|
|
||||||
);
|
|
||||||
|
|
||||||
stdout.write_line(&text).into_diagnostic()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_extract(_stdout: console::Term, file: String, out: String, force: bool) -> Result<()> {
|
|
||||||
let file = std::fs::File::open(file).into_diagnostic()?;
|
|
||||||
let list = libnres::reader::get_list(&file).into_diagnostic()?;
|
|
||||||
let bar = indicatif::ProgressBar::new(list.len() as u64);
|
|
||||||
|
|
||||||
bar.set_style(get_bar_style()?);
|
|
||||||
|
|
||||||
for element in list {
|
|
||||||
bar.set_message(element.get_filename());
|
|
||||||
|
|
||||||
let path = format!("{}/{}", out, element.get_filename());
|
|
||||||
|
|
||||||
if !force && is_exist_file(&path) {
|
|
||||||
let message = format!("File \"{}\" exists. Overwrite it?", path);
|
|
||||||
|
|
||||||
if !dialoguer::Confirm::new()
|
|
||||||
.with_prompt(message)
|
|
||||||
.interact()
|
|
||||||
.into_diagnostic()?
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut output = std::fs::File::create(path).into_diagnostic()?;
|
|
||||||
let mut buffer = libnres::reader::get_file(&file, &element).into_diagnostic()?;
|
|
||||||
|
|
||||||
output.write_all(&buffer).into_diagnostic()?;
|
|
||||||
buffer.clear();
|
|
||||||
bar.inc(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
bar.finish();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_ls(stdout: console::Term, file: String) -> Result<()> {
|
|
||||||
let file = std::fs::File::open(file).into_diagnostic()?;
|
|
||||||
let list = libnres::reader::get_list(&file).into_diagnostic()?;
|
|
||||||
|
|
||||||
for element in list {
|
|
||||||
stdout.write_line(&element.name).into_diagnostic()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bar_style() -> Result<indicatif::ProgressStyle> {
|
|
||||||
Ok(
|
|
||||||
indicatif::ProgressStyle::with_template("[{bar:32}] {pos:>7}/{len:7} {msg}")
|
|
||||||
.into_diagnostic()?
|
|
||||||
.progress_chars("=>-"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_exist_file(path: &String) -> bool {
|
|
||||||
let metadata = std::path::Path::new(path);
|
|
||||||
metadata.exists()
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "texture-decoder"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
byteorder = "1.4.3"
|
|
||||||
image = "0.25.0"
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Декодировщик текстур
|
|
||||||
|
|
||||||
Сборка:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
Запуск:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./target/release/texture-decoder ./out/AIM_02.0 ./out/AIM_02.0.png
|
|
||||||
```
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
use std::io::Read;
|
|
||||||
|
|
||||||
use byteorder::ReadBytesExt;
|
|
||||||
use image::Rgba;
|
|
||||||
|
|
||||||
fn decode_texture(file_path: &str, output_path: &str) -> Result<(), std::io::Error> {
|
|
||||||
// Читаем файл
|
|
||||||
let mut file = std::fs::File::open(file_path)?;
|
|
||||||
let mut buffer: Vec<u8> = Vec::new();
|
|
||||||
file.read_to_end(&mut buffer)?;
|
|
||||||
|
|
||||||
// Декодируем метаданные
|
|
||||||
let mut cursor = std::io::Cursor::new(&buffer[4..]);
|
|
||||||
let img_width = cursor.read_u32::<byteorder::LittleEndian>()?;
|
|
||||||
let img_height = cursor.read_u32::<byteorder::LittleEndian>()?;
|
|
||||||
|
|
||||||
// Пропустить оставшиеся байты метаданных
|
|
||||||
cursor.set_position(20);
|
|
||||||
|
|
||||||
// Извлекаем данные изображения
|
|
||||||
let image_data = buffer[cursor.position() as usize..].to_vec();
|
|
||||||
let img =
|
|
||||||
image::ImageBuffer::<Rgba<u8>, _>::from_raw(img_width, img_height, image_data.to_vec())
|
|
||||||
.expect("Failed to decode image");
|
|
||||||
|
|
||||||
// Сохраняем изображение
|
|
||||||
img.save(output_path).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
|
||||||
|
|
||||||
let input = &args[1];
|
|
||||||
let output = &args[2];
|
|
||||||
|
|
||||||
if let Err(err) = decode_texture(input, output) {
|
|
||||||
eprintln!("Error: {}", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "unpacker"
|
|
||||||
version = "0.1.1"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
byteorder = "1.4.3"
|
|
||||||
serde = { version = "1.0.160", features = ["derive"] }
|
|
||||||
serde_json = "1.0.96"
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# NRes Game Resource Unpacker
|
|
||||||
|
|
||||||
At the moment, this is a demonstration of the NRes game resource unpacking algorithm in action.
|
|
||||||
It unpacks 100% of the NRes game resources for the game "Parkan: Iron Strategy".
|
|
||||||
The unpacked resources can be packed again using the [packer](../packer) utility and replace the original game files.
|
|
||||||
|
|
||||||
__Attention!__
|
|
||||||
This is a test version of the utility.
|
|
||||||
It overwrites existing files without asking.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
To build the tools, you need to run the following command in the root directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
You can run the utility with the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./target/release/unpacker /path/to/file.ex /path/to/output
|
|
||||||
```
|
|
||||||
|
|
||||||
- `/path/to/file.ex`: This is the file containing the game resources that will be unpacked.
|
|
||||||
- `/path/to/output`: This is the directory where the unpacked files will be placed.
|
|
||||||
|
|
||||||
## How it Works
|
|
||||||
|
|
||||||
The structure describing the packed game resources is not fully understood yet.
|
|
||||||
Therefore, the utility saves unpacked files in the format `file_name.file_index` because some files have the same name.
|
|
||||||
|
|
||||||
Additionally, an `index.json` file is created, which is important for re-packing the files.
|
|
||||||
This file lists all the fields that game resources have in their packed form.
|
|
||||||
It is essential to preserve the file index for the game to function correctly, as the game engine looks for the necessary files by index.
|
|
||||||
|
|
||||||
Files can be replaced and packed back using the [packer](../packer).
|
|
||||||
The newly obtained game resource files are correctly processed by the game engine.
|
|
||||||
For example, sounds and 3D models of warbots' weapons were successfully replaced.
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
|
|
||||||
|
|
||||||
use byteorder::{ByteOrder, LittleEndian};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct FileHeader {
|
|
||||||
pub size: u32,
|
|
||||||
pub total: u32,
|
|
||||||
pub type1: u32,
|
|
||||||
pub type2: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct ListElement {
|
|
||||||
pub extension: String,
|
|
||||||
pub index: u32,
|
|
||||||
pub name: String,
|
|
||||||
#[serde(skip_serializing)]
|
|
||||||
pub position: u32,
|
|
||||||
#[serde(skip_serializing)]
|
|
||||||
pub size: u32,
|
|
||||||
pub unknown0: u32,
|
|
||||||
pub unknown1: u32,
|
|
||||||
pub unknown2: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
|
|
||||||
let input = &args[1];
|
|
||||||
let output = &args[2];
|
|
||||||
|
|
||||||
unpack(String::from(input), String::from(output));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unpack(input: String, output: String) {
|
|
||||||
let file = File::open(input).unwrap();
|
|
||||||
let metadata = file.metadata().unwrap();
|
|
||||||
|
|
||||||
let mut reader = BufReader::new(file);
|
|
||||||
let mut list: Vec<ListElement> = Vec::new();
|
|
||||||
|
|
||||||
// Считываем заголовок файла
|
|
||||||
let mut header_buffer = [0u8; 16];
|
|
||||||
reader.seek(SeekFrom::Start(0)).unwrap();
|
|
||||||
reader.read_exact(&mut header_buffer).unwrap();
|
|
||||||
|
|
||||||
let file_header = FileHeader {
|
|
||||||
size: LittleEndian::read_u32(&header_buffer[12..16]),
|
|
||||||
total: LittleEndian::read_u32(&header_buffer[8..12]),
|
|
||||||
type1: LittleEndian::read_u32(&header_buffer[0..4]),
|
|
||||||
type2: LittleEndian::read_u32(&header_buffer[4..8]),
|
|
||||||
};
|
|
||||||
|
|
||||||
if file_header.type1 != 1936020046 || file_header.type2 != 256 {
|
|
||||||
panic!("this isn't NRes file");
|
|
||||||
}
|
|
||||||
|
|
||||||
if metadata.len() != file_header.size as u64 {
|
|
||||||
panic!("incorrect size")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Считываем список файлов
|
|
||||||
let list_files_start_position = file_header.size - (file_header.total * 64);
|
|
||||||
let list_files_size = file_header.total * 64;
|
|
||||||
|
|
||||||
let mut list_buffer = vec![0u8; list_files_size as usize];
|
|
||||||
reader
|
|
||||||
.seek(SeekFrom::Start(list_files_start_position as u64))
|
|
||||||
.unwrap();
|
|
||||||
reader.read_exact(&mut list_buffer).unwrap();
|
|
||||||
|
|
||||||
if !list_buffer.len().is_multiple_of(64) {
|
|
||||||
panic!("invalid files list")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0..(list_buffer.len() / 64) {
|
|
||||||
let from = i * 64;
|
|
||||||
let to = (i * 64) + 64;
|
|
||||||
let chunk: &[u8] = &list_buffer[from..to];
|
|
||||||
|
|
||||||
let element_list = ListElement {
|
|
||||||
extension: String::from_utf8_lossy(&chunk[0..4])
|
|
||||||
.trim_matches(char::from(0))
|
|
||||||
.to_string(),
|
|
||||||
index: LittleEndian::read_u32(&chunk[60..64]),
|
|
||||||
name: String::from_utf8_lossy(&chunk[20..56])
|
|
||||||
.trim_matches(char::from(0))
|
|
||||||
.to_string(),
|
|
||||||
position: LittleEndian::read_u32(&chunk[56..60]),
|
|
||||||
size: LittleEndian::read_u32(&chunk[12..16]),
|
|
||||||
unknown0: LittleEndian::read_u32(&chunk[4..8]),
|
|
||||||
unknown1: LittleEndian::read_u32(&chunk[8..12]),
|
|
||||||
unknown2: LittleEndian::read_u32(&chunk[16..20]),
|
|
||||||
};
|
|
||||||
|
|
||||||
list.push(element_list)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Распаковываем файлы в директорию
|
|
||||||
for element in &list {
|
|
||||||
let path = format!("{}/{}.{}", output, element.name, element.index);
|
|
||||||
let mut file = File::create(path).unwrap();
|
|
||||||
|
|
||||||
let mut file_buffer = vec![0u8; element.size as usize];
|
|
||||||
reader
|
|
||||||
.seek(SeekFrom::Start(element.position as u64))
|
|
||||||
.unwrap();
|
|
||||||
reader.read_exact(&mut file_buffer).unwrap();
|
|
||||||
|
|
||||||
file.write_all(&file_buffer).unwrap();
|
|
||||||
file_buffer.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выгрузка списка файлов в JSON
|
|
||||||
let path = format!("{}/{}", output, "index.json");
|
|
||||||
let file = File::create(path).unwrap();
|
|
||||||
let mut writer = BufWriter::new(file);
|
|
||||||
serde_json::to_writer_pretty(&mut writer, &list).unwrap();
|
|
||||||
writer.flush().unwrap();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user