use std::collections::HashMap;
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::Path;
#[derive(Debug)]
pub struct ObbyArchive<R: Read + Seek> {
entries: HashMap<String, EntryInfo>,
reader: R,
data_start_pos: u64,
}
#[derive(Debug)]
struct EntryInfo {
offset: u64,
length: i32,
compressed_length: i32,
}
struct BinaryReader<R: Read> {
reader: R,
}
impl<R: Read> BinaryReader<R> {
fn new(reader: R) -> Self {
BinaryReader { reader }
}
fn read_u8(&mut self) -> io::Result<u8> {
let mut byte = [0u8; 1];
self.reader.read_exact(&mut byte)?;
Ok(byte[0])
}
fn read_bytes(&mut self, length: usize) -> io::Result<Vec<u8>> {
let mut buffer = vec![0u8; length];
self.reader.read_exact(&mut buffer)?;
Ok(buffer)
}
fn read_i32(&mut self) -> io::Result<i32> {
let mut bytes = [0u8; 4];
self.reader.read_exact(&mut bytes)?;
Ok(i32::from_le_bytes(bytes))
}
}
fn read_csharp_string<R: Read>(reader: &mut BinaryReader<R>) -> io::Result<String> {
let mut string_len = 0;
let mut done = false;
let mut step = 0;
while !done {
let byte = reader.read_u8()?;
string_len |= ((byte & 0x7F) as u32) << (step * 7);
done = (byte & 0x80) == 0;
step += 1;
}
let buf = reader.read_bytes(string_len as usize)?;
Ok(String::from_utf8_lossy(&buf).to_string())
}
impl<R: Read + Seek> ObbyArchive<R> {
pub fn new(mut reader: R) -> io::Result<Self> {
let mut binary_reader = BinaryReader::new(&mut reader);
let mut header = [0u8; 4];
binary_reader.reader.read_exact(&mut header)?;
if &header != b"OBBY" {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid plugin header"));
}
let _api_version = read_csharp_string(&mut binary_reader)?;
let _hash = binary_reader.read_bytes(48)?;
let mut is_signed = [0u8; 1];
binary_reader.reader.read_exact(&mut is_signed)?;
if is_signed[0] != 0 {
let _signature = binary_reader.read_bytes(384)?;
}
let _data_length = binary_reader.read_i32()?;
let _plugin_assembly = read_csharp_string(&mut binary_reader)?;
let _plugin_version = read_csharp_string(&mut binary_reader)?;
let entry_count = binary_reader.read_i32()? as usize;
let mut entries = HashMap::new();
let mut current_offset = 0u64;
for _ in 0..entry_count {
let name = read_csharp_string(&mut binary_reader)?;
let length = binary_reader.read_i32()?;
let compressed_length = binary_reader.read_i32()?;
entries.insert(name, EntryInfo {
offset: current_offset,
length,
compressed_length,
});
current_offset += compressed_length as u64;
}
let data_start_pos = reader.stream_position()?;
Ok(ObbyArchive {
entries,
reader,
data_start_pos,
})
}
pub fn list_entries(&self) -> Vec<String> {
self.entries.keys().cloned().collect()
}
pub fn extract_entry(&mut self, entry_name: &str) -> io::Result<Vec<u8>> {
let entry = self.entries.get(entry_name).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("Entry '{}' not found in archive", entry_name),
)
})?;
self.reader.seek(SeekFrom::Start(self.data_start_pos + entry.offset))?;
let mut reader = BinaryReader::new(&mut self.reader);
let compressed_data = reader.read_bytes(entry.compressed_length as usize)?;
if entry.compressed_length != entry.length {
let mut decompressed_data = Vec::new();
let mut decoder = flate2::read::DeflateDecoder::new(&compressed_data[..]);
decoder.read_to_end(&mut decompressed_data)?;
Ok(decompressed_data)
} else {
Ok(compressed_data)
}
}
}
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<ObbyArchive<File>> {
let file = File::open(path)?;
ObbyArchive::new(file)
}
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "wasm")]
use std::io::Cursor;
use js_sys::Uint8Array;
#[wasm_bindgen]
pub struct WasmObbyArchive {
inner: ObbyArchive<Cursor<Vec<u8>>>
}
#[wasm_bindgen]
impl WasmObbyArchive {
#[wasm_bindgen(constructor)]
pub fn new(buffer: &[u8]) -> Result<WasmObbyArchive, JsValue> {
let cursor = Cursor::new(buffer.to_vec());
let inner = ObbyArchive::new(cursor)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(WasmObbyArchive { inner })
}
#[wasm_bindgen]
pub fn list_entries(&self) -> Box<[JsValue]> {
self.inner
.list_entries()
.into_iter()
.map(JsValue::from)
.collect::<Vec<_>>()
.into_boxed_slice()
}
#[wasm_bindgen]
pub fn extract_entry(&mut self, entry_name: &str) -> Result<Uint8Array, JsValue> {
let data = self.inner
.extract_entry(entry_name)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(Uint8Array::from(&data[..]))
}
#[wasm_bindgen]
pub fn extract_plugin_json(&mut self) -> Result<String, JsValue> {
let data = self.extract_entry("plugin.json")?;
let text = String::from_utf8(data.to_vec())
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(text)
}
}
pub fn extract_plugin_json<P: AsRef<Path>>(path: P) -> io::Result<String> {
let mut archive = open(path)?;
let data = archive.extract_entry("plugin.json")?;
String::from_utf8(data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Cursor, Write};
use tempfile::NamedTempFile;
use flate2::{write::DeflateEncoder, Compression};
fn create_test_plugin_json() -> String {
r#"{
"id": "test-plugin",
"name": "Test Plugin",
"version": "1.0.0",
"description": "A test plugin"
}"#.to_string()
}
fn load_test_obby_bytes() -> Vec<u8> {
let mut file = File::open("test_dir/ObsidianPlugin.obby").unwrap();
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).unwrap();
buffer
}
#[test]
fn test_memory_buffer() {
let buffer = load_test_obby_bytes();
let cursor = Cursor::new(buffer);
let archive = ObbyArchive::new(cursor);
assert!(archive.is_ok());
}
}