summaryrefslogtreecommitdiff
path: root/tools/asm-processor/rust/src
diff options
context:
space:
mode:
Diffstat (limited to 'tools/asm-processor/rust/src')
-rw-r--r--tools/asm-processor/rust/src/main.rs518
-rw-r--r--tools/asm-processor/rust/src/postprocess.rs1312
-rw-r--r--tools/asm-processor/rust/src/preprocess.rs969
3 files changed, 2799 insertions, 0 deletions
diff --git a/tools/asm-processor/rust/src/main.rs b/tools/asm-processor/rust/src/main.rs
new file mode 100644
index 0000000..8e214ee
--- /dev/null
+++ b/tools/asm-processor/rust/src/main.rs
@@ -0,0 +1,518 @@
+mod postprocess;
+mod preprocess;
+
+use std::{
+ borrow::Cow,
+ ffi::OsString,
+ fmt::Display,
+ fs::{self, File},
+ io::Write,
+ path::{Path, PathBuf},
+ process::{exit, Command},
+ str::FromStr,
+};
+
+use anyhow::Result;
+use argp::{EarlyExit, FromArgs, HelpStyle};
+use encoding_rs::EUC_JP;
+use enum_map::{Enum, EnumMap};
+use temp_dir::TempDir;
+
+use postprocess::fixup_objfile;
+use preprocess::parse_source;
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug, Enum)]
+enum OutputSection {
+ Text,
+ Data,
+ Rodata,
+ Bss,
+}
+
+impl OutputSection {
+ fn as_str(&self) -> &'static str {
+ match self {
+ OutputSection::Text => ".text",
+ OutputSection::Data => ".data",
+ OutputSection::Rodata => ".rodata",
+ OutputSection::Bss => ".bss",
+ }
+ }
+}
+
+impl Display for OutputSection {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.as_str())
+ }
+}
+
+#[derive(Clone, Debug)]
+struct Function {
+ text_glabels: Vec<String>,
+ asm_conts: Vec<String>,
+ late_rodata_dummy_bytes: Vec<[u8; 4]>,
+ jtbl_rodata_size: usize,
+ late_rodata_asm_conts: Vec<String>,
+ fn_desc: String,
+ data: EnumMap<OutputSection, (Option<String>, usize)>,
+}
+
+#[derive(Clone, Copy, Debug)]
+enum Encoding {
+ Latin1,
+ Custom(&'static encoding_rs::Encoding),
+}
+
+impl Encoding {
+ fn encode<'a>(&self, s: &'a str) -> Result<Cow<'a, [u8]>> {
+ match self {
+ Encoding::Latin1 => {
+ if encoding_rs::mem::is_str_latin1(s) {
+ return Ok(encoding_rs::mem::encode_latin1_lossy(s));
+ }
+ }
+ Encoding::Custom(enc) => {
+ if *enc == EUC_JP {
+ let s = s.replace("〜", "~");
+ let (ret, _, failed) = enc.encode(&s);
+ if !failed {
+ return Ok(Cow::Owned(ret.into_owned()));
+ }
+ } else {
+ let (ret, _, failed) = enc.encode(s);
+ if !failed {
+ return Ok(ret);
+ }
+ }
+ }
+ }
+ Err(anyhow::anyhow!("Failed to encode string: {}", s))
+ }
+
+ fn decode<'a>(&self, bytes: &'a [u8]) -> Result<Cow<'a, str>> {
+ match self {
+ Encoding::Latin1 => Ok(encoding_rs::mem::decode_latin1(bytes)),
+ Encoding::Custom(enc) => {
+ let (ret, _, failed) = enc.decode(bytes);
+ if !failed {
+ Ok(ret)
+ } else {
+ Err(anyhow::anyhow!("Failed to decode string: {}", ret))
+ }
+ }
+ }
+ }
+}
+
+impl FromStr for Encoding {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s == "latin1" {
+ Ok(Encoding::Latin1)
+ } else {
+ match encoding_rs::Encoding::for_label(s.as_bytes()) {
+ Some(enc) => Ok(Encoding::Custom(enc)),
+ None => Err(format!("Unsupported encoding: {}", s)),
+ }
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+enum ConvertStatics {
+ No,
+ Local,
+ Global,
+ GlobalWithFilename,
+}
+
+impl FromStr for ConvertStatics {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(match s {
+ "no" => ConvertStatics::No,
+ "local" => ConvertStatics::Local,
+ "global" => ConvertStatics::Global,
+ "global-with-filename" => ConvertStatics::GlobalWithFilename,
+ _ => return Err("invalid value for symbol visibility".into()),
+ })
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+enum OptLevel {
+ O0,
+ O1,
+ O2,
+ G,
+ G3,
+}
+
+/// Pre-process .c files and post-process .o files to enable embedding MIPS assembly into IDO-compiled C.
+#[derive(FromArgs)]
+struct AsmProcArgs {
+ /// Path to a file containing a prelude to the assembly file (with .set and .macro directives, e.g.)
+ #[argp(option, arg_name = "FILE")]
+ asm_prelude: Option<PathBuf>,
+
+ /// Input encoding (default: latin1)
+ #[argp(
+ option,
+ default = "Encoding::Latin1",
+ from_str_fn(FromStr::from_str),
+ arg_name = "ENCODING"
+ )]
+ input_enc: Encoding,
+
+ /// Output encoding (default: latin1)
+ #[argp(
+ option,
+ default = "Encoding::Latin1",
+ from_str_fn(FromStr::from_str),
+ arg_name = "ENCODING"
+ )]
+ output_enc: Encoding,
+
+ /// Drop mdebug and gptab sections
+ #[argp(switch)]
+ drop_mdebug_gptab: bool,
+
+ /// Change symbol visibility for static variables. Mode must be one of:
+ /// no, local, global, global-with-filename (default: local)
+ #[argp(
+ option,
+ default = "ConvertStatics::Local",
+ from_str_fn(FromStr::from_str),
+ arg_name = "MODE"
+ )]
+ convert_statics: ConvertStatics,
+
+ /// Force processing of files without GLOBAL_ASM blocks
+ #[argp(switch)]
+ force: bool,
+
+ /// Emit temporary files to this directory
+ #[argp(option, arg_name = "DIR")]
+ keep_preprocessed: Option<PathBuf>,
+
+ /// Replace floats with their encoded hexadecimal representation in CutsceneData data
+ #[argp(switch)]
+ encode_cutscene_data_floats: bool,
+
+ /// Don't generate a .d make dependency file
+ #[argp(switch)]
+ no_dep_file: bool,
+
+ #[argp(positional, greedy)]
+ rest: Vec<String>,
+}
+
+struct CompileOpts {
+ opt: OptLevel,
+ framepointer: bool,
+ mips1: bool,
+ kpic: bool,
+ pascal: bool,
+}
+
+fn extract_compiler_input_output(
+ compile_args: &[String],
+) -> Result<(PathBuf, PathBuf, Vec<String>), &'static str> {
+ let mut compile_args: Vec<String> = compile_args.to_vec();
+ let out_ind = compile_args
+ .iter()
+ .position(|arg| arg == "-o")
+ .ok_or("missing -o argument")?;
+ let out_filename = compile_args
+ .get(out_ind + 1)
+ .ok_or("missing argument after -o")?
+ .clone();
+ compile_args.remove(out_ind + 1);
+ compile_args.remove(out_ind);
+
+ let in_file_str = compile_args
+ .last()
+ .ok_or("missing input file argument")?
+ .clone();
+ compile_args.pop();
+
+ let out_file: PathBuf = out_filename.into();
+ let in_file: PathBuf = in_file_str.into();
+
+ Ok((in_file, out_file, compile_args))
+}
+
+fn parse_compile_args(
+ compile_args: &[String],
+ in_file: &Path,
+) -> Result<CompileOpts, &'static str> {
+ let mut opt_flags = vec![];
+ for x in compile_args {
+ opt_flags.push(match x.as_str() {
+ "-g" => OptLevel::G,
+ "-O0" => OptLevel::O0,
+ "-O1" => OptLevel::O1,
+ "-O2" => OptLevel::O2,
+ _ => continue,
+ });
+ }
+
+ if opt_flags.len() != 1 {
+ return Err("exactly one of -g/-O0/-O1/-O2 must be passed");
+ }
+ let mut opt = opt_flags[0];
+
+ let mips1 = !compile_args.contains(&"-mips2".to_string());
+ let framepointer = compile_args.contains(&"-framepointer".to_string());
+ let kpic = compile_args.contains(&"-KPIC".to_string());
+ if compile_args.contains(&"-g3".to_string()) {
+ if opt != OptLevel::O2 {
+ return Err("-g3 is only supported together with -O2");
+ }
+ opt = OptLevel::G3;
+ }
+
+ if mips1 && (!matches!(opt, OptLevel::O1 | OptLevel::O2) || framepointer) {
+ return Err("-mips1 is only supported together with -O1 or -O2");
+ }
+
+ let in_file_str = in_file.to_string_lossy();
+ let pascal = in_file_str.ends_with(".p")
+ || in_file_str.ends_with(".pas")
+ || in_file_str.ends_with(".pp");
+
+ if pascal && !matches!(opt, OptLevel::O1 | OptLevel::O2 | OptLevel::G3) {
+ return Err("Pascal is only supported together with -O1, -O2 or -O2 -g3");
+ }
+
+ Ok(CompileOpts {
+ opt,
+ framepointer,
+ mips1,
+ kpic,
+ pascal,
+ })
+}
+
+fn parse_rest(rest: &[String]) -> Option<(&[String], &[String], &[String])> {
+ let mut iter = rest.splitn(3, |x| *x == "--");
+ let compiler = iter.next()?;
+ let assembler = iter.next()?;
+ let compile_args = iter.next()?;
+ assert!(iter.next().is_none());
+ Some((compiler, assembler, compile_args))
+}
+
+struct ParsedArgs {
+ args: AsmProcArgs,
+ compiler: Vec<String>,
+ assembler: Vec<String>,
+ compile_args: Vec<String>,
+ in_file: PathBuf,
+ out_file: PathBuf,
+ opts: CompileOpts,
+}
+
+/// Parse command line arguments while allowing --a=b syntax.
+///
+/// This provides backward compatibility with Python argparse.
+fn from_args_allow_eq(progname: &[&str], args: &[OsString]) -> Result<AsmProcArgs, EarlyExit> {
+ let base_res = AsmProcArgs::from_args(progname, args);
+ if base_res.is_ok() {
+ return base_res;
+ }
+
+ // Try splitting up = chars successively, until we get a valid parse.
+ // This ensures we don't impact the compiler/assembler parts.
+ //
+ // Technically this might end up splitting --a --b=c into --a --b c even
+ // where --a is a flag that takes an argument --b=c, but this seems unlikely
+ // with our use case.
+ let mut i = 0;
+ let mut args = args.to_vec();
+ while i < args.len() {
+ let arg = args[i].as_encoded_bytes();
+ if arg.starts_with(b"--") {
+ if let Some(eq) = arg.iter().position(|&x| x == b'=') {
+ // SAFETY: splitting on ASCII still results in valid encoded bytes
+ let before = unsafe { OsString::from_encoded_bytes_unchecked(arg[0..eq].into()) };
+ let after = unsafe { OsString::from_encoded_bytes_unchecked(arg[eq + 1..].into()) };
+ args.splice(i..i + 1, [before, after]);
+ let new_res = AsmProcArgs::from_args(progname, &args);
+ if new_res.is_ok() {
+ return new_res;
+ }
+ i += 1;
+ }
+ }
+ i += 1;
+ }
+
+ base_res
+}
+
+fn parse_args_or_exit() -> ParsedArgs {
+ let argv: Vec<_> = std::env::args_os().collect();
+ let help_style = HelpStyle {
+ short_usage: true,
+ ..HelpStyle::default()
+ };
+ let progname = argv[0].to_string_lossy();
+
+ let args = from_args_allow_eq(&[&progname], &argv[1..]).unwrap_or_else(|early_exit| {
+ exit(match early_exit {
+ EarlyExit::Help(help) => {
+ println!(
+ "{}",
+ help.generate(&help_style).replace(
+ "[rest...]",
+ "<compiler...> -- <assembler...> -- <compiler flags...>"
+ )
+ );
+ 0
+ }
+ EarlyExit::Err(err) => {
+ eprintln!("{}\nRun {} --help for more information.", err, progname);
+ 1
+ }
+ })
+ });
+
+ let Some((compiler, assembler, compile_args)) = parse_rest(&args.rest) else {
+ eprintln!(
+ "Usage: {} [options] <compiler...> -- <assembler...> -- <compiler flags...>",
+ progname
+ );
+ eprintln!("Run {} --help for more information.", progname);
+ exit(1);
+ };
+
+ let (in_file, out_file, compile_args) = extract_compiler_input_output(compile_args)
+ .unwrap_or_else(|err| {
+ eprintln!("Failed to parse compiler flags: {}", err);
+ exit(1);
+ });
+
+ let opts = parse_compile_args(&compile_args, &in_file).unwrap_or_else(|err| {
+ eprintln!("Unsupported compiler flags: {}", err);
+ exit(1);
+ });
+
+ let compiler = compiler.into();
+ let assembler = assembler.into();
+
+ ParsedArgs {
+ args,
+ compiler,
+ assembler,
+ compile_args,
+ in_file,
+ out_file,
+ opts,
+ }
+}
+
+fn main() -> Result<()> {
+ let ParsedArgs {
+ args,
+ compiler,
+ assembler,
+ compile_args,
+ in_file,
+ out_file,
+ opts,
+ } = parse_args_or_exit();
+
+ let assembler_sh = assembler
+ .iter()
+ .map(|s| shlex::try_quote(s).unwrap().into_owned())
+ .collect::<Vec<String>>()
+ .join(" ");
+
+ let in_dir = fs::canonicalize(in_file.parent().unwrap().join("."))?;
+
+ let temp_dir = TempDir::with_prefix("asm_processor")?;
+ let preprocessed_filename = format!(
+ "preprocessed_{}",
+ in_file.file_name().unwrap().to_str().unwrap()
+ );
+ let preprocessed_path = temp_dir.path().join(&preprocessed_filename);
+ let mut preprocessed_file = File::create(&preprocessed_path)?;
+
+ let res = parse_source(&in_file, &args, &opts, true)?;
+ preprocessed_file.write_all(&res.output)?;
+
+ if let Some(keep_output_dir) = &args.keep_preprocessed {
+ fs::create_dir_all(keep_output_dir)?;
+ fs::copy(
+ &preprocessed_path,
+ keep_output_dir.join(&preprocessed_filename),
+ )?;
+ }
+
+ // Run compiler
+ let mut compile_command = Command::new(&compiler[0]);
+ compile_command
+ .args(&compile_args)
+ .arg("-I")
+ .arg(in_dir)
+ .arg("-o")
+ .arg(&out_file)
+ .arg(&preprocessed_path);
+
+ match compile_command.status() {
+ Ok(status) if status.success() => {}
+ _ => {
+ return Err(anyhow::anyhow!(
+ "Failed to compile file {}. Command line:\n\n{:?}\n",
+ in_file.display(),
+ compile_command
+ ));
+ }
+ }
+
+ if !res.functions.is_empty() || args.force {
+ let prelude_str;
+ let asm_prelude = match &args.asm_prelude {
+ Some(prelude) => {
+ if let Ok(res) = fs::read_to_string(prelude) {
+ prelude_str = res;
+ &prelude_str
+ } else {
+ return Err(anyhow::anyhow!("Failed to read asm prelude"));
+ }
+ }
+ None => include_str!("../../prelude.inc"),
+ };
+
+ fixup_objfile(
+ &out_file,
+ &res.functions,
+ asm_prelude,
+ &assembler_sh,
+ &args.output_enc,
+ args.drop_mdebug_gptab,
+ args.convert_statics,
+ )?;
+ }
+
+ if !res.deps.is_empty() && !args.no_dep_file {
+ let deps_file = out_file.with_extension("asmproc.d");
+ let mut deps_file = File::create(&deps_file)?;
+
+ writeln!(
+ deps_file,
+ "{}: {}",
+ out_file.to_str().unwrap(),
+ res.deps.join(" \\\n ")
+ )?;
+
+ for dep in res.deps {
+ writeln!(deps_file, "\n{dep}:")?;
+ }
+ }
+
+ Ok(())
+}
diff --git a/tools/asm-processor/rust/src/postprocess.rs b/tools/asm-processor/rust/src/postprocess.rs
new file mode 100644
index 0000000..60b782b
--- /dev/null
+++ b/tools/asm-processor/rust/src/postprocess.rs
@@ -0,0 +1,1312 @@
+use anyhow::Result;
+use std::{
+ cmp::Ordering,
+ collections::{HashMap, HashSet},
+ fs::{self, File},
+ io::{self, BufWriter, Cursor, Seek, SeekFrom, Write},
+ path::Path,
+ process::Command,
+};
+
+use binrw::{binrw, BinRead, BinResult, BinWrite, Endian};
+use enum_map::EnumMap;
+use temp_dir::TempDir;
+
+use crate::{ConvertStatics, Encoding, Function, OutputSection};
+
+const EI_NIDENT: usize = 16;
+const EI_CLASS: usize = 4;
+const EI_DATA: usize = 5;
+
+const SHN_UNDEF: usize = 0;
+const SHN_ABS: usize = 0xfff1;
+const SHN_XINDEX: usize = 0xffff;
+
+const STT_OBJECT: u8 = 1;
+const STT_FUNC: u8 = 2;
+
+const STB_LOCAL: u8 = 0;
+const STB_GLOBAL: u8 = 1;
+
+const STV_DEFAULT: u8 = 0;
+
+const SHT_NULL: u32 = 0;
+const SHT_SYMTAB: u32 = 2;
+const SHT_STRTAB: u32 = 3;
+const SHT_RELA: u32 = 4;
+const SHT_NOBITS: u32 = 8;
+const SHT_REL: u32 = 9;
+const SHT_MIPS_GPTAB: u32 = 0x70000003;
+const SHT_MIPS_DEBUG: u32 = 0x70000005;
+
+const SHF_LINK_ORDER: u32 = 0x80;
+
+const MIPS_DEBUG_ST_STATIC: usize = 2;
+const MIPS_DEBUG_ST_PROC: usize = 6;
+const MIPS_DEBUG_ST_BLOCK: usize = 7;
+const MIPS_DEBUG_ST_END: usize = 8;
+const MIPS_DEBUG_ST_FILE: usize = 11;
+const MIPS_DEBUG_ST_STATIC_PROC: usize = 14;
+const MIPS_DEBUG_ST_STRUCT: usize = 26;
+const MIPS_DEBUG_ST_UNION: usize = 27;
+const MIPS_DEBUG_ST_ENUM: usize = 28;
+
+#[binrw]
+struct ElfHeader {
+ e_ident: [u8; EI_NIDENT],
+ e_type: u16,
+ e_machine: u16,
+ e_version: u32,
+ e_entry: u32,
+ e_phoff: u32,
+ e_shoff: u32,
+ e_flags: u32,
+ e_ehsize: u16,
+ e_phentsize: u16,
+ e_phnum: u16,
+ e_shentsize: u16,
+ e_shnum: u16,
+ e_shstrndx: u16,
+}
+
+impl ElfHeader {
+ const SIZE: usize = 52;
+
+ fn new(data: &[u8], endian: Endian) -> BinResult<Self> {
+ let mut cursor = Cursor::new(data);
+
+ let header = Self::read_options(&mut cursor, endian, ())?;
+
+ assert_eq!(header.e_ident[EI_CLASS], 1, "ELF must be 32-bit");
+ assert_eq!(header.e_type, 1, "ELF must be relocatable");
+ assert_eq!(header.e_machine, 8, "ELF must be MIPS 1");
+ assert_eq!(header.e_phoff, 0, "ELF must not have program headers");
+ assert_ne!(header.e_shoff, 0, "ELF must have section headers");
+ assert_ne!(
+ header.e_shstrndx, SHN_UNDEF as u16,
+ "ELF must have a section header string table"
+ );
+
+ Ok(header)
+ }
+
+ fn to_bin(&self, endian: Endian) -> [u8; Self::SIZE] {
+ let mut rv = [0; Self::SIZE];
+ let mut cursor = Cursor::new(rv.as_mut_slice());
+
+ self.write_options(&mut cursor, endian, ()).unwrap();
+ rv
+ }
+}
+
+#[binrw]
+struct SymbolData {
+ st_name: u32,
+ st_value: u32,
+ st_size: u32,
+ st_info: u8,
+ st_other: u8,
+ st_shndx: u16,
+}
+
+#[derive(Clone)]
+struct Symbol {
+ st_name: usize,
+ st_value: usize,
+ st_size: usize,
+ st_shndx: usize,
+ st_type: u8,
+ st_bind: u8,
+ st_visibility: u8,
+ name: Vec<u8>,
+}
+
+impl Symbol {
+ fn new(data: &[u8], strtab: &Section, endian: Endian) -> BinResult<Self> {
+ let mut cursor = Cursor::new(data);
+
+ let data = SymbolData::read_options(&mut cursor, endian, ())?;
+ if data.st_shndx == SHN_XINDEX as u16 {
+ panic!("too many sections (SHN_XINDEX not supported)");
+ }
+ let st_type = data.st_info & 0xf;
+ let st_bind = data.st_info >> 4;
+ let st_visibility = data.st_other & 0x3;
+ let name = strtab.lookup_str(data.st_name as usize);
+
+ Ok(Self {
+ st_name: data.st_name as usize,
+ st_value: data.st_value as usize,
+ st_size: data.st_size as usize,
+ st_shndx: data.st_shndx as usize,
+ st_type,
+ st_bind,
+ st_visibility,
+ name,
+ })
+ }
+
+ fn to_bin(&self) -> Vec<u8> {
+ let mut rv = vec![];
+ let mut cursor = Cursor::new(&mut rv);
+
+ SymbolData {
+ st_name: self.st_name as u32,
+ st_value: self.st_value as u32,
+ st_size: self.st_size as u32,
+ st_info: self.st_bind << 4 | self.st_type,
+ st_other: self.st_visibility,
+ st_shndx: self.st_shndx as u16,
+ }
+ .write_options(&mut cursor, Endian::Big, ())
+ .unwrap();
+ rv
+ }
+}
+
+#[derive(Clone)]
+struct Relocation {
+ r_offset: usize,
+ sym_index: usize,
+ rel_type: u32,
+ r_addend: Option<u32>,
+}
+
+impl Relocation {
+ fn new(data: &[u8], sh_type: u32, endian: Endian) -> BinResult<Self> {
+ let mut cursor = Cursor::new(data);
+
+ let r_offset = u32::read_options(&mut cursor, endian, ())? as usize;
+ let r_info = u32::read_options(&mut cursor, endian, ())?;
+ let r_addend = if sh_type == SHT_REL {
+ None
+ } else {
+ Some(u32::read_options(&mut cursor, endian, ())?)
+ };
+
+ let sym_index = (r_info >> 8) as usize;
+ let rel_type = r_info & 0xff;
+
+ Ok(Self {
+ r_offset,
+ sym_index,
+ rel_type,
+ r_addend,
+ })
+ }
+
+ fn to_bin(&self, endian: Endian) -> Vec<u8> {
+ let mut rv = vec![];
+ let mut cursor = Cursor::new(&mut rv);
+ let r_offset = self.r_offset as u32;
+ let r_info = ((self.sym_index as u32) << 8) | self.rel_type;
+ r_offset.write_options(&mut cursor, endian, ()).unwrap();
+ r_info.write_options(&mut cursor, endian, ()).unwrap();
+ self.r_addend
+ .write_options(&mut cursor, endian, ())
+ .unwrap();
+ rv
+ }
+}
+
+#[binrw]
+struct Hdrr {
+ magic: u16,
+ vstamp: u16,
+ iline_max: u32,
+ cb_line: u32,
+ cb_line_offset: u32,
+ idn_max: u32,
+ cb_dn_offset: u32,
+ ipd_max: u32,
+ cb_pd_offset: u32,
+ isym_max: u32,
+ cb_sym_offset: u32,
+ iopt_max: u32,
+ cb_opt_offset: u32,
+ iaux_max: u32,
+ cb_aux_offset: u32,
+ iss_max: u32,
+ cb_ss_offset: u32,
+ iss_ext_max: u32,
+ cb_ss_ext_offset: u32,
+ ifd_max: u32,
+ cb_fd_offset: u32,
+ crfd: u32,
+ cb_rfd_offset: u32,
+ iext_max: u32,
+ cb_ext_offset: u32,
+}
+
+impl Hdrr {
+ const SIZE: usize = 96;
+}
+
+#[binrw]
+#[derive(Clone)]
+struct SectionHeader {
+ sh_name: u32,
+ sh_type: u32,
+ sh_flags: u32,
+ sh_addr: u32,
+ sh_offset: u32,
+ sh_size: u32,
+ sh_link: u32,
+ sh_info: u32,
+ sh_addralign: u32,
+ sh_entsize: u32,
+}
+
+impl SectionHeader {
+ const SIZE: usize = 40;
+}
+
+#[derive(Clone)]
+struct Section {
+ header: SectionHeader,
+ data: Vec<u8>,
+ index: usize,
+ relocated_by: Vec<usize>,
+ relocations: Vec<Relocation>,
+ name: String,
+}
+
+impl Section {
+ fn new(data: &[u8], other_data: &[u8], index: usize, endian: Endian) -> BinResult<Self> {
+ let mut cursor = Cursor::new(data);
+
+ let header = SectionHeader::read_options(&mut cursor, endian, ())?;
+ assert!(header.sh_flags & SHF_LINK_ORDER == 0);
+ if header.sh_entsize != 0 {
+ assert_eq!(header.sh_size % header.sh_entsize, 0);
+ }
+
+ let data = if header.sh_type == SHT_NOBITS {
+ vec![]
+ } else {
+ other_data[header.sh_offset as usize..(header.sh_offset + header.sh_size) as usize]
+ .to_vec()
+ };
+ Ok(Self {
+ header,
+ data,
+ index,
+ relocated_by: vec![],
+ relocations: vec![],
+ name: "".into(),
+ })
+ }
+
+ fn from_parts(
+ sh_name: u32,
+ fields: &HeaderFields,
+ data: &[u8],
+ index: usize,
+ endian: Endian,
+ ) -> Self {
+ let header = SectionHeader {
+ sh_name,
+ sh_type: fields.sh_type,
+ sh_flags: fields.sh_flags,
+ sh_addr: 0,
+ sh_offset: 0,
+ sh_size: data.len() as u32,
+ sh_link: fields.sh_link,
+ sh_info: fields.sh_info,
+ sh_addralign: fields.sh_addralign,
+ sh_entsize: fields.sh_entsize,
+ };
+
+ let mut rv = [0; SectionHeader::SIZE];
+ let mut cursor = Cursor::new(rv.as_mut_slice());
+
+ header.write_options(&mut cursor, endian, ()).unwrap();
+
+ Self::new(&rv, data, index, endian).unwrap()
+ }
+
+ fn lookup_str(&self, index: usize) -> Vec<u8> {
+ assert_eq!(self.header.sh_type, SHT_STRTAB);
+ let to = self.data[index..]
+ .iter()
+ .position(|&x| x == 0)
+ .expect("bad strtab index")
+ + index;
+ self.data[index..to].to_owned()
+ }
+
+ fn add_str(&mut self, string: &[u8]) -> u32 {
+ assert_eq!(self.header.sh_type, SHT_STRTAB);
+ let index = self.data.len() as u32;
+
+ self.data.extend_from_slice(string);
+ self.data.push(0);
+ index
+ }
+
+ fn is_rel(&self) -> bool {
+ self.header.sh_type == SHT_REL || self.header.sh_type == SHT_RELA
+ }
+
+ fn header_to_bin(&mut self, endian: Endian) -> [u8; SectionHeader::SIZE] {
+ if self.header.sh_type != SHT_NOBITS {
+ self.header.sh_size = self.data.len() as u32;
+ }
+
+ let mut rv = [0; SectionHeader::SIZE];
+ let mut cursor = Cursor::new(rv.as_mut_slice());
+
+ self.header.write_options(&mut cursor, endian, ()).unwrap();
+
+ rv
+ }
+
+ fn init_relocs(&mut self, endian: Endian) {
+ assert!(self.is_rel());
+
+ let mut entries = vec![];
+ for i in (0..self.header.sh_size).step_by(self.header.sh_entsize as usize) {
+ entries.push(
+ Relocation::new(
+ &self.data[i as usize..(i + self.header.sh_entsize) as usize],
+ self.header.sh_type,
+ endian,
+ )
+ .unwrap(),
+ );
+ }
+ self.relocations = entries;
+ }
+
+ fn relocate_mdebug(&mut self, original_offset: u32, endian: Endian) {
+ assert_eq!(self.header.sh_type, SHT_MIPS_DEBUG);
+ let shift_by = self.header.sh_offset.wrapping_sub(original_offset);
+
+ let mut hdrr = Hdrr::read_options(&mut Cursor::new(&self.data), endian, ()).unwrap();
+
+ assert_eq!(hdrr.magic, 0x7009);
+
+ let relocate = |a, b: &mut u32| {
+ if a != 0 {
+ *b = b.wrapping_add(shift_by);
+ }
+ };
+ relocate(hdrr.cb_line, &mut hdrr.cb_line_offset);
+ relocate(hdrr.idn_max, &mut hdrr.cb_dn_offset);
+ relocate(hdrr.ipd_max, &mut hdrr.cb_pd_offset);
+ relocate(hdrr.isym_max, &mut hdrr.cb_sym_offset);
+ relocate(hdrr.iopt_max, &mut hdrr.cb_opt_offset);
+ relocate(hdrr.iaux_max, &mut hdrr.cb_aux_offset);
+ relocate(hdrr.iss_max, &mut hdrr.cb_ss_offset);
+ relocate(hdrr.iss_ext_max, &mut hdrr.cb_ss_ext_offset);
+ relocate(hdrr.ifd_max, &mut hdrr.cb_fd_offset);
+ relocate(hdrr.crfd, &mut hdrr.cb_rfd_offset);
+ relocate(hdrr.iext_max, &mut hdrr.cb_ext_offset);
+
+ let mut new_data = [0; Hdrr::SIZE];
+ let mut cursor = Cursor::new(new_data.as_mut_slice());
+ hdrr.write_options(&mut cursor, endian, ()).unwrap();
+
+ self.data = new_data.to_vec();
+ }
+}
+
+struct ElfFile {
+ data: Vec<u8>,
+ endian: Endian,
+ header: ElfHeader,
+ sections: Vec<Section>,
+ symbol_entries: Vec<Symbol>,
+ symtab: usize,
+ sym_strtab: usize,
+}
+
+struct HeaderFields {
+ sh_type: u32,
+ sh_flags: u32,
+ sh_link: u32,
+ sh_info: u32,
+ sh_addralign: u32,
+ sh_entsize: u32,
+}
+
+impl ElfFile {
+ fn new(data: &[u8]) -> BinResult<Self> {
+ let data = data.to_vec();
+ assert_eq!(data[..4], [0x7f, b'E', b'L', b'F']);
+
+ let endian: Endian = if data[EI_DATA] == 1 {
+ Endian::Little
+ } else if data[5] == 2 {
+ Endian::Big
+ } else {
+ panic!("Invalid ELF endianness");
+ };
+ let header = ElfHeader::new(&data[..ElfHeader::SIZE], endian).unwrap();
+ let offset = header.e_shoff as usize;
+ let size = header.e_shentsize as usize;
+ let null_section = Section::new(&data[offset..offset + size], &data, 0, endian).unwrap();
+ let num_sections = if header.e_shnum == 0 {
+ null_section.header.sh_size as usize
+ } else {
+ header.e_shnum as usize
+ };
+ let mut sections = vec![null_section];
+
+ for i in 1..num_sections {
+ let ind = offset + i * size;
+ let section = Section::new(&data[ind..ind + size], &data, i, endian).unwrap();
+ sections.push(section);
+ }
+
+ let symtab_index = sections
+ .iter()
+ .position(|s| s.header.sh_type == SHT_SYMTAB)
+ .expect("missing symtab");
+ let sym_strtab_index = sections[symtab_index].header.sh_link as usize;
+
+ let symtab = &sections[symtab_index];
+ let sym_strtab = &sections[sym_strtab_index];
+ let symbol_entries = ElfFile::init_symbols(symtab, sym_strtab, endian);
+
+ let shstr = sections[header.e_shstrndx as usize].clone();
+
+ for i in 0..sections.len() {
+ let s = &mut sections[i];
+ s.name = String::from_utf8(shstr.lookup_str(s.header.sh_name as usize)).unwrap();
+ assert!(s.name.is_ascii());
+
+ if s.is_rel() {
+ let target_index = s.header.sh_info as usize;
+ s.init_relocs(endian);
+ sections[target_index].relocated_by.push(i);
+ }
+ }
+
+ Ok(ElfFile {
+ data,
+ endian,
+ header,
+ sections,
+ symbol_entries,
+ symtab: symtab_index,
+ sym_strtab: sym_strtab_index,
+ })
+ }
+
+ fn find_section(&self, name: &str) -> Option<&Section> {
+ self.sections.iter().find(|s| s.name == name)
+ }
+
+ fn find_section_mut(&mut self, name: &str) -> Option<&mut Section> {
+ self.sections.iter_mut().find(|s| s.name == name)
+ }
+
+ fn symtab(&self) -> &Section {
+ &self.sections[self.symtab]
+ }
+
+ fn symtab_mut(&mut self) -> &mut Section {
+ &mut self.sections[self.symtab]
+ }
+
+ fn sym_strtab(&self) -> &Section {
+ &self.sections[self.sym_strtab]
+ }
+
+ fn sym_strtab_mut(&mut self) -> &mut Section {
+ &mut self.sections[self.sym_strtab]
+ }
+
+ fn init_symbols(symtab: &Section, strtab: &Section, endian: Endian) -> Vec<Symbol> {
+ assert_eq!(symtab.header.sh_type, SHT_SYMTAB);
+ assert_eq!(symtab.header.sh_entsize, 16);
+
+ let mut syms = Vec::new();
+ for i in 0..(symtab.data.len() / 16) {
+ syms.push(Symbol::new(&symtab.data[i * 16..(i + 1) * 16], strtab, endian).unwrap());
+ }
+ syms
+ }
+
+ fn find_symbol(&self, name: &[u8]) -> Option<(usize, usize)> {
+ for s in &self.symbol_entries {
+ if s.name == name {
+ return Some((s.st_shndx, s.st_value));
+ }
+ }
+ None
+ }
+
+ fn find_symbol_in_section(&self, name: &[u8], section: &Section) -> usize {
+ let Some((st_shndx, st_value)) = self.find_symbol(name) else {
+ panic!("failed to find symbol: {}", String::from_utf8_lossy(name));
+ };
+ if st_shndx != section.index {
+ panic!(
+ "symbol {} is in wrong section",
+ String::from_utf8_lossy(name)
+ );
+ }
+ st_value
+ }
+
+ fn add_section(&mut self, name: &str, fields: &HeaderFields, data: &[u8], endian: Endian) {
+ let shstr = self
+ .sections
+ .get_mut(self.header.e_shstrndx as usize)
+ .expect("bad e_shstrndx");
+ let sh_name = shstr.add_str(name.as_bytes());
+ let mut s = Section::from_parts(sh_name, fields, data, self.sections.len(), endian);
+ s.name = name.to_string();
+ self.sections.push(s);
+ }
+
+ fn drop_mdebug_gptab(&mut self) {
+ // We can only drop sections at the end, since otherwise section
+ // references might be wrong. Luckily, these sections typically are.
+ while let Some(s) = self.sections.last() {
+ if s.header.sh_type != SHT_MIPS_DEBUG && s.header.sh_type != SHT_MIPS_GPTAB {
+ break;
+ }
+ self.sections.pop();
+ }
+ }
+
+ fn pad_out(writer: &mut BufWriter<&mut File>, align: usize) -> io::Result<()> {
+ let pos = writer.stream_position()? as usize;
+
+ if align > 0 && pos % align != 0 {
+ let pad = align - (pos % align);
+ for _ in 0..pad {
+ writer.write_all(&[0])?;
+ }
+ }
+ Ok(())
+ }
+
+ fn write(&mut self, writer: &mut BufWriter<&mut File>) -> io::Result<()> {
+ self.header.e_shnum = self.sections.len() as u16;
+ writer.write_all(&self.header.to_bin(self.endian))?;
+
+ for s in &mut self.sections {
+ if s.header.sh_type != SHT_NOBITS && s.header.sh_type != SHT_NULL {
+ Self::pad_out(writer, s.header.sh_addralign as usize)?;
+ let old_offset = s.header.sh_offset;
+ s.header.sh_offset = writer.stream_position()? as u32;
+ if s.header.sh_type == SHT_MIPS_DEBUG && s.header.sh_offset != old_offset {
+ // The .mdebug section has moved, relocate offsets
+ s.relocate_mdebug(old_offset, self.endian);
+ }
+ writer.write_all(&s.data)?;
+ }
+ }
+
+ Self::pad_out(writer, 4)?;
+ self.header.e_shoff = writer.stream_position()? as u32;
+
+ for s in &mut self.sections {
+ writer.write_all(&s.header_to_bin(self.endian))?;
+ }
+
+ writer.seek(SeekFrom::Start(0))?;
+ writer.write_all(&self.header.to_bin(self.endian))?;
+ writer.flush()?;
+ Ok(())
+ }
+}
+
+pub(crate) fn fixup_objfile(
+ objfile_path: &Path,
+ functions: &[Function],
+ asm_prelude: &str,
+ assembler: &str,
+ output_enc: &Encoding,
+ drop_mdebug_gptab: bool,
+ convert_statics: ConvertStatics,
+) -> Result<()> {
+ const OUTPUT_SECTIONS: [OutputSection; 4] = [
+ OutputSection::Data,
+ OutputSection::Text,
+ OutputSection::Rodata,
+ OutputSection::Bss,
+ ];
+ const INPUT_SECTION_NAMES: [&str; 5] = [".data", ".text", ".rodata", ".bss", ".late_rodata"];
+
+ let objfile_data = fs::read(objfile_path)?;
+ let mut objfile = ElfFile::new(&objfile_data)?;
+ let endian = objfile.endian;
+
+ let mut prev_locs: EnumMap<OutputSection, usize> = EnumMap::default();
+
+ struct ToCopyData {
+ loc: usize,
+ size: usize,
+ temp_name: String,
+ fn_desc: String,
+ }
+
+ let mut to_copy: EnumMap<OutputSection, Vec<ToCopyData>> = EnumMap::default();
+
+ let mut asm: Vec<String> = vec![];
+ let mut all_late_rodata_dummy_bytes: Vec<Vec<[u8; 4]>> = vec![];
+ let mut all_jtbl_rodata_size: Vec<usize> = vec![];
+ let mut late_rodata_asm: Vec<String> = vec![];
+ let late_rodata_source_name_start = "_asmpp_late_rodata_start";
+ let late_rodata_source_name_end = "_asmpp_late_rodata_end";
+
+ // Generate an assembly file with all the assembly we need to fill in. For
+ // simplicity we pad with nops/.space so that addresses match exactly, so we
+ // don't have to fix up relocations/symbol references.
+ let mut all_text_glabels: HashSet<Vec<u8>> = HashSet::new();
+ let mut func_sizes: HashMap<Vec<u8>, usize> = HashMap::new();
+
+ for function in functions.iter() {
+ let text_glabels = function
+ .text_glabels
+ .iter()
+ .map(|x| output_enc.encode(x))
+ .collect::<Result<Vec<_>>>()?;
+ let mut ifdefed = false;
+ for (sectype, &(ref temp_name, size)) in function.data.iter() {
+ let Some(temp_name) = temp_name else { continue };
+ if size == 0 {
+ panic!("Size of section {} is 0", sectype.as_str());
+ }
+ let Some((_, loc)) = objfile.find_symbol(temp_name.as_bytes()) else {
+ ifdefed = true;
+ break;
+ };
+ let prev_loc = prev_locs[sectype];
+ if loc < prev_loc {
+ // If the dummy C generates too little asm, and we have two
+ // consecutive GLOBAL_ASM blocks, we detect that error here.
+ // On the other hand, if it generates too much, we don't have
+ // a good way of discovering that error: it's indistinguishable
+ // from a static symbol occurring after the GLOBAL_ASM block.
+ panic!(
+ "Wrongly computed size for section {} (diff {}). This is an asm-processor bug!",
+ sectype,
+ prev_loc - loc
+ );
+ }
+ if loc != prev_loc {
+ asm.push(format!(".section {}", sectype));
+ if sectype == OutputSection::Text {
+ for _ in 0..((loc - prev_loc) / 4) {
+ asm.push("nop".to_owned());
+ }
+ } else {
+ asm.push(format!(".space {}", loc - prev_loc));
+ }
+ }
+ to_copy[sectype].push(ToCopyData {
+ loc,
+ size,
+ temp_name: temp_name.clone(),
+ fn_desc: function.fn_desc.clone(),
+ });
+ if !text_glabels.is_empty() && sectype == OutputSection::Text {
+ func_sizes.insert(text_glabels[0].to_vec(), size);
+ }
+ prev_locs[sectype] = loc + size;
+ }
+
+ if !ifdefed {
+ all_text_glabels.extend(text_glabels.iter().map(|x| x.to_vec()));
+ all_late_rodata_dummy_bytes.push(function.late_rodata_dummy_bytes.clone());
+ all_jtbl_rodata_size.push(function.jtbl_rodata_size);
+ late_rodata_asm.extend(function.late_rodata_asm_conts.iter().cloned());
+ for (sectype, (temp_name, _)) in function.data.iter() {
+ if let Some(temp_name) = temp_name {
+ asm.push(format!(".section {}", sectype));
+ asm.push(format!("glabel {}_asm_start", temp_name));
+ }
+ }
+ asm.push(".text".to_owned());
+ asm.extend(function.asm_conts.iter().cloned());
+ for (sectype, (temp_name, _)) in function.data.iter() {
+ if let Some(temp_name) = temp_name {
+ asm.push(format!(".section {}", sectype));
+ asm.push(format!("glabel {}_asm_end", temp_name));
+ }
+ }
+ }
+ }
+
+ if !late_rodata_asm.is_empty() {
+ asm.push(".section .late_rodata".to_string());
+ // Put some padding at the start to avoid conflating symbols with
+ // references to the whole section.
+ asm.push(".word 0, 0".to_string());
+ asm.push(format!("glabel {}", late_rodata_source_name_start));
+ asm.extend(late_rodata_asm.iter().cloned());
+ asm.push(format!("glabel {}", late_rodata_source_name_end));
+ }
+
+ let temp_dir = TempDir::with_prefix("asm_processor")?;
+
+ let obj_stem = objfile_path.file_stem().unwrap().to_str().unwrap();
+
+ let o_file_path = temp_dir
+ .path()
+ .join(format!("asm_processor_{}.o", obj_stem));
+ let s_file_path = temp_dir
+ .path()
+ .join(format!("asm_processor_{}.s", obj_stem));
+ {
+ let mut s_file = File::create(&s_file_path)?;
+ s_file.write_all(&output_enc.encode(asm_prelude)?)?;
+ s_file.write_all(&output_enc.encode("\n")?)?;
+
+ for line in asm {
+ s_file.write_all(&output_enc.encode(&line)?)?;
+ s_file.write_all(&output_enc.encode("\n")?)?;
+ }
+ }
+
+ let status = Command::new("sh")
+ .arg("-c")
+ .arg(format!(
+ "{} {} -o {}",
+ assembler,
+ shlex::try_quote(s_file_path.to_str().unwrap()).unwrap(),
+ shlex::try_quote(o_file_path.to_str().unwrap()).unwrap(),
+ ))
+ .status()
+ .expect("Failed to run shell");
+ if !status.success() {
+ return Err(anyhow::anyhow!("Failed to assemble"));
+ }
+ let asm_objfile = ElfFile::new(&fs::read(&o_file_path)?)?;
+
+ // Remove clutter from objdump output for tests, and make the tests
+ // portable by avoiding absolute paths. Outside of tests .mdebug is
+ // useful for showing source together with asm, though.
+ let mdebug_section = objfile.find_section(".mdebug").cloned();
+ if drop_mdebug_gptab {
+ objfile.drop_mdebug_gptab();
+ }
+
+ // Unify reginfo sections
+ if let Some(target_reginfo) = objfile.find_section_mut(".reginfo") {
+ let source_reginfo = &asm_objfile
+ .find_section(".reginfo")
+ .expect("couldn't find source .reginfo");
+ for (s, t) in source_reginfo
+ .data
+ .iter()
+ .zip(target_reginfo.data.iter_mut())
+ {
+ *t |= *s;
+ }
+ }
+
+ // Move over section contents
+ let mut modified_text_positions = HashSet::new();
+ let mut jtbl_rodata_positions: HashSet<usize> = HashSet::new();
+ let mut last_rodata_pos = 0;
+ for sectype in OUTPUT_SECTIONS {
+ if to_copy[sectype].is_empty() {
+ continue;
+ }
+ let Some(source) = asm_objfile.find_section(sectype.as_str()) else {
+ panic!("didn't find source section: {}", sectype);
+ };
+ for &ToCopyData {
+ loc,
+ size,
+ ref temp_name,
+ ref fn_desc,
+ } in to_copy[sectype].iter()
+ {
+ let loc1 = asm_objfile
+ .find_symbol_in_section(format!("{}_asm_start", &temp_name).as_bytes(), source);
+ let loc2 = asm_objfile
+ .find_symbol_in_section(format!("{}_asm_end", &temp_name).as_bytes(), source);
+ if loc1 != loc {
+ panic!(
+ "assembly and C files don't line up for section {}, {}",
+ sectype, fn_desc
+ );
+ }
+ if loc2 - loc1 != size {
+ return Err(anyhow::anyhow!(
+ "incorrectly computed size for section {}, {}. If using .double, make sure to provide explicit alignment padding.",
+ sectype,
+ fn_desc
+ ));
+ }
+ }
+
+ if sectype == OutputSection::Bss {
+ continue;
+ }
+
+ let Some(target) = objfile.find_section_mut(sectype.as_str()) else {
+ panic!("didn't find target section: {}", sectype);
+ };
+
+ for &ToCopyData { loc, size, .. } in to_copy[sectype].iter() {
+ target.data[loc..loc + size].copy_from_slice(&source.data[loc..loc + size]);
+
+ if sectype == OutputSection::Text {
+ assert_eq!(size % 4, 0);
+ assert_eq!(loc % 4, 0);
+ for j in 0..size / 4 {
+ modified_text_positions.insert(loc + 4 * j);
+ }
+ } else if sectype == OutputSection::Rodata {
+ last_rodata_pos = loc + size;
+ }
+ }
+ }
+
+ // Move over late rodata. This is heuristic, sadly, since I can't think
+ // of another way of doing it.
+ let mut moved_late_rodata: HashMap<usize, usize> = HashMap::new();
+ if all_late_rodata_dummy_bytes.iter().any(|b| !b.is_empty())
+ || all_jtbl_rodata_size.iter().any(|&s| s > 0)
+ {
+ let source = asm_objfile
+ .find_section(".late_rodata")
+ .expect(".late_rodata source section should exist");
+ let target = objfile
+ .find_section_mut(".rodata")
+ .expect(".rodata target section should exist");
+ let mut source_pos =
+ asm_objfile.find_symbol_in_section(late_rodata_source_name_start.as_bytes(), source);
+ let source_end =
+ asm_objfile.find_symbol_in_section(late_rodata_source_name_end.as_bytes(), source);
+ let num_dummies: usize = all_late_rodata_dummy_bytes.iter().map(|x| x.len()).sum();
+ let expected_size = num_dummies * 4 + all_jtbl_rodata_size.iter().sum::<usize>();
+
+ if source_end - source_pos != expected_size {
+ return Err(anyhow::anyhow!("computed wrong size of .late_rodata"));
+ }
+ let mut new_data = target.data.clone();
+
+ for (dummy_bytes_list, &jtbl_rodata_size) in all_late_rodata_dummy_bytes
+ .iter_mut()
+ .zip(all_jtbl_rodata_size.iter())
+ {
+ let dummy_bytes_list_len = dummy_bytes_list.len();
+
+ for (index, dummy_bytes) in dummy_bytes_list.iter_mut().enumerate() {
+ if endian == Endian::Little {
+ dummy_bytes.reverse();
+ }
+
+ let mut pos = target.data[last_rodata_pos..]
+ .windows(4)
+ .position(|x| x == dummy_bytes)
+ .expect("failed to find dummy .late_rodata bytes")
+ + last_rodata_pos;
+
+ if index == 0
+ && dummy_bytes_list_len > 1
+ && target.data[pos + 4..pos + 8] == *b"\0\0\0\0"
+ {
+ // Ugly hack to handle double alignment for non-matching builds.
+ // We were told by .late_rodata_alignment (or deduced from a .double)
+ // that a function's late_rodata started out 4 (mod 8), and emitted
+ // a float and then a double. But it was actually 0 (mod 8), so our
+ // double was moved by 4 bytes. To make them adjacent to keep jump
+ // tables correct, move the float by 4 bytes as well.
+ new_data[pos..pos + 4].copy_from_slice(b"\0\0\0\0");
+ pos += 4;
+ }
+ new_data[pos..pos + 4].copy_from_slice(&source.data[source_pos..source_pos + 4]);
+ moved_late_rodata.insert(source_pos, pos);
+ last_rodata_pos = pos + 4;
+ source_pos += 4;
+ }
+
+ if jtbl_rodata_size > 0 {
+ assert!(!dummy_bytes_list.is_empty());
+ let pos = last_rodata_pos;
+ new_data[pos..pos + jtbl_rodata_size]
+ .copy_from_slice(&source.data[source_pos..source_pos + jtbl_rodata_size]);
+ for i in (0..jtbl_rodata_size).step_by(4) {
+ moved_late_rodata.insert(source_pos + i, pos + i);
+ jtbl_rodata_positions.insert(pos + i);
+ }
+ last_rodata_pos += jtbl_rodata_size;
+ source_pos += jtbl_rodata_size;
+ }
+ }
+ target.data = new_data;
+ }
+
+ // Merge strtab data.
+ let strtab = objfile.sym_strtab_mut();
+ let strtab_adj = strtab.data.len();
+ strtab.data.extend(&asm_objfile.sym_strtab().data);
+
+ // Find relocated symbols in asm_objfile
+ let mut relocated_symbols = HashSet::new();
+ for sectype in INPUT_SECTION_NAMES.iter() {
+ if let Some(sec) = asm_objfile.find_section(sectype) {
+ for reltab_idx in &sec.relocated_by {
+ let reltab = &asm_objfile.sections[*reltab_idx];
+ for rel in &reltab.relocations {
+ relocated_symbols.insert(rel.sym_index);
+ }
+ }
+ }
+ }
+
+ enum SymInd {
+ Obj(usize),
+ Asm(usize),
+ }
+
+ // Move over symbols, deleting the temporary function labels.
+ // Skip over new local symbols that aren't relocated against, to
+ // avoid conflicts.
+ let empty_symbol = objfile.symbol_entries[0].clone();
+ let mut new_syms: Vec<(Symbol, Vec<SymInd>)> = objfile
+ .symbol_entries
+ .iter()
+ .enumerate()
+ .map(|(i, x)| (x, vec![SymInd::Obj(i)]))
+ .skip(1)
+ .filter(|(x, _)| !x.name.starts_with(b"_asmpp_"))
+ .map(|(x, inds)| (x.clone(), inds))
+ .collect();
+
+ for (i, s) in asm_objfile.symbol_entries.iter().enumerate() {
+ let is_local = i < asm_objfile.symtab().header.sh_info as usize;
+ if is_local && !relocated_symbols.contains(&i) {
+ continue;
+ }
+ if s.name.starts_with(b"_asmpp_") {
+ assert!(!relocated_symbols.contains(&i));
+ continue;
+ }
+ let mut s = s.clone();
+ if s.st_shndx != SHN_UNDEF && s.st_shndx != SHN_ABS {
+ let section_name = asm_objfile.sections[s.st_shndx].name.clone();
+ let mut target_section_name = section_name.clone();
+ if section_name == ".late_rodata" {
+ target_section_name = ".rodata".to_string();
+ } else if !INPUT_SECTION_NAMES.contains(&section_name.as_str()) {
+ return Err(anyhow::anyhow!(
+ "generated assembly .o must only have symbols for .text, .data, .rodata, .late_rodata, ABS and UNDEF, but found {}",
+ section_name
+ ));
+ }
+ let Some(objfile_section) = objfile.find_section(&target_section_name) else {
+ return Err(anyhow::anyhow!(
+ "generated assembly .o has section that real objfile lacks: {}",
+ target_section_name
+ ));
+ };
+ s.st_shndx = objfile_section.index;
+ // glabels aren't marked as functions, making objdump output confusing. Fix that.
+ if all_text_glabels.contains(&s.name) {
+ s.st_type = STT_FUNC;
+ if let Some(&size) = func_sizes.get(&s.name) {
+ s.st_size = size;
+ }
+ }
+ if section_name == ".late_rodata" {
+ if s.st_value == 0 {
+ // This must be a symbol corresponding to the whole .late_rodata
+ // section, being referred to from a relocation.
+ // Moving local symbols is tricky, because it requires fixing up
+ // lo16/hi16 relocation references to .late_rodata+<offset>.
+ // Just disallow it for now.
+ return Err(anyhow::anyhow!(
+ "local symbols in .late_rodata are not allowed"
+ ));
+ }
+ s.st_value = moved_late_rodata[&s.st_value];
+ }
+ }
+ s.st_name += strtab_adj;
+ new_syms.push((s, vec![SymInd::Asm(i)]));
+ }
+
+ // Add static symbols from .mdebug, so they can be referred to from GLOBAL_ASM
+ if mdebug_section.is_some() && convert_statics != ConvertStatics::No {
+ let mdebug_section = mdebug_section.unwrap();
+ let mut static_name_count: HashMap<Vec<u8>, usize> = HashMap::new();
+ let mut strtab_index = objfile.sym_strtab().data.len();
+ let mut new_strtab_data = vec![];
+
+ let read_u32 = |data: &[u8], offset| {
+ u32::from_be_bytes(data[offset..offset + 4].try_into().unwrap()) as usize
+ };
+
+ let ifd_max = read_u32(&mdebug_section.data, 18 * 4);
+ let cb_fd_offset = read_u32(&mdebug_section.data, 19 * 4);
+ let cb_sym_offset = read_u32(&mdebug_section.data, 9 * 4);
+ let cb_ss_offset = read_u32(&mdebug_section.data, 15 * 4);
+
+ for i in 0..ifd_max {
+ let offset = cb_fd_offset + 18 * 4 * i;
+ let iss_base = read_u32(&objfile.data, offset + 2 * 4);
+ let isym_base = read_u32(&objfile.data, offset + 4 * 4);
+ let csym = read_u32(&objfile.data, offset + 5 * 4);
+ let mut scope_level = 0;
+
+ for j in 0..csym {
+ let offset2 = cb_sym_offset + 12 * (isym_base + j);
+ let iss = read_u32(&objfile.data, offset2);
+ let value = read_u32(&objfile.data, offset2 + 4);
+ let st_sc_index = read_u32(&objfile.data, offset2 + 8);
+ let st = st_sc_index >> 26;
+ let sc = (st_sc_index >> 21) & 0x1F;
+
+ if st == MIPS_DEBUG_ST_STATIC || st == MIPS_DEBUG_ST_STATIC_PROC {
+ let symbol_name_offset = cb_ss_offset + iss_base + iss;
+ let symbol_name_offset_end = objfile_data[symbol_name_offset..]
+ .iter()
+ .position(|x| *x == 0)
+ .expect("bad .mdebug strtab reference")
+ + symbol_name_offset;
+ let mut symbol_name =
+ objfile_data[symbol_name_offset..symbol_name_offset_end].to_owned();
+ if scope_level > 1 {
+ // For in-function statics, append an increasing counter to
+ // the name, to avoid duplicate conflicting symbols.
+ let count = static_name_count.get(&symbol_name).unwrap_or(&0) + 1;
+ static_name_count.insert(symbol_name.clone(), count);
+ symbol_name.extend(format!(":{}", count).as_bytes());
+ }
+ let mut emitted_symbol_name = symbol_name.clone();
+ if convert_statics == ConvertStatics::GlobalWithFilename {
+ // Change the emitted symbol name to include the filename,
+ // but don't let that affect deduplication logic (we still
+ // want to be able to reference statics from GLOBAL_ASM).
+ let mut new_name = objfile_path.to_string_lossy().into_owned().into_bytes();
+ new_name.push(b':');
+ new_name.extend(emitted_symbol_name);
+ emitted_symbol_name = new_name;
+ };
+ let section_name = match sc {
+ 1 => ".text",
+ 2 => ".data",
+ 3 => ".bss",
+ 15 => ".rodata",
+ _ => {
+ return Err(anyhow::anyhow!("unsupported MIPS_DEBUG_SC value: {}", sc));
+ }
+ };
+ let Some(section) = objfile.find_section(section_name) else {
+ panic!(
+ "couldn't find section referenced from .mdebug: {}",
+ section_name
+ );
+ };
+ let symtype = if sc == 1 { STT_FUNC } else { STT_OBJECT };
+ let binding = match convert_statics {
+ ConvertStatics::Global | ConvertStatics::GlobalWithFilename => STB_GLOBAL,
+ _ => STB_LOCAL,
+ };
+ let sym = Symbol {
+ st_name: strtab_index,
+ st_value: value,
+ st_size: 0,
+ st_bind: binding,
+ st_type: symtype,
+ st_visibility: STV_DEFAULT,
+ st_shndx: section.index,
+ name: symbol_name,
+ };
+ strtab_index += emitted_symbol_name.len() + 1;
+ new_strtab_data.extend(&emitted_symbol_name);
+ new_strtab_data.push(b'\0');
+ new_syms.push((sym, vec![]));
+ }
+ match st {
+ MIPS_DEBUG_ST_FILE
+ | MIPS_DEBUG_ST_STRUCT
+ | MIPS_DEBUG_ST_UNION
+ | MIPS_DEBUG_ST_ENUM
+ | MIPS_DEBUG_ST_BLOCK
+ | MIPS_DEBUG_ST_PROC
+ | MIPS_DEBUG_ST_STATIC_PROC => {
+ scope_level += 1;
+ }
+ MIPS_DEBUG_ST_END => {
+ scope_level -= 1;
+ }
+ _ => {}
+ }
+ }
+ assert_eq!(scope_level, 0);
+ }
+
+ objfile.sym_strtab_mut().data.extend(new_strtab_data);
+ }
+
+ // Get rid of duplicate symbols, favoring ones that are not UNDEF.
+ // Skip this for unnamed local symbols though.
+ new_syms.sort_by(|(a, _), (b, _)| {
+ if a.st_shndx != SHN_UNDEF && b.st_shndx == SHN_UNDEF {
+ Ordering::Less
+ } else {
+ Ordering::Greater
+ }
+ });
+
+ let new_syms_prev = new_syms;
+ let mut new_syms = vec![];
+ let mut name_to_sym = HashMap::new();
+ for (mut s, inds) in new_syms_prev {
+ if s.name == b"_gp_disp" {
+ s.st_type = STT_OBJECT;
+ }
+ if s.st_bind == STB_LOCAL && s.st_shndx == SHN_UNDEF {
+ return Err(anyhow::anyhow!(
+ "local symbol \"{}\" is undefined",
+ String::from_utf8_lossy(&s.name)
+ ));
+ }
+ if s.name.is_empty() {
+ if s.st_bind != STB_LOCAL {
+ return Err(anyhow::anyhow!("global symbol with no name"));
+ }
+ new_syms.push((s.clone(), inds));
+ } else {
+ match name_to_sym.get(&s.name) {
+ None => {
+ name_to_sym.insert(s.name.clone(), new_syms.len());
+ new_syms.push((s.clone(), inds));
+ }
+ Some(&existing) => {
+ let (s2, inds2) = &mut new_syms[existing];
+ if s.st_shndx != SHN_UNDEF
+ && !(s2.st_shndx == s.st_shndx && s2.st_value == s.st_value)
+ {
+ return Err(anyhow::anyhow!(
+ "symbol \"{}\" defined twice",
+ String::from_utf8_lossy(&s.name)
+ ));
+ }
+ inds2.extend(inds);
+ }
+ }
+ }
+ }
+
+ // Put local symbols in front, with the initial dummy entry first, and
+ // _gp_disp at the end if it exists.
+ new_syms.insert(0, (empty_symbol.clone(), vec![]));
+ new_syms.sort_by_key(|(a, _)| (a.st_bind != STB_LOCAL, a.name == b"_gp_disp"));
+
+ let mut obj_new_index: HashMap<usize, usize> = HashMap::new();
+ let mut asm_new_index: HashMap<usize, usize> = HashMap::new();
+
+ for (new_index, (_, inds)) in new_syms.iter().enumerate() {
+ for i in inds {
+ match i {
+ SymInd::Obj(i) => obj_new_index.insert(*i, new_index),
+ SymInd::Asm(i) => asm_new_index.insert(*i, new_index),
+ };
+ }
+ }
+
+ let new_syms: Vec<_> = new_syms.iter().map(|(s, _)| s).collect();
+ let num_local_syms = new_syms.iter().filter(|s| s.st_bind == STB_LOCAL).count();
+ let new_sym_data: Vec<u8> = new_syms.iter().flat_map(|s| s.to_bin()).collect();
+
+ objfile.symtab_mut().data = new_sym_data;
+ objfile.symtab_mut().header.sh_info = num_local_syms as u32;
+
+ // Fix up relocation symbol references
+ for sectype in OUTPUT_SECTIONS {
+ let target = objfile.find_section(sectype.as_str()).cloned();
+
+ if let Some(target) = target {
+ // fixup relocation symbol indices, since we butchered them above
+ for reltab in target.relocated_by.iter() {
+ let reltab = &mut objfile.sections[*reltab];
+ let mut nrels = vec![];
+ for rel in reltab.relocations.iter() {
+ let mut rel = rel.clone();
+ if (sectype == OutputSection::Text
+ && modified_text_positions.contains(&rel.r_offset))
+ || (sectype == OutputSection::Rodata
+ && jtbl_rodata_positions.contains(&rel.r_offset))
+ {
+ // don't include relocations for late_rodata dummy code
+ continue;
+ }
+ rel.sym_index = obj_new_index[&rel.sym_index];
+ nrels.push(rel);
+ }
+ reltab.data = nrels.iter().flat_map(|x| x.to_bin(endian)).collect();
+ reltab.relocations = nrels;
+ }
+ }
+ }
+
+ // Move over relocations
+ for sectype in INPUT_SECTION_NAMES.iter() {
+ if let Some(source) = asm_objfile.find_section(sectype) {
+ if source.data.is_empty() {
+ continue;
+ }
+
+ let target_sectype = if *sectype == ".late_rodata" {
+ ".rodata"
+ } else {
+ sectype
+ };
+ let target_index = objfile
+ .find_section(target_sectype)
+ .expect("didn't find target section")
+ .index;
+ for reltab in &source.relocated_by {
+ let reltab = &mut asm_objfile.sections[*reltab].clone();
+ for rel in &mut reltab.relocations {
+ rel.sym_index = asm_new_index[&rel.sym_index];
+ if *sectype == ".late_rodata" {
+ rel.r_offset = moved_late_rodata[&rel.r_offset];
+ }
+ }
+ let new_data: Vec<u8> = reltab
+ .relocations
+ .iter()
+ .flat_map(|x| x.to_bin(endian))
+ .collect();
+
+ let (prefix, sh_entsize) = if reltab.header.sh_type == SHT_REL {
+ (".rel", 8)
+ } else {
+ (".rela", 12)
+ };
+ let rel_section_name = format!("{}{}", prefix, target_sectype);
+
+ if let Some(target_reltab) = objfile.find_section_mut(&rel_section_name) {
+ target_reltab.data.extend(new_data);
+ } else {
+ objfile.add_section(
+ &rel_section_name,
+ &HeaderFields {
+ sh_type: reltab.header.sh_type,
+ sh_flags: 0,
+ sh_link: objfile.symtab().index as u32,
+ sh_info: target_index as u32,
+ sh_addralign: 4,
+ sh_entsize,
+ },
+ &new_data,
+ endian,
+ );
+ }
+ }
+ }
+ }
+
+ let mut file = std::fs::File::create(objfile_path).expect("unable to write to .o file");
+ let mut writer = BufWriter::new(&mut file);
+ objfile.write(&mut writer)?;
+
+ fs::remove_file(s_file_path)?;
+ fs::remove_file(o_file_path)?;
+ Ok(())
+}
diff --git a/tools/asm-processor/rust/src/preprocess.rs b/tools/asm-processor/rust/src/preprocess.rs
new file mode 100644
index 0000000..73af653
--- /dev/null
+++ b/tools/asm-processor/rust/src/preprocess.rs
@@ -0,0 +1,969 @@
+use std::{fs, io::Write, iter, path::Path, sync::OnceLock};
+
+use anyhow::Result;
+use enum_map::{Enum, EnumMap};
+use regex_lite::Regex;
+
+use crate::{AsmProcArgs, CompileOpts, Encoding, Function, OptLevel, OutputSection};
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug, Enum)]
+enum InputSection {
+ Text,
+ Data,
+ Rodata,
+ LateRodata,
+ Bss,
+}
+
+impl InputSection {
+ fn from_str(name: &str) -> Option<InputSection> {
+ match name {
+ ".text" => Some(InputSection::Text),
+ ".data" => Some(InputSection::Data),
+ ".rodata" => Some(InputSection::Rodata),
+ ".late_rodata" => Some(InputSection::LateRodata),
+ ".bss" => Some(InputSection::Bss),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+struct GlobalState {
+ late_rodata_hex: u32,
+ valuectr: usize,
+ namectr: usize,
+ min_instr_count: usize,
+ skip_instr_count: usize,
+ use_jtbl_for_rodata: bool,
+ prelude_if_late_rodata: usize,
+ mips1: bool,
+ pascal: bool,
+}
+
+impl GlobalState {
+ fn new(
+ min_instr_count: usize,
+ skip_instr_count: usize,
+ use_jtbl_for_rodata: bool,
+ prelude_if_late_rodata: usize,
+ mips1: bool,
+ pascal: bool,
+ ) -> Self {
+ Self {
+ // A value that hopefully never appears as a 32-bit rodata constant (or we
+ // miscompile late rodata). Increases by 1 in each step.
+ late_rodata_hex: 0xE0123456,
+ valuectr: 0,
+ namectr: 0,
+ min_instr_count,
+ skip_instr_count,
+ use_jtbl_for_rodata,
+ prelude_if_late_rodata,
+ mips1,
+ pascal,
+ }
+ }
+
+ fn next_late_rodata_hex(&mut self) -> [u8; 4] {
+ let dummy_bytes = self.late_rodata_hex.to_be_bytes();
+ if (self.late_rodata_hex & 0xffff) == 0 {
+ // Avoid lui
+ self.late_rodata_hex += 1;
+ }
+ self.late_rodata_hex += 1;
+ dummy_bytes
+ }
+
+ fn make_name(&mut self, cat: &str) -> String {
+ self.namectr += 1;
+ format!("_asmpp_{}{}", cat, self.namectr)
+ }
+
+ fn func_prologue(&self, name: &str) -> String {
+ if self.pascal {
+ [
+ &format!("procedure {}();", name),
+ "type",
+ " pi = ^integer;",
+ " pf = ^single;",
+ " pd = ^double;",
+ "var",
+ " vi: pi;",
+ " vf: pf;",
+ " vd: pd;",
+ "begin",
+ " vi := vi;",
+ " vf := vf;",
+ " vd := vd;",
+ ]
+ .join(" ")
+ } else {
+ format!("void {}(void) {{", name)
+ }
+ }
+
+ fn func_epilogue(&self) -> String {
+ if self.pascal {
+ "end;".to_string()
+ } else {
+ '}'.to_string()
+ }
+ }
+
+ fn pascal_assignment_float(&mut self, val: f32) -> String {
+ self.valuectr += 1;
+ let address = (8 * self.valuectr) & 0x7FFF;
+ format!("vf := pf({}); vf^ := {:?};", address, val)
+ }
+
+ fn pascal_assignment_double(&mut self, val: f64) -> String {
+ self.valuectr += 1;
+ let address = (8 * self.valuectr) & 0x7FFF;
+ format!("vd := pd({}); vd^ := {:?};", address, val)
+ }
+
+ fn pascal_assignment_int(&mut self, val: i32) -> String {
+ self.valuectr += 1;
+ let address = (8 * self.valuectr) & 0x7FFF;
+ format!("vi := pi({}); vi^ := {};", address, val)
+ }
+}
+
+#[derive(Clone, Debug)]
+struct GlobalAsmBlock {
+ fn_desc: String,
+ cur_section: InputSection,
+ asm_conts: Vec<String>,
+ late_rodata_asm_conts: Vec<String>,
+ late_rodata_alignment: usize,
+ late_rodata_alignment_from_context: bool,
+ text_glabels: Vec<String>,
+ fn_section_sizes: EnumMap<InputSection, usize>,
+ fn_ins_inds: Vec<(usize, usize)>,
+ glued_line: String,
+ num_lines: usize,
+}
+
+impl GlobalAsmBlock {
+ fn new(fn_desc: String) -> Self {
+ Self {
+ fn_desc,
+ cur_section: InputSection::Text,
+ asm_conts: vec![],
+ late_rodata_asm_conts: vec![],
+ late_rodata_alignment: 0,
+ late_rodata_alignment_from_context: false,
+ text_glabels: vec![],
+ fn_section_sizes: EnumMap::default(),
+ fn_ins_inds: vec![],
+ glued_line: String::new(),
+ num_lines: 0,
+ }
+ }
+
+ fn fail_without_line<T>(&self, msg: &str) -> Result<T> {
+ Err(anyhow::anyhow!("{}\nwithin {}", msg, self.fn_desc))
+ }
+
+ fn fail_at_line<T>(&self, msg: &str, line: &str) -> Result<T> {
+ Err(anyhow::anyhow!(
+ "{}\nwithin {} at line {}",
+ msg,
+ self.fn_desc,
+ line
+ ))
+ }
+
+ fn count_quoted_size(
+ &self,
+ line: &str,
+ z: bool,
+ real_line: &str,
+ output_enc: &Encoding,
+ ) -> Result<usize> {
+ let line = output_enc.encode(line)?;
+
+ let mut in_quote = false;
+ let mut has_comma = true;
+ let mut num_parts = 0;
+ let mut ret = 0;
+ let mut i = 0;
+ let digits = b"0123456789"; // 0-7 would be more sane, but this matches GNU as
+ let hexdigits = b"0123456789abcdefABCDEF";
+
+ while i < line.len() {
+ let c = line[i];
+ i += 1;
+ if !in_quote {
+ if c == b'"' {
+ in_quote = true;
+ if z && !has_comma {
+ return self.fail_at_line(".asciiz with glued strings is not supported due to GNU as version diffs", real_line);
+ }
+ num_parts += 1;
+ } else if c == b',' {
+ has_comma = true;
+ }
+ } else {
+ if c == b'"' {
+ in_quote = false;
+ has_comma = false;
+ continue;
+ }
+ ret += 1;
+ if c != b'\\' {
+ continue;
+ }
+ if i == line.len() {
+ return self.fail_at_line("backslash at end of line not supported", real_line);
+ }
+ let c = line[i];
+ i += 1;
+ // (if c is in "bfnrtv", we have a real escaped literal)
+ if c == b'x' {
+ // hex literal, consume any number of hex chars, possibly none
+ while i < line.len() && hexdigits.contains(&line[i]) {
+ i += 1;
+ }
+ } else if digits.contains(&c) {
+ // octal literal, consume up to two more digits
+ let mut it = 0;
+ while i < line.len() && digits.contains(&line[i]) && it < 2 {
+ i += 1;
+ it += 1;
+ }
+ }
+ }
+ }
+
+ if in_quote {
+ return self.fail_at_line("unterminated string literal", real_line);
+ }
+ if num_parts == 0 {
+ return self.fail_at_line(".ascii with no string", real_line);
+ }
+ Ok(ret + if z { num_parts } else { 0 })
+ }
+
+ fn align(&mut self, n: usize) {
+ let size = &mut self.fn_section_sizes[self.cur_section];
+ while *size % n != 0 {
+ *size += 1;
+ }
+ }
+
+ fn add_sized(&mut self, size: isize, line: &str) -> Result<()> {
+ if (self.cur_section == InputSection::Text || self.cur_section == InputSection::LateRodata)
+ && size % 4 != 0
+ {
+ return self.fail_at_line("size must be a multiple of 4", line);
+ }
+
+ if size < 0 {
+ return self.fail_at_line("size cannot be negative", line);
+ }
+
+ self.fn_section_sizes[self.cur_section] += size as usize;
+
+ if self.cur_section == InputSection::Text {
+ if self.text_glabels.is_empty() {
+ return self.fail_at_line(".text block without an initial glabel", line);
+ }
+ self.fn_ins_inds
+ .push((self.num_lines - 1, size as usize / 4));
+ }
+
+ Ok(())
+ }
+
+ fn process_line(&mut self, line: &str, output_enc: &Encoding) -> Result<()> {
+ self.num_lines += 1;
+ if let Some(stripped) = line.strip_suffix("\\") {
+ self.glued_line = format!("{}{}", self.glued_line, stripped);
+ return Ok(());
+ }
+ let mut line = self.glued_line.clone() + line;
+ self.glued_line = String::new();
+
+ static CACHE: OnceLock<(Regex, Regex)> = OnceLock::new();
+ let (re_comment_or_string, re_label) = CACHE.get_or_init(|| {
+ (
+ Regex::new(r#"#.*|/\*.*?\*/|"(?:\\.|[^\\"])*""#).unwrap(),
+ Regex::new(r"^[a-zA-Z0-9_]+:\s*").unwrap(),
+ )
+ });
+
+ fn re_comment_replacer(caps: &regex_lite::Captures) -> String {
+ let s = caps[0].to_string();
+ if s.starts_with("/") || s.starts_with("#") {
+ " ".to_owned()
+ } else {
+ s
+ }
+ }
+
+ let real_line = line.clone();
+ line = re_comment_or_string
+ .replace_all(&line, re_comment_replacer)
+ .into_owned();
+ line = line.trim().to_string();
+ line = re_label.replace_all(&line, "").into_owned();
+ let mut changed_section = false;
+ let mut emitting_double = false;
+
+ if (line.starts_with("glabel ") || line.starts_with("jlabel "))
+ && self.cur_section == InputSection::Text
+ {
+ self.text_glabels
+ .push(line.split_whitespace().nth(1).unwrap().to_string());
+ }
+ if line.is_empty() {
+ // empty line
+ } else if line.starts_with("glabel ")
+ || line.starts_with("dlabel ")
+ || line.starts_with("jlabel ")
+ || line.starts_with("alabel ")
+ || line.starts_with("endlabel ")
+ || line.starts_with("enddlabel ")
+ || line.starts_with("nonmatching ")
+ || (!line.contains(" ") && line.ends_with(":"))
+ {
+ // label
+ } else if line.starts_with(".section")
+ || matches!(
+ line.as_str(),
+ ".text" | ".data" | ".rdata" | ".rodata" | ".bss" | ".late_rodata"
+ )
+ {
+ // section change
+ self.cur_section = if line == ".rdata" {
+ InputSection::Rodata
+ } else {
+ let first_arg = line.split(',').next().unwrap().to_string();
+ let name = first_arg.split_whitespace().last().unwrap();
+ match InputSection::from_str(name) {
+ Some(s) => s,
+ None => {
+ return self.fail_at_line("unrecognized .section directive", &real_line)
+ }
+ }
+ };
+
+ changed_section = true;
+ } else if line.starts_with(".late_rodata_alignment") {
+ if self.cur_section != InputSection::LateRodata {
+ return self.fail_at_line(
+ ".late_rodata_alignment must occur within .late_rodata section",
+ &real_line,
+ );
+ }
+
+ let value = line.split_whitespace().nth(1).unwrap().parse::<usize>()?;
+ if value != 4 && value != 8 {
+ return self
+ .fail_at_line(".late_rodata_alignment argument must be 4 or 8", &real_line);
+ }
+ if self.late_rodata_alignment != 0 && self.late_rodata_alignment != value {
+ return self.fail_without_line(
+ ".late_rodata_alignment alignment assumption conflicts with earlier .double directive. Make sure to provide explicit alignment padding."
+ );
+ }
+ self.late_rodata_alignment = value;
+ changed_section = true;
+ } else if line.starts_with(".incbin") {
+ let size = line
+ .split(',')
+ .next_back()
+ .unwrap()
+ .trim()
+ .parse::<isize>()?;
+ self.add_sized(size, &real_line)?;
+ } else if line.starts_with(".word")
+ || line.starts_with(".gpword")
+ || line.starts_with(".float")
+ {
+ self.align(4);
+
+ self.add_sized(4 * line.split(',').count() as isize, &real_line)?;
+ } else if line.starts_with(".double") {
+ self.align(4);
+
+ if self.cur_section == InputSection::LateRodata {
+ let align8 = self.fn_section_sizes[self.cur_section] % 8;
+ // Automatically set late_rodata_alignment, so the generated C code uses doubles.
+ // This gives us correct alignment for the transferred doubles even when the
+ // late_rodata_alignment is wrong, e.g. for non-matching compilation.
+ if self.late_rodata_alignment == 0 {
+ self.late_rodata_alignment = 8 - align8;
+ self.late_rodata_alignment_from_context = true;
+ } else if self.late_rodata_alignment != 8 - align8 {
+ if self.late_rodata_alignment_from_context {
+ return self.fail_at_line(
+ "found two .double directives with different start addresses mod 8. Make sure to provide explicit alignment padding.",
+ &real_line
+ );
+ } else {
+ return self.fail_at_line(
+ ".double at address that is not 0 mod 8 (based on .late_rodata_alignment assumption). Make sure to provide explicit alignment padding.\n{}",
+ &real_line
+ );
+ }
+ }
+
+ self.add_sized(8 * line.split(',').count() as isize, &real_line)?;
+ emitting_double = true;
+ }
+ } else if line.starts_with(".space") {
+ let size = line.split_whitespace().nth(1).unwrap().parse::<isize>()?;
+ self.add_sized(size, &real_line)?;
+ } else if line.starts_with(".balign") {
+ let align = line.split_whitespace().nth(1).unwrap().parse::<isize>()?;
+ if align != 4 {
+ return self.fail_at_line("only .balign 4 is supported", &real_line);
+ }
+ self.align(4);
+ } else if line.starts_with(".align") {
+ let align = line.split_whitespace().nth(1).unwrap().parse::<isize>()?;
+ if align != 2 {
+ return self.fail_at_line("only .align 2 is supported", &real_line);
+ }
+ self.align(4);
+ } else if line.starts_with(".asci") {
+ let z = line.starts_with(".asciz") || line.starts_with(".asciiz");
+ self.add_sized(
+ self.count_quoted_size(&line, z, &real_line, output_enc)? as isize,
+ &real_line,
+ )?;
+ } else if line.starts_with(".byte") {
+ self.add_sized(line.split(',').count() as isize, &real_line)?;
+ } else if line.starts_with(".half")
+ || line.starts_with(".hword")
+ || line.starts_with(".short")
+ {
+ self.align(2);
+ self.add_sized(2 * line.split(',').count() as isize, &real_line)?;
+ } else if line.starts_with(".size") {
+ } else if line.starts_with('.') {
+ return self.fail_at_line("asm directive not supported", &real_line);
+ } else {
+ // Unfortunately, macros are hard to support for .rodata --
+ // we don't know how how space they will expand to before
+ // running the assembler, but we need that information to
+ // construct the C code. So if we need that we'll either
+ // need to run the assembler twice (at least in some rare
+ // cases), or change how this program is invoked.
+ // Similarly, we can't currently deal with pseudo-instructions
+ // that expand to several real instructions.
+ if self.cur_section != InputSection::Text {
+ return self.fail_at_line(
+ "instruction or macro call in non-.text section? not supported",
+ &real_line,
+ );
+ }
+ self.add_sized(4, &real_line)?;
+ }
+
+ if self.cur_section == InputSection::LateRodata {
+ if !changed_section {
+ if emitting_double {
+ self.late_rodata_asm_conts.push(".align 0".to_string());
+ }
+ self.late_rodata_asm_conts.push(real_line.clone());
+ if emitting_double {
+ self.late_rodata_asm_conts.push(".align 2".to_string());
+ }
+ }
+ } else {
+ self.asm_conts.push(real_line.clone());
+ }
+
+ Ok(())
+ }
+
+ const MAX_FN_SIZE: usize = 100;
+
+ fn finish(&self, state: &mut GlobalState) -> Result<(Vec<String>, Function)> {
+ let mut src = vec!["".to_owned(); self.num_lines + 1];
+ let mut late_rodata_dummy_bytes = vec![];
+ let mut jtbl_rodata_size = 0;
+ let mut late_rodata_fn_output = vec![];
+
+ let num_instr = self.fn_section_sizes[InputSection::Text] / 4;
+
+ if self.fn_section_sizes[InputSection::LateRodata] > 0 {
+ // Generate late rodata by emitting unique float constants.
+ // This requires 3 instructions for each 4 bytes of rodata.
+ // If we know alignment, we can use doubles, which give 3
+ // instructions for 8 bytes of rodata.
+ let size = self.fn_section_sizes[InputSection::LateRodata] / 4;
+ let mut skip_next = false;
+ let mut needs_double = self.late_rodata_alignment != 0;
+ let mut extra_mips1_nop = false;
+ let (jtbl_size, jtbl_min_rodata_size) = match (state.pascal, state.mips1) {
+ (true, true) => (9, 2),
+ (true, false) => (8, 2),
+ (false, true) => (11, 5),
+ (false, false) => (9, 5),
+ };
+
+ for i in 0..size {
+ if skip_next {
+ skip_next = false;
+ continue;
+ }
+ // Jump tables give 9 instructions (11 with -mips1) for >= 5 words of rodata,
+ // and should be emitted when:
+ // - -O2 or -O2 -g3 are used, which give the right codegen
+ // - we have emitted our first .float/.double (to ensure that we find the
+ // created rodata in the binary)
+ // - we have emitted our first .double, if any (to ensure alignment of doubles
+ // in shifted rodata sections)
+ // - we have at least 5 words of rodata left to emit (otherwise IDO does not
+ // generate a jump table)
+ // - we have at least 10 more instructions to go in this function (otherwise our
+ // function size computation will be wrong since the delay slot goes unused)
+ if !needs_double
+ && state.use_jtbl_for_rodata
+ && i >= 1
+ && size - i >= jtbl_min_rodata_size
+ && num_instr - late_rodata_fn_output.len() >= jtbl_size + 1
+ {
+ let line = if state.pascal {
+ let cases: String = (0..(size - i))
+ .map(|case| format!("{}: ;", case))
+ .collect::<Vec<String>>()
+ .join("\n");
+ format!("case 0 of {} otherwise end;", cases)
+ } else {
+ let cases: String = (0..(size - i))
+ .map(|case| format!("case {}:", case))
+ .collect::<Vec<String>>()
+ .join(" ");
+ format!("switch (*(volatile int*)0) {{ {} ; }}", cases)
+ };
+ late_rodata_fn_output.push(line);
+ late_rodata_fn_output.extend(iter::repeat_n("".to_owned(), jtbl_size - 1));
+ jtbl_rodata_size = (size - i) * 4;
+ extra_mips1_nop = i != 2;
+ break;
+ }
+
+ let dummy_bytes = state.next_late_rodata_hex();
+ late_rodata_dummy_bytes.push(dummy_bytes);
+ if self.late_rodata_alignment == 4 * ((i + 1) % 2 + 1) && i + 1 < size {
+ let dummy_bytes2 = state.next_late_rodata_hex();
+ late_rodata_dummy_bytes.push(dummy_bytes2);
+ let combined = [dummy_bytes, dummy_bytes2].concat().try_into().unwrap();
+ let fval = f64::from_be_bytes(combined);
+ let line = if state.pascal {
+ state.pascal_assignment_double(fval)
+ } else {
+ format!("*(volatile double*)0 = {:?};", fval)
+ };
+ late_rodata_fn_output.push(line);
+ skip_next = true;
+ needs_double = false;
+ if state.mips1 {
+ // mips1 does not have ldc1/sdc1
+ late_rodata_fn_output.push("".to_owned());
+ late_rodata_fn_output.push("".to_owned());
+ }
+ extra_mips1_nop = false;
+ } else {
+ let fval = f32::from_be_bytes(dummy_bytes);
+ let line = if state.pascal {
+ state.pascal_assignment_float(fval)
+ } else {
+ format!("*(volatile float*)0 = {:?}f;", fval)
+ };
+ late_rodata_fn_output.push(line);
+ extra_mips1_nop = true;
+ }
+ late_rodata_fn_output.push("".to_owned());
+ late_rodata_fn_output.push("".to_owned());
+ }
+
+ if state.mips1 && extra_mips1_nop {
+ late_rodata_fn_output.push("".to_owned());
+ }
+ }
+
+ let mut text_name = None;
+ if self.fn_section_sizes[InputSection::Text] > 0 || !late_rodata_fn_output.is_empty() {
+ let new_name = state.make_name("func");
+ src[0] = state.func_prologue(&new_name);
+ text_name = Some(new_name);
+ src[self.num_lines] = state.func_epilogue();
+ let instr_count = self.fn_section_sizes[InputSection::Text] / 4;
+ if instr_count < state.min_instr_count {
+ return self.fail_without_line("too short .text block");
+ }
+ let mut tot_emitted = 0;
+ let mut tot_skipped = 0;
+ let mut fn_emitted = 0;
+ let mut fn_skipped = 0;
+ let mut skipping = true;
+ let mut rodata_stack: Vec<String> = late_rodata_fn_output.clone();
+ rodata_stack.reverse();
+
+ for &(line, count) in &self.fn_ins_inds {
+ for _ in 0..count {
+ if fn_emitted > Self::MAX_FN_SIZE
+ && instr_count - tot_emitted > state.min_instr_count
+ && (rodata_stack.is_empty() || !rodata_stack.last().unwrap().is_empty())
+ {
+ // Don't let functions become too large. When a function reaches 284
+ // instructions, and -O2 -framepointer flags are passed, the IRIX
+ // compiler decides it is a great idea to start optimizing more.
+ // Also, Pascal cannot handle too large functions before it runs out
+ // of unique statements to write.
+ fn_emitted = 0;
+ fn_skipped = 0;
+ skipping = true;
+ let large_func_name = state.make_name("large_func");
+ src[line] += &format!(
+ " {} {} ",
+ state.func_epilogue(),
+ state.func_prologue(&large_func_name)
+ );
+ }
+
+ let skip_for_late_rodata = if !rodata_stack.is_empty() {
+ state.prelude_if_late_rodata
+ } else {
+ 0
+ };
+ if skipping && fn_skipped < state.skip_instr_count + skip_for_late_rodata {
+ fn_skipped += 1;
+ tot_skipped += 1;
+ } else {
+ skipping = false;
+ if let Some(entry) = rodata_stack.pop() {
+ src[line] += &entry;
+ } else if state.pascal {
+ src[line] += &state.pascal_assignment_int(0);
+ } else {
+ src[line] += "*(volatile int*)0 = 0;";
+ }
+ }
+ tot_emitted += 1;
+ fn_emitted += 1;
+ }
+ }
+
+ if !rodata_stack.is_empty() {
+ let size = late_rodata_fn_output.len() / 3;
+ let available = instr_count - tot_skipped;
+ return self.fail_without_line(&format!(
+ "late rodata to text ratio is too high: {} / {} must be <= 1/3\n
+ add .late_rodata_alignment (4|8) to the .late_rodata block
+ to double the allowed ratio.",
+ size, available
+ ));
+ }
+ }
+
+ let mut rodata_name = None;
+ if self.fn_section_sizes[InputSection::Rodata] > 0 {
+ if state.pascal {
+ return self.fail_without_line(".rodata isn't supported with Pascal for now");
+ }
+ let new_name = state.make_name("rodata");
+ src[self.num_lines] += &format!(
+ " const char {}[{}] = {{1}};",
+ new_name,
+ self.fn_section_sizes[InputSection::Rodata]
+ );
+ rodata_name = Some(new_name);
+ }
+
+ let mut data_name = None;
+ if self.fn_section_sizes[InputSection::Data] > 0 {
+ let new_name = state.make_name("data");
+ let line = if state.pascal {
+ format!(
+ " var {}: packed array[1..{}] of char := [otherwise: 0];",
+ new_name,
+ self.fn_section_sizes[InputSection::Data]
+ )
+ } else {
+ format!(
+ " char {}[{}] = {{1}};",
+ new_name,
+ self.fn_section_sizes[InputSection::Data]
+ )
+ };
+ src[self.num_lines] += &line;
+ data_name = Some(new_name);
+ }
+
+ let mut bss_name = None;
+ if self.fn_section_sizes[InputSection::Bss] > 0 {
+ let new_name = state.make_name("bss");
+ if state.pascal {
+ return self.fail_without_line(".bss isn't supported with Pascal for now");
+ }
+ src[self.num_lines] += &format!(
+ " char {}[{}];",
+ new_name,
+ self.fn_section_sizes[InputSection::Bss]
+ );
+ bss_name = Some(new_name);
+ }
+
+ let mut data = EnumMap::default();
+ data[OutputSection::Text] = (text_name, self.fn_section_sizes[InputSection::Text]);
+ data[OutputSection::Data] = (data_name, self.fn_section_sizes[InputSection::Data]);
+ data[OutputSection::Rodata] = (rodata_name, self.fn_section_sizes[InputSection::Rodata]);
+ data[OutputSection::Bss] = (bss_name, self.fn_section_sizes[InputSection::Bss]);
+ let ret_fn = Function {
+ text_glabels: self.text_glabels.clone(),
+ asm_conts: self.asm_conts.clone(),
+ late_rodata_dummy_bytes,
+ jtbl_rodata_size,
+ late_rodata_asm_conts: self.late_rodata_asm_conts.clone(),
+ fn_desc: self.fn_desc.clone(),
+ data,
+ };
+
+ Ok((src, ret_fn))
+ }
+}
+
+/// Convert a float string to its hexadecimal representation
+fn repl_float_hex(cap: &regex_lite::Captures) -> String {
+ let float_str = cap[0].trim().trim_end_matches('f');
+ let float_val = float_str.parse::<f32>().unwrap();
+ let hex_val = f32::to_be_bytes(float_val);
+ format!("{}", u32::from_be_bytes(hex_val))
+}
+
+pub(crate) struct ParseSourceResult {
+ pub functions: Vec<Function>,
+ pub deps: Vec<String>,
+ pub output: Vec<u8>,
+}
+
+pub(crate) fn parse_source(
+ infile_path: &Path,
+ args: &AsmProcArgs,
+ opts: &CompileOpts,
+ encode: bool,
+) -> Result<ParseSourceResult> {
+ let (mut min_instr_count, mut skip_instr_count) = match opts.opt {
+ OptLevel::O0 => match opts.framepointer {
+ true => (8, 8),
+ false => (4, 4),
+ },
+ OptLevel::O1 | OptLevel::O2 => match opts.framepointer {
+ true => (6, 5),
+ false => (2, 1),
+ },
+ OptLevel::G => match opts.framepointer {
+ true => (7, 7),
+ false => (4, 4),
+ },
+ OptLevel::G3 => match opts.framepointer {
+ true => (4, 4),
+ false => (2, 2),
+ },
+ };
+
+ let mut prelude_if_late_rodata = 0;
+ if opts.kpic {
+ // Without optimizations, the PIC prelude always takes up 3 instructions.
+ // With optimizations, the prelude is optimized out if there's no late rodata.
+ if matches!(opts.opt, OptLevel::O2 | OptLevel::G3) {
+ prelude_if_late_rodata = 3;
+ } else {
+ min_instr_count += 3;
+ skip_instr_count += 3;
+ }
+ }
+
+ let use_jtbl_for_rodata =
+ matches!(opts.opt, OptLevel::O2 | OptLevel::G3) && !opts.framepointer && !opts.kpic;
+
+ let mut state = GlobalState::new(
+ min_instr_count,
+ skip_instr_count,
+ use_jtbl_for_rodata,
+ prelude_if_late_rodata,
+ opts.mips1,
+ opts.pascal,
+ );
+ let input_enc = &args.input_enc;
+ let output_enc = &args.output_enc;
+ let mut global_asm: Option<(GlobalAsmBlock, usize)> = None;
+ let mut asm_functions: Vec<Function> = vec![];
+ let mut output_lines: Vec<String> = vec![format!("#line 1 \"{}\"", infile_path.display())];
+ let mut deps: Vec<String> = vec![];
+
+ let mut is_cutscene_data = false;
+ let mut is_early_include = false;
+
+ let cutscene_re = Regex::new(r"CutsceneData (.|\n)*\[\] = \{")?;
+ let float_re = Regex::new(r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?f")?;
+
+ let file_contents = fs::read(infile_path)?;
+ for (line_no, line) in input_enc.decode(&file_contents)?.lines().enumerate() {
+ let line_no = line_no + 1;
+ let mut raw_line = line.trim().to_owned();
+ let line = raw_line.trim_start();
+
+ // Print exactly one output line per source line, to make compiler
+ // errors have correct line numbers. These will be overridden with
+ // reasonable content further down.
+ output_lines.push("".to_owned());
+
+ if let Some((ref mut gasm, start_index)) = global_asm {
+ if line.starts_with(')') {
+ let (mut src, fun) = gasm.finish(&mut state)?;
+ if state.pascal {
+ // Pascal has a 1600-character line length limit, so some
+ // of the lines we emit may be broken up. Correct for that
+ // using a #line directive.
+ *src.last_mut().unwrap() += &format!("\n#line {}", line_no + 1);
+ }
+ for (i, line2) in src.iter().enumerate() {
+ output_lines[start_index + i] = line2.clone();
+ }
+ asm_functions.push(fun);
+ global_asm = None;
+ } else {
+ gasm.process_line(&raw_line, output_enc)?;
+ }
+ } else if line == "GLOBAL_ASM(" || line == "#pragma GLOBAL_ASM(" {
+ global_asm = Some((
+ GlobalAsmBlock::new(format!("GLOBAL_ASM block at line {}", &line_no.to_string())),
+ output_lines.len(),
+ ));
+ } else if ((line.starts_with("GLOBAL_ASM(\"") || line.starts_with("#pragma GLOBAL_ASM(\""))
+ && line.ends_with("\")"))
+ || ((line.starts_with("INCLUDE_ASM(\"") || line.starts_with("INCLUDE_RODATA(\""))
+ && line.contains("\",")
+ && line.ends_with(");"))
+ {
+ let (prologue, fname) = if line.starts_with("INCLUDE_") {
+ // INCLUDE_ASM("path/to", functionname);
+ let (before, after) = line.split_once("\",").unwrap();
+ let fname = format!(
+ "{}/{}.s",
+ before[before.find('(').unwrap() + 2..].to_owned(),
+ after.trim()[..after.len() - 3].trim()
+ );
+
+ if line.starts_with("INCLUDE_RODATA") {
+ (vec![".section .rodata".to_string()], fname)
+ } else {
+ (vec![], fname)
+ }
+ } else {
+ // GLOBAL_ASM("path/to/file.s")
+ let fname = line[line.find('(').unwrap() + 2..line.len() - 2].to_string();
+ (vec![], fname)
+ };
+
+ let mut gasm = GlobalAsmBlock::new(fname.clone());
+ for line2 in prologue {
+ gasm.process_line(line2.trim_end(), output_enc)?;
+ }
+
+ if !Path::new(&fname).exists() {
+ // The GLOBAL_ASM block might be surrounded by an ifdef, so it's
+ // not clear whether a missing file actually represents a compile
+ // error. Pass the responsibility for determining that on to the
+ // compiler by emitting a bad include directive. (IDO treats
+ // #error as a warning for some reason.)
+ let output_lines_len = output_lines.len();
+ output_lines[output_lines_len - 1] = format!("#include \"GLOBAL_ASM:{}\"", fname);
+ continue;
+ }
+
+ let file_contents = fs::read(&fname)?;
+ for line2 in input_enc.decode(&file_contents)?.lines() {
+ gasm.process_line(line2.trim_end(), output_enc)?;
+ }
+
+ let (mut src, fun) = gasm.finish(&mut state)?;
+ let output_lines_len = output_lines.len();
+ if state.pascal {
+ // Pascal has a 1600-character line length limit, so avoid putting
+ // everything on the same line.
+ src.push(format!("#line {}", line_no + 1));
+ output_lines[output_lines_len - 1] = src.join("\n");
+ } else {
+ output_lines[output_lines_len - 1] = src.join("");
+ }
+ asm_functions.push(fun);
+ deps.push(fname);
+ } else if line == "#pragma asmproc recurse" {
+ // C includes qualified as
+ // #pragma asmproc recurse
+ // #include "file.c"
+ // will be processed recursively when encountered
+ is_early_include = true;
+ } else if is_early_include {
+ // Previous line was a #pragma asmproc recurse
+ is_early_include = false;
+ if !line.starts_with("#include ") {
+ return Err(anyhow::anyhow!(
+ "#pragma asmproc recurse must be followed by an #include "
+ ));
+ }
+ let fpath = infile_path.parent().unwrap();
+ let fname = fpath.join(line[line.find(' ').unwrap() + 2..].trim());
+ deps.push(fname.to_str().unwrap().to_string());
+ let mut res = parse_source(&fname, args, opts, false)?;
+ deps.append(&mut res.deps);
+ let res_str = format!(
+ "{}#line {} \"{}\"",
+ String::from_utf8(res.output).expect("nested calls generate utf-8"),
+ line_no + 1,
+ infile_path.file_name().unwrap().to_str().unwrap()
+ );
+
+ let output_lines_len = output_lines.len();
+ output_lines[output_lines_len - 1] = res_str;
+ } else {
+ if args.encode_cutscene_data_floats {
+ // This is a hack to replace all floating-point numbers in an array of a particular type
+ // (in this case CutsceneData) with their corresponding IEEE-754 hexadecimal representation
+ if cutscene_re.is_match(line) {
+ is_cutscene_data = true;
+ } else if line.ends_with("};") {
+ is_cutscene_data = false;
+ }
+
+ if is_cutscene_data {
+ raw_line = float_re.replace_all(&raw_line, repl_float_hex).into_owned();
+ }
+ }
+ let output_lines_len = output_lines.len();
+ output_lines[output_lines_len - 1] = raw_line.to_owned();
+ }
+ }
+
+ let out_data = if encode {
+ let newline_encoded = output_enc.encode("\n")?;
+
+ let mut data = vec![];
+ for line in output_lines {
+ let line_encoded = output_enc.encode(&line)?;
+ data.write_all(&line_encoded)?;
+ data.write_all(&newline_encoded)?;
+ }
+ data
+ } else {
+ let str = format!("{}\n", output_lines.join("\n"));
+
+ str.as_bytes().to_vec()
+ };
+
+ Ok(ParseSourceResult {
+ functions: asm_functions,
+ deps,
+ output: out_data,
+ })
+}