use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::error::Error;
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::{BufReader, BufWriter, Read, Seek, Write};
use std::path::Path;
use std::sync::Arc;
use lazycell::LazyCell;
use parking_lot::Mutex;
use regex::Regex;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zip::{write::FileOptions, ZipWriter};
use symbolic_common::{Arch, AsSelf, CodeId, DebugId};
use crate::base::*;
use crate::shared::Parse;
use crate::{DebugSession, ObjectKind, ObjectLike};
static BUNDLE_MAGIC: [u8; 4] = *b"SYSB";
static BUNDLE_VERSION: u32 = 2;
static MANIFEST_PATH: &str = "manifest.json";
static FILES_PATH: &str = "files";
lazy_static::lazy_static! {
static ref SANE_PATH_RE: Regex = Regex::new(r#":?[/\\]+"#).unwrap();
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SourceBundleErrorKind {
BadZip,
BadManifest,
BadDebugFile,
WriteFailed,
}
impl fmt::Display for SourceBundleErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadZip => write!(f, "malformed zip archive"),
Self::BadManifest => write!(f, "failed to read/write source bundle manifest"),
Self::BadDebugFile => write!(f, "malformed debug info file"),
Self::WriteFailed => write!(f, "failed to write source bundle"),
}
}
}
#[derive(Debug, Error)]
#[error("{kind}")]
pub struct SourceBundleError {
kind: SourceBundleErrorKind,
#[source]
source: Option<Box<dyn Error + Send + Sync + 'static>>,
}
impl SourceBundleError {
pub fn new<E>(kind: SourceBundleErrorKind, source: E) -> Self
where
E: Into<Box<dyn Error + Send + Sync>>,
{
let source = Some(source.into());
Self { kind, source }
}
pub fn kind(&self) -> SourceBundleErrorKind {
self.kind
}
}
impl From<SourceBundleErrorKind> for SourceBundleError {
fn from(kind: SourceBundleErrorKind) -> Self {
Self { kind, source: None }
}
}
fn trim_end_matches<F>(string: &mut String, pat: F)
where
F: FnMut(char) -> bool,
{
let cutoff = string.trim_end_matches(pat).len();
string.truncate(cutoff);
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceFileType {
Source,
MinifiedSource,
SourceMap,
IndexedRamBundle,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SourceFileInfo {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
ty: Option<SourceFileType>,
#[serde(default, skip_serializing_if = "String::is_empty")]
path: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
url: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
headers: BTreeMap<String, String>,
}
impl SourceFileInfo {
pub fn new() -> Self {
Self::default()
}
pub fn ty(&self) -> Option<SourceFileType> {
self.ty
}
pub fn set_ty(&mut self, ty: SourceFileType) {
self.ty = Some(ty);
}
pub fn path(&self) -> Option<&str> {
match self.path.as_str() {
"" => None,
path => Some(path),
}
}
pub fn set_path(&mut self, path: String) {
self.path = path;
}
pub fn url(&self) -> Option<&str> {
match self.url.as_str() {
"" => None,
url => Some(url),
}
}
pub fn set_url(&mut self, url: String) {
self.url = url;
}
pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
self.headers.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
pub fn header(&self, header: &str) -> Option<&str> {
self.headers.get(header).map(String::as_str)
}
pub fn add_header(&mut self, header: String, value: String) {
self.headers.insert(header, value);
}
pub fn is_empty(&self) -> bool {
self.path.is_empty() && self.ty.is_none() && self.headers.is_empty()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct SourceBundleVersion(pub u32);
impl SourceBundleVersion {
pub fn new(version: u32) -> Self {
Self(version)
}
pub fn is_valid(self) -> bool {
self.0 <= BUNDLE_VERSION
}
pub fn is_latest(self) -> bool {
self.0 == BUNDLE_VERSION
}
}
impl Default for SourceBundleVersion {
fn default() -> Self {
Self(BUNDLE_VERSION)
}
}
#[repr(C, packed)]
#[derive(Clone, Copy, Debug)]
struct SourceBundleHeader {
pub magic: [u8; 4],
pub version: u32,
}
impl SourceBundleHeader {
fn as_bytes(&self) -> &[u8] {
let ptr = self as *const Self as *const u8;
unsafe { std::slice::from_raw_parts(ptr, std::mem::size_of::<Self>()) }
}
}
impl Default for SourceBundleHeader {
fn default() -> Self {
SourceBundleHeader {
magic: BUNDLE_MAGIC,
version: BUNDLE_VERSION,
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
struct SourceBundleManifest {
#[serde(default)]
pub files: BTreeMap<String, SourceFileInfo>,
#[serde(flatten)]
pub attributes: BTreeMap<String, String>,
}
pub struct SourceBundle<'data> {
manifest: Arc<SourceBundleManifest>,
archive: Arc<Mutex<zip::read::ZipArchive<std::io::Cursor<&'data [u8]>>>>,
data: &'data [u8],
}
impl fmt::Debug for SourceBundle<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SourceBundle")
.field("code_id", &self.code_id())
.field("debug_id", &self.debug_id())
.field("arch", &self.arch())
.field("kind", &self.kind())
.field("load_address", &format_args!("{:#x}", self.load_address()))
.field("has_symbols", &self.has_symbols())
.field("has_debug_info", &self.has_debug_info())
.field("has_unwind_info", &self.has_unwind_info())
.field("has_sources", &self.has_sources())
.field("is_malformed", &self.is_malformed())
.finish()
}
}
impl<'data> SourceBundle<'data> {
pub fn test(bytes: &[u8]) -> bool {
bytes.starts_with(&BUNDLE_MAGIC)
}
pub fn parse(data: &'data [u8]) -> Result<SourceBundle<'data>, SourceBundleError> {
let mut archive = zip::read::ZipArchive::new(std::io::Cursor::new(data))
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
let manifest_file = archive
.by_name("manifest.json")
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
let manifest = serde_json::from_reader(manifest_file)
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadManifest, e))?;
Ok(SourceBundle {
manifest: Arc::new(manifest),
archive: Arc::new(Mutex::new(archive)),
data,
})
}
pub fn version(&self) -> SourceBundleVersion {
SourceBundleVersion(BUNDLE_VERSION)
}
pub fn file_format(&self) -> FileFormat {
FileFormat::SourceBundle
}
pub fn code_id(&self) -> Option<CodeId> {
self.manifest
.attributes
.get("code_id")
.and_then(|x| x.parse().ok())
}
pub fn debug_id(&self) -> DebugId {
self.manifest
.attributes
.get("debug_id")
.and_then(|x| x.parse().ok())
.unwrap_or_default()
}
pub fn name(&self) -> Option<&str> {
self.manifest
.attributes
.get("object_name")
.map(|x| x.as_str())
}
pub fn arch(&self) -> Arch {
self.manifest
.attributes
.get("arch")
.and_then(|s| s.parse().ok())
.unwrap_or_default()
}
fn kind(&self) -> ObjectKind {
ObjectKind::Sources
}
pub fn load_address(&self) -> u64 {
0
}
pub fn has_symbols(&self) -> bool {
false
}
pub fn symbols(&self) -> SourceBundleSymbolIterator<'data> {
std::iter::empty()
}
pub fn symbol_map(&self) -> SymbolMap<'data> {
self.symbols().collect()
}
pub fn has_debug_info(&self) -> bool {
false
}
pub fn debug_session(&self) -> Result<SourceBundleDebugSession<'data>, SourceBundleError> {
Ok(SourceBundleDebugSession {
manifest: self.manifest.clone(),
archive: self.archive.clone(),
files_by_path: LazyCell::new(),
})
}
pub fn has_unwind_info(&self) -> bool {
false
}
pub fn has_sources(&self) -> bool {
true
}
pub fn is_malformed(&self) -> bool {
false
}
pub fn data(&self) -> &'data [u8] {
self.data
}
pub fn is_empty(&self) -> bool {
self.manifest.files.is_empty()
}
}
impl<'slf, 'data: 'slf> AsSelf<'slf> for SourceBundle<'data> {
type Ref = SourceBundle<'slf>;
fn as_self(&'slf self) -> &Self::Ref {
unsafe { std::mem::transmute(self) }
}
}
impl<'data> Parse<'data> for SourceBundle<'data> {
type Error = SourceBundleError;
fn parse(data: &'data [u8]) -> Result<Self, Self::Error> {
SourceBundle::parse(data)
}
fn test(data: &'data [u8]) -> bool {
SourceBundle::test(data)
}
}
impl<'data: 'object, 'object> ObjectLike<'data, 'object> for SourceBundle<'data> {
type Error = SourceBundleError;
type Session = SourceBundleDebugSession<'data>;
type SymbolIterator = SourceBundleSymbolIterator<'data>;
fn file_format(&self) -> FileFormat {
self.file_format()
}
fn code_id(&self) -> Option<CodeId> {
self.code_id()
}
fn debug_id(&self) -> DebugId {
self.debug_id()
}
fn arch(&self) -> Arch {
self.arch()
}
fn kind(&self) -> ObjectKind {
self.kind()
}
fn load_address(&self) -> u64 {
self.load_address()
}
fn has_symbols(&self) -> bool {
self.has_symbols()
}
fn symbol_map(&self) -> SymbolMap<'data> {
self.symbol_map()
}
fn symbols(&self) -> Self::SymbolIterator {
self.symbols()
}
fn has_debug_info(&self) -> bool {
self.has_debug_info()
}
fn debug_session(&self) -> Result<Self::Session, Self::Error> {
self.debug_session()
}
fn has_unwind_info(&self) -> bool {
self.has_unwind_info()
}
fn has_sources(&self) -> bool {
self.has_sources()
}
fn is_malformed(&self) -> bool {
self.is_malformed()
}
}
pub type SourceBundleSymbolIterator<'data> = std::iter::Empty<Symbol<'data>>;
pub struct SourceBundleDebugSession<'data> {
manifest: Arc<SourceBundleManifest>,
archive: Arc<Mutex<zip::read::ZipArchive<std::io::Cursor<&'data [u8]>>>>,
files_by_path: LazyCell<HashMap<String, String>>,
}
impl<'data> SourceBundleDebugSession<'data> {
pub fn files(&self) -> SourceBundleFileIterator<'_> {
SourceBundleFileIterator {
files: self.manifest.files.values(),
}
}
pub fn functions(&self) -> SourceBundleFunctionIterator<'_> {
std::iter::empty()
}
fn get_files_by_path(&self) -> HashMap<String, String> {
let files = &self.manifest.files;
let mut files_by_path = HashMap::with_capacity(files.len());
for (zip_path, file_info) in files {
if !file_info.path.is_empty() {
files_by_path.insert(file_info.path.clone(), zip_path.clone());
}
}
files_by_path
}
fn zip_path_by_source_path(&self, path: &str) -> Option<&str> {
self.files_by_path
.borrow_with(|| self.get_files_by_path())
.get(path)
.map(|zip_path| zip_path.as_str())
}
fn source_by_zip_path(&self, zip_path: &str) -> Result<Option<String>, SourceBundleError> {
let mut archive = self.archive.lock();
let mut file = archive
.by_name(zip_path)
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
let mut source_content = String::new();
file.read_to_string(&mut source_content)
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadZip, e))?;
Ok(Some(source_content))
}
pub fn source_by_path(&self, path: &str) -> Result<Option<Cow<'_, str>>, SourceBundleError> {
let zip_path = match self.zip_path_by_source_path(path) {
Some(zip_path) => zip_path,
None => return Ok(None),
};
self.source_by_zip_path(zip_path)
.map(|opt| opt.map(Cow::Owned))
}
}
impl<'data, 'session> DebugSession<'session> for SourceBundleDebugSession<'data> {
type Error = SourceBundleError;
type FunctionIterator = SourceBundleFunctionIterator<'session>;
type FileIterator = SourceBundleFileIterator<'session>;
fn functions(&'session self) -> Self::FunctionIterator {
self.functions()
}
fn files(&'session self) -> Self::FileIterator {
self.files()
}
fn source_by_path(&self, path: &str) -> Result<Option<Cow<'_, str>>, Self::Error> {
self.source_by_path(path)
}
}
pub struct SourceBundleFileIterator<'s> {
files: std::collections::btree_map::Values<'s, String, SourceFileInfo>,
}
impl<'s> Iterator for SourceBundleFileIterator<'s> {
type Item = Result<FileEntry<'s>, SourceBundleError>;
fn next(&mut self) -> Option<Self::Item> {
let source_file = self.files.next()?;
Some(Ok(FileEntry {
compilation_dir: &[],
info: FileInfo::from_path(source_file.path.as_bytes()),
}))
}
}
pub type SourceBundleFunctionIterator<'s> =
std::iter::Empty<Result<Function<'s>, SourceBundleError>>;
impl SourceBundleManifest {
pub fn new() -> Self {
Self::default()
}
}
fn sanitize_bundle_path(path: &str) -> String {
let mut sanitized = SANE_PATH_RE.replace_all(path, "/").into_owned();
if sanitized.starts_with('/') {
sanitized.remove(0);
}
sanitized
}
pub struct SourceBundleWriter<W>
where
W: Seek + Write,
{
manifest: SourceBundleManifest,
writer: ZipWriter<W>,
}
impl<W> SourceBundleWriter<W>
where
W: Seek + Write,
{
pub fn start(mut writer: W) -> Result<Self, SourceBundleError> {
let header = SourceBundleHeader::default();
writer
.write_all(header.as_bytes())
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
Ok(SourceBundleWriter {
manifest: SourceBundleManifest::new(),
writer: ZipWriter::new(writer),
})
}
pub fn is_empty(&self) -> bool {
self.manifest.files.is_empty()
}
pub fn set_attribute<K, V>(&mut self, key: K, value: V) -> Option<String>
where
K: Into<String>,
V: Into<String>,
{
self.manifest.attributes.insert(key.into(), value.into())
}
pub fn remove_attribute<K>(&mut self, key: K) -> Option<String>
where
K: AsRef<str>,
{
self.manifest.attributes.remove(key.as_ref())
}
pub fn attribute<K>(&mut self, key: K) -> Option<&str>
where
K: AsRef<str>,
{
self.manifest
.attributes
.get(key.as_ref())
.map(String::as_str)
}
pub fn has_file<S>(&self, path: S) -> bool
where
S: AsRef<str>,
{
let full_path = &self.file_path(path.as_ref());
self.manifest.files.contains_key(full_path)
}
pub fn add_file<S, R>(
&mut self,
path: S,
mut file: R,
info: SourceFileInfo,
) -> Result<(), SourceBundleError>
where
S: AsRef<str>,
R: Read,
{
let full_path = self.file_path(path.as_ref());
let unique_path = self.unique_path(full_path);
self.writer
.start_file(unique_path.clone(), FileOptions::default())
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
std::io::copy(&mut file, &mut self.writer)
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
self.manifest.files.insert(unique_path, info);
Ok(())
}
pub fn write_object<'data, 'object, O, E>(
self,
object: &'object O,
object_name: &str,
) -> Result<bool, SourceBundleError>
where
O: ObjectLike<'data, 'object, Error = E>,
E: std::error::Error + Send + Sync + 'static,
{
self.write_object_with_filter(object, object_name, |_| true)
}
pub fn write_object_with_filter<'data, 'object, O, E, F>(
mut self,
object: &'object O,
object_name: &str,
mut filter: F,
) -> Result<bool, SourceBundleError>
where
O: ObjectLike<'data, 'object, Error = E>,
E: std::error::Error + Send + Sync + 'static,
F: FnMut(&FileEntry) -> bool,
{
let mut files_handled = BTreeSet::new();
let session = object
.debug_session()
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadDebugFile, e))?;
self.set_attribute("arch", object.arch().to_string());
self.set_attribute("debug_id", object.debug_id().to_string());
self.set_attribute("object_name", object_name);
if let Some(code_id) = object.code_id() {
self.set_attribute("code_id", code_id.to_string());
}
for file_result in session.files() {
let file = file_result
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadDebugFile, e))?;
let filename = file.abs_path_str();
if files_handled.contains(&filename) {
continue;
}
let source = if (filename.starts_with('<') && filename.ends_with('>')) || !filter(&file)
{
None
} else {
File::open(&filename).ok().map(BufReader::new)
};
if let Some(source) = source {
let bundle_path = sanitize_bundle_path(&filename);
let mut info = SourceFileInfo::new();
info.set_ty(SourceFileType::Source);
info.set_path(filename.clone());
self.add_file(bundle_path, source, info)?;
}
files_handled.insert(filename);
}
let is_empty = self.is_empty();
self.finish()?;
Ok(!is_empty)
}
pub fn finish(mut self) -> Result<(), SourceBundleError> {
self.write_manifest()?;
self.writer
.finish()
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
Ok(())
}
fn file_path(&self, path: &str) -> String {
format!("{}/{}", FILES_PATH, path)
}
fn unique_path(&self, mut path: String) -> String {
let mut duplicates = 0;
while self.manifest.files.contains_key(&path) {
duplicates += 1;
match duplicates {
1 => path.push_str(".1"),
_ => {
use std::fmt::Write;
trim_end_matches(&mut path, char::is_numeric);
write!(path, ".{}", duplicates).unwrap();
}
}
}
path
}
fn write_manifest(&mut self) -> Result<(), SourceBundleError> {
self.writer
.start_file(MANIFEST_PATH, FileOptions::default())
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
serde_json::to_writer(&mut self.writer, &self.manifest)
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::BadManifest, e))?;
Ok(())
}
}
impl SourceBundleWriter<BufWriter<File>> {
pub fn create<P>(path: P) -> Result<SourceBundleWriter<BufWriter<File>>, SourceBundleError>
where
P: AsRef<Path>,
{
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(|e| SourceBundleError::new(SourceBundleErrorKind::WriteFailed, e))?;
Self::start(BufWriter::new(file))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use similar_asserts::assert_eq;
#[test]
fn test_has_file() -> Result<(), SourceBundleError> {
let writer = Cursor::new(Vec::new());
let mut bundle = SourceBundleWriter::start(writer)?;
bundle.add_file("bar.txt", &b"filecontents"[..], SourceFileInfo::default())?;
assert!(bundle.has_file("bar.txt"));
bundle.finish()?;
Ok(())
}
#[test]
fn test_duplicate_files() -> Result<(), SourceBundleError> {
let writer = Cursor::new(Vec::new());
let mut bundle = SourceBundleWriter::start(writer)?;
bundle.add_file("bar.txt", &b"filecontents"[..], SourceFileInfo::default())?;
bundle.add_file("bar.txt", &b"othercontents"[..], SourceFileInfo::default())?;
assert!(bundle.has_file("bar.txt"));
assert!(bundle.has_file("bar.txt.1"));
bundle.finish()?;
Ok(())
}
#[test]
fn test_bundle_paths() {
assert_eq!(sanitize_bundle_path("foo"), "foo");
assert_eq!(sanitize_bundle_path("foo/bar"), "foo/bar");
assert_eq!(sanitize_bundle_path("/foo/bar"), "foo/bar");
assert_eq!(sanitize_bundle_path("C:/foo/bar"), "C/foo/bar");
assert_eq!(sanitize_bundle_path("\\foo\\bar"), "foo/bar");
assert_eq!(sanitize_bundle_path("\\\\UNC\\foo\\bar"), "UNC/foo/bar");
}
}