Created
July 28, 2020 01:01
-
-
Save gustavderdrache/ab40589767480091ac39049853c388e0 to your computer and use it in GitHub Desktop.
TextToSkyrim scratch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use bitflags::bitflags; | |
use nom::{ | |
bytes::streaming as bytes, combinator, error::ErrorKind, multi, number::streaming as number, | |
Err as NomErr, IResult, | |
}; | |
use std::{ | |
fs::File as StdFile, | |
io::{self, Read as _, Seek as _, SeekFrom}, | |
path::Path, | |
}; | |
use crate::name_hash::NameHash; | |
use crate::parse; | |
fn io_error(message: &str) -> io::Error { | |
io::Error::new(io::ErrorKind::Other, message) | |
} | |
#[repr(u32)] | |
#[derive(Debug, Eq, PartialEq)] | |
pub(crate) enum Version { | |
Skyrim = 0x68, | |
SkyrimSE = 0x69, | |
} | |
fn version(input: &[u8]) -> IResult<&[u8], Version> { | |
let (input, version) = number::le_u32(input)?; | |
let version = if version == Version::Skyrim as u32 { | |
Version::Skyrim | |
} else if version == Version::SkyrimSE as u32 { | |
Version::SkyrimSE | |
} else { | |
return Err(NomErr::Error((input, ErrorKind::Tag))); | |
}; | |
Ok((input, version)) | |
} | |
bitflags! { | |
pub(crate) struct ArchiveFlags: u32 { | |
const INCLUDE_DIRECTORY_NAMES = 0x0001; | |
const INCLUDE_FILE_NAMES = 0x0002; | |
const COMPRESSED = 0x0004; | |
const RETAIN_DIRECTORY_NAMES = 0x0010; | |
const RETAIN_FILE_NAME_OFFSETS = 0x0020; | |
const XBOX_360 = 0x0040; | |
const RETAIN_STRINGS_DURING_STARTUP = 0x0080; | |
const EMBED_FILE_NAMES = 0x0100; | |
const XMEM_CODEC = 0x0200; | |
} | |
} | |
fn archive_flags(input: &[u8]) -> IResult<&[u8], ArchiveFlags> { | |
let (input, flags) = number::le_u32(input)?; | |
let flags = | |
ArchiveFlags::from_bits(flags).ok_or_else(|| NomErr::Error((input, ErrorKind::Tag)))?; | |
Ok((input, flags)) | |
} | |
bitflags! { | |
pub(crate) struct FileFlags: u32 { | |
const MESHES = 0x0001; | |
const TEXTURES = 0x0002; | |
const MENUS = 0x0004; | |
const SOUNDS = 0x0008; | |
const VOICES = 0x0010; | |
const SHADERS = 0x0020; | |
const TREES = 0x0080; | |
const FONTS = 0x0100; | |
} | |
} | |
fn file_flags(input: &[u8]) -> IResult<&[u8], FileFlags> { | |
let (input, flags) = number::le_u32(input)?; | |
let flags = | |
FileFlags::from_bits(flags).ok_or_else(|| NomErr::Error((input, ErrorKind::Tag)))?; | |
Ok((input, flags)) | |
} | |
#[derive(Debug)] | |
pub(crate) struct Header { | |
pub(crate) version: Version, | |
pub(crate) offset: u32, | |
pub(crate) archive_flags: ArchiveFlags, | |
pub(crate) folder_count: u32, | |
pub(crate) file_count: u32, | |
pub(crate) total_folder_name_length: u32, | |
pub(crate) total_file_name_length: u32, | |
pub(crate) file_flags: FileFlags, | |
} | |
fn header(input: &[u8]) -> IResult<&[u8], Header> { | |
let (input, _) = bytes::tag(b"BSA\x00")(input)?; | |
let (input, version) = version(input)?; | |
let (input, offset) = number::le_u32(input)?; | |
let (input, archive_flags) = archive_flags(input)?; | |
let (input, folder_count) = number::le_u32(input)?; | |
let (input, file_count) = number::le_u32(input)?; | |
let (input, total_folder_name_length) = number::le_u32(input)?; | |
let (input, total_file_name_length) = number::le_u32(input)?; | |
let (input, file_flags) = file_flags(input)?; | |
let header = Header { | |
version, | |
offset, | |
archive_flags, | |
folder_count, | |
file_count, | |
total_folder_name_length, | |
total_file_name_length, | |
file_flags, | |
}; | |
Ok((input, header)) | |
} | |
#[derive(Debug)] | |
pub(crate) struct Directory { | |
pub(crate) name_hash: NameHash, | |
pub(crate) count: u32, | |
pub(crate) offset: u64, | |
pub(crate) name: Option<String>, | |
pub(crate) files: Vec<File>, | |
} | |
fn directory_header(input: &[u8]) -> IResult<&[u8], Directory> { | |
let (input, name_hash) = parse::name_hash(input)?; | |
let (input, count) = number::le_u32(input)?; | |
let (input, offset) = number::le_u32(input)?; | |
let header = Directory { | |
name_hash, | |
count, | |
offset: offset.into(), | |
name: None, | |
files: Vec::new(), | |
}; | |
Ok((input, header)) | |
} | |
fn directory_header_se(input: &[u8]) -> IResult<&[u8], Directory> { | |
let (input, name_hash) = parse::name_hash(input)?; | |
let (input, count) = number::le_u32(input)?; | |
let (input, _) = number::le_u32(input)?; | |
let (input, offset) = number::le_u64(input)?; | |
let header = Directory { | |
name_hash, | |
count, | |
offset, | |
name: None, | |
files: Vec::new(), | |
}; | |
Ok((input, header)) | |
} | |
#[derive(Debug)] | |
pub(crate) struct File { | |
pub(crate) name_hash: NameHash, | |
pub(crate) size: u32, | |
pub(crate) offset: u32, | |
pub(crate) name: Option<String>, | |
} | |
fn file(input: &[u8]) -> IResult<&[u8], File> { | |
let (input, name_hash) = parse::name_hash(input)?; | |
let (input, size) = number::le_u32(input)?; | |
let (input, offset) = number::le_u32(input)?; | |
let file = File { | |
name_hash, | |
size, | |
offset, | |
name: None, | |
}; | |
Ok((input, file)) | |
} | |
fn directory_data<'a, 'b>( | |
input: &'a [u8], | |
directory: &'b mut Directory, | |
has_directory_names: bool, | |
) -> IResult<&'a [u8], ()> { | |
let (input, name) = if has_directory_names { | |
combinator::map(parse::bzstring, Some)(input)? | |
} else { | |
(input, None) | |
}; | |
let (input, files) = multi::count(file, directory.count as usize)(input)?; | |
directory.name = name; | |
directory.files = files; | |
Ok((input, ())) | |
} | |
#[derive(Debug)] | |
pub(crate) struct Bsa { | |
pub(crate) file: StdFile, | |
pub(crate) header: Header, | |
pub(crate) directories: Vec<Directory>, | |
} | |
impl Bsa { | |
pub(crate) fn open(path: impl AsRef<Path>) -> io::Result<Self> { | |
const BUFFER_SIZE: usize = 1024 * 1024; | |
let mut file = StdFile::open(path)?; | |
let mut count = 1; | |
let (header, directories) = 'outer: loop { | |
macro_rules! parse_result { | |
($result:expr) => { | |
match $result { | |
Ok((input, value)) => (input, value), | |
Err(NomErr::Incomplete(_)) => { | |
count += 1; | |
continue 'outer; | |
} | |
Err(_) => { | |
return Err(io_error("Failed to parse BSA")); | |
} | |
} | |
}; | |
} | |
let mut buffer = vec![0u8; count * BUFFER_SIZE]; | |
file.seek(SeekFrom::Start(0))?; | |
file.read_exact(&mut buffer)?; | |
let input = &buffer; | |
let (input, header) = parse_result!(header(input)); | |
let parser = match header.version { | |
Version::Skyrim => directory_header, | |
Version::SkyrimSE => directory_header_se, | |
}; | |
let has_directory_names = header | |
.archive_flags | |
.contains(ArchiveFlags::INCLUDE_DIRECTORY_NAMES); | |
let (input, mut directories) = | |
parse_result!(multi::count(parser, header.folder_count as usize)(input)); | |
let mut input = input; | |
for directory in &mut directories { | |
let result = parse_result!(directory_data(input, directory, has_directory_names)); | |
input = result.0; | |
} | |
if header | |
.archive_flags | |
.contains(ArchiveFlags::INCLUDE_FILE_NAMES) | |
{ | |
for directory in &mut directories { | |
for file in &mut directory.files { | |
let (next_input, name) = parse_result!(parse::bstring(input)); | |
input = next_input; | |
file.name = Some(name); | |
} | |
} | |
} | |
break (header, directories); | |
}; | |
Ok(Bsa { | |
file, | |
header, | |
directories, | |
}) | |
} | |
pub(crate) fn find_file( | |
&mut self, | |
directory_name: &str, | |
file_name: &str, | |
) -> io::Result<Vec<u8>> { | |
if self | |
.header | |
.archive_flags | |
.contains(ArchiveFlags::EMBED_FILE_NAMES) | |
{ | |
return Err(io_error("Can't handle embedded file names")); | |
} | |
let directory = self | |
.directories | |
.iter() | |
.find(|dir| dir.name.as_ref().map(AsRef::as_ref) == Some(directory_name)) | |
.ok_or_else(|| io_error("No such directory"))?; | |
let file = directory | |
.files | |
.iter() | |
.find(|file| file.name.as_ref().map(AsRef::as_ref) == Some(file_name)) | |
.ok_or_else(|| io_error("No such file"))?; | |
let mut buffer = vec![0u8; file.size as usize]; | |
self.file.seek(SeekFrom::Start(file.offset.into()))?; | |
self.file.read_exact(&mut buffer)?; | |
Ok(buffer) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use encoding_rs::WINDOWS_1252; | |
use nom::{ | |
bytes::streaming as bytes, error::ErrorKind, number::streaming as number, Err as NomErr, | |
IResult, | |
}; | |
use crate::name_hash::NameHash; | |
pub(crate) fn decode(bytes: &[u8]) -> String { | |
let owned = bytes.to_owned(); | |
String::from_utf8(owned).unwrap_or_else(|_| WINDOWS_1252.decode(bytes).0.into()) | |
} | |
pub(crate) fn bstring(input: &[u8]) -> IResult<&[u8], String> { | |
let (input, string) = bytes::take_till(|c| c == 0x00)(input)?; | |
let string = decode(string); | |
let (input, _) = bytes::tag(b"\x00")(input)?; | |
Ok((input, string)) | |
} | |
pub(crate) fn bzstring(input: &[u8]) -> IResult<&[u8], String> { | |
let (input, length) = number::le_u8(input)?; | |
let length: usize = length.into(); | |
if length == 0 { | |
return Err(NomErr::Error((input, ErrorKind::Tag))); | |
} | |
let (input, string) = bytes::take(length)(input)?; | |
let string = decode(string); | |
Ok((input, string)) | |
} | |
pub(crate) fn name_hash(input: &[u8]) -> IResult<&[u8], NameHash> { | |
let (input, name_hash) = number::le_u64(input)?; | |
Ok((input, name_hash.into())) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use color_eyre::{eyre::eyre, Result}; | |
use nom::{ | |
bytes::complete as bytes, error::ErrorKind, multi, number::complete as number, | |
Err as NomErr, IResult, | |
}; | |
use crate::parse; | |
#[derive(Debug)] | |
struct TableHeader { | |
id: u32, | |
offset: usize, | |
} | |
fn table_header(input: &[u8]) -> IResult<&[u8], TableHeader> { | |
let (input, id) = number::le_u32(input)?; | |
let (input, offset) = number::le_u32(input)?; | |
let header = TableHeader { | |
id, | |
offset: offset as usize, | |
}; | |
Ok((input, header)) | |
} | |
#[derive(Debug)] | |
pub(crate) struct TableEntry { | |
pub(crate) id: u32, | |
pub(crate) data: String, | |
} | |
fn string_table(input: &[u8]) -> IResult<&[u8], Vec<TableEntry>> { | |
let buffer = input; | |
let (input, count) = number::le_u32(input)?; | |
let (input, _) = number::le_u32(input)?; | |
let count = count as usize; | |
let (input, headers) = multi::count(table_header, count)(input)?; | |
let offset = 8 * (count + 1); | |
let mut entries = Vec::new(); | |
for header in headers { | |
let offset = offset + header.offset; | |
let input = buffer | |
.get(offset..) | |
.ok_or_else(|| NomErr::Failure((buffer, ErrorKind::Eof)))?; | |
let (input, length) = number::le_u32(input)?; | |
let length = length as usize; | |
let (input, bytes) = bytes::take(length - 1)(input)?; | |
let (_, _) = bytes::tag(b"\x00")(input)?; | |
let data = parse::decode(bytes); | |
let entry = TableEntry { | |
id: header.id, | |
data, | |
}; | |
entries.push(entry); | |
} | |
Ok((input, entries)) | |
} | |
pub(crate) fn load_table(input: &[u8]) -> Result<Vec<TableEntry>> { | |
match string_table(input) { | |
Ok((_, data)) => Ok(data), | |
Err(_) => Err(eyre!("Failed to parse strings table")), | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment