Skip to content

Instantly share code, notes, and snippets.

@gustavderdrache
Created July 28, 2020 01:01
Show Gist options
  • Save gustavderdrache/ab40589767480091ac39049853c388e0 to your computer and use it in GitHub Desktop.
Save gustavderdrache/ab40589767480091ac39049853c388e0 to your computer and use it in GitHub Desktop.
TextToSkyrim scratch
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)
}
}
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()))
}
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