use std::{
ffi::{CStr, CString},
fs::File,
io::{self, ErrorKind, Read, Seek, SeekFrom},
path::Path,
};
use anyhow::Context;
use crate::pascal_str::PascalString;
const HEADER_SIZE: usize = 128;
#[derive(Debug, Default, Clone)]
pub struct Header {
pub magic_number: String,
pub file_version: u16,
pub dir_entry: DirEntry,
}
impl Header {
pub fn new(file_version: u16) -> Self {
let mut root_dir_entry = DirEntry::default();
root_dir_entry.tag_name = "tdir".to_string();
root_dir_entry.tag_number = 1;
root_dir_entry.element_size = 28;
root_dir_entry.element_type_code = 1023;
Self {
magic_number: "ABIF".to_string(),
file_version: file_version,
dir_entry: root_dir_entry,
}
}
pub fn from_bytes(bytes: &[u8; HEADER_SIZE]) -> anyhow::Result<Self> {
let mut header = Self::default();
header.magic_number = String::from_utf8(bytes[..4].to_vec())?;
if !header.magic_number.eq("ABIF") {
anyhow::bail!("not a valid magic number: {}", header.magic_number);
}
header.file_version = u16::from_be_bytes(bytes[4..6].try_into().context("try into error")?);
header.dir_entry = DirEntry::from_bytes(&bytes[6..(6 + DIR_ENTRY_SIZE)])?;
Ok(header)
}
pub fn to_bytes(&self) -> [u8; HEADER_SIZE] {
let mut header_bytes = [0_u8; HEADER_SIZE];
header_bytes[..4].copy_from_slice(self.magic_number.as_bytes());
header_bytes[4..6].copy_from_slice(&self.file_version.to_be_bytes());
header_bytes[6..(6 + DIR_ENTRY_SIZE)].copy_from_slice(&self.dir_entry.to_bytes());
header_bytes
}
}
const DIR_ENTRY_SIZE: usize = 28;
#[derive(Debug, Default, Clone)]
pub struct DirEntry {
pub tag_name: String, pub tag_number: u32,
pub element_type_code: u16,
pub element_size: u16,
pub num_elements: u32,
pub data_size: u32,
pub data_offset: u32,
pub data_handle: u32,
}
impl DirEntry {
pub fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
assert!(bytes.len() == DIR_ENTRY_SIZE);
Ok(Self {
tag_name: String::from_utf8(bytes[..4].to_vec())?,
tag_number: u32::from_be_bytes(bytes[4..8].try_into().context("try into error")?),
element_type_code: u16::from_be_bytes(
bytes[8..10].try_into().context("try into error")?,
),
element_size: u16::from_be_bytes(bytes[10..12].try_into().context("try into error")?),
num_elements: u32::from_be_bytes(bytes[12..16].try_into().context("try into error")?),
data_size: u32::from_be_bytes(bytes[16..20].try_into().context("try into error")?),
data_offset: u32::from_be_bytes(bytes[20..24].try_into().context("try into error")?),
data_handle: u32::from_be_bytes(bytes[24..28].try_into().context("try into error")?),
})
}
pub fn to_bytes(&self) -> [u8; DIR_ENTRY_SIZE] {
let mut result_bytes = [0_u8; DIR_ENTRY_SIZE];
result_bytes[..4].copy_from_slice(self.tag_name.as_bytes());
result_bytes[4..8].copy_from_slice(&self.tag_number.to_be_bytes());
result_bytes[8..10].copy_from_slice(&self.element_type_code.to_be_bytes());
result_bytes[10..12].copy_from_slice(&self.element_size.to_be_bytes());
result_bytes[12..16].copy_from_slice(&self.num_elements.to_be_bytes());
result_bytes[16..20].copy_from_slice(&self.data_size.to_be_bytes());
result_bytes[20..24].copy_from_slice(&self.data_offset.to_be_bytes());
result_bytes[24..28].copy_from_slice(&self.data_handle.to_be_bytes());
result_bytes
}
}
#[derive(Debug)]
struct AbiFileReader<R: Read + Seek> {
stream: R,
header: Header,
tagged_datas: Vec<TaggedData>,
}
impl<R: Read + Seek> AbiFileReader<R> {
pub fn new(mut stream: R) -> anyhow::Result<Self> {
let mut header_bytes = [0_u8; HEADER_SIZE];
stream
.read_exact(&mut header_bytes)
.context("read header error")?;
let header = Header::from_bytes(&header_bytes).context("bytes to header error")?;
eprintln!("AbiFileReader:{header:?}");
Ok(Self {
stream,
header,
tagged_datas: vec![],
})
}
#[allow(unused)]
pub fn header(&self) -> &Header {
&self.header
}
pub fn get_tagged_datas(&self) -> &[TaggedData] {
&self.tagged_datas
}
pub fn extract_tagged_datas(&mut self) -> anyhow::Result<()> {
for i in 0..self.header.dir_entry.num_elements {
let dir_entry_start = self.header.dir_entry.data_offset as usize
+ (i * self.header.dir_entry.element_size as u32) as usize;
self.stream.seek(SeekFrom::Start(dir_entry_start as u64))?;
let mut dir_buf = [0; DIR_ENTRY_SIZE];
if self.stream.read(&mut dir_buf)? == 0 {
break;
};
let dir = DirEntry::from_bytes(&dir_buf)?;
let tag_buf = if dir.data_size <= 4 {
let mut tag_buf = vec![0_u8; dir.data_size as usize];
tag_buf.copy_from_slice(&dir.data_offset.to_be_bytes()[..dir.data_size as usize]);
tag_buf
} else {
self.stream.seek(SeekFrom::Start(dir.data_offset as u64))?;
let mut tag_buf = vec![0; dir.data_size as usize];
if self.stream.read(&mut tag_buf)? == 0 {
break;
};
tag_buf
};
eprintln!("tag_info:{dir:?}");
let tag_data =
parse_tag_data(dir.element_type_code, dir.num_elements as usize, &tag_buf)?;
self.tagged_datas.push(TaggedData {
tag: dir.tag_name.clone(),
tag_number: dir.tag_number,
data: tag_data,
});
}
return Ok(());
}
}
pub struct AbiFile {
header: Header,
tagged_datas: Vec<TaggedData>,
data_entries: Vec<DirEntry>,
write_position: u32,
}
impl AbiFile {
pub fn new(version: u16) -> Self {
let header = Header::new(version);
Self {
header,
tagged_datas: vec![],
data_entries: vec![],
write_position: 128,
}
}
pub fn push_tagged_data(&mut self, tagged_data: TaggedData) {
self.header.dir_entry.num_elements += 1;
self.header.dir_entry.data_size =
self.header.dir_entry.element_size as u32 * self.header.dir_entry.num_elements;
self.data_entries
.push(tagged_data.build_dir_entry(self.write_position));
if tagged_data.data.get_data_size() > 4 {
self.write_position += tagged_data.data.get_data_size();
}
self.tagged_datas.push(tagged_data);
self.header.dir_entry.data_offset = self.write_position;
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut result_bytes = vec![];
result_bytes.extend_from_slice(&self.header.to_bytes());
for tagged_data in &self.tagged_datas {
if tagged_data.data.get_data_size() > 4 {
result_bytes.extend_from_slice(&tagged_data.data.to_bytes());
}
}
for entry in &self.data_entries {
result_bytes.extend_from_slice(&entry.to_bytes());
}
result_bytes
}
}
#[derive(Debug, Clone)]
pub struct TaggedData {
pub tag: String, pub tag_number: u32,
pub data: Data,
}
impl TaggedData {
pub fn build_dir_entry(&self, data_offset: u32) -> DirEntry {
let mut entry = DirEntry::default();
entry.tag_name = self.tag.clone();
entry.tag_number = self.tag_number;
entry.element_type_code = self.data.element_code();
entry.element_size = self.data.get_ele_size();
entry.num_elements = self.data.get_num_ele();
entry.data_offset = data_offset;
entry.data_size = self.data.get_data_size();
if entry.data_size <= 4 {
let mut b16_bytes = [0_u8; 4];
let data_bytes = self.data.to_bytes();
let copy_start = 4 - data_bytes.len();
b16_bytes[copy_start..].copy_from_slice(&data_bytes);
entry.data_offset = u32::from_be_bytes(b16_bytes);
}
entry
}
}
#[derive(Debug, Clone)]
pub enum Data {
U8(Vec<u8>),
U16(Vec<u16>),
U32(Vec<u32>),
String(String),
PascallString(PascalString),
CString(CString),
}
impl Data {
pub fn get_ele_size(&self) -> u16 {
match self {
Self::U8(_) => 1,
Self::U16(_) => 2,
Self::U32(_) => 4,
Self::String(_) => 1,
Self::PascallString(_) => 1,
Self::CString(_) => 1,
}
}
pub fn get_num_ele(&self) -> u32 {
let num_ele = match self {
Self::U8(v) => v.len(),
Self::U16(v) => v.len(),
Self::U32(v) => v.len(),
Self::String(v) => v.len(),
Self::PascallString(v) => v.bytes_len(),
Self::CString(v) => v.as_bytes().len() + 1,
};
num_ele as u32
}
pub fn get_data_size(&self) -> u32 {
self.get_ele_size() as u32 * self.get_num_ele()
}
pub fn to_bytes(&self) -> Vec<u8> {
match self {
Self::U8(v) => v.to_vec(),
Self::U16(v) => v.iter().flat_map(|v| v.to_be_bytes().into_iter()).collect(),
Self::U32(v) => v.iter().flat_map(|v| v.to_be_bytes().into_iter()).collect(),
Self::String(v) => v.as_bytes().to_vec(),
Self::PascallString(v) => v.bytes_raw(),
Self::CString(v) => v.as_bytes_with_nul().to_vec(),
}
}
pub fn element_code(&self) -> u16 {
match self {
Self::U8(_) => 1,
Self::U16(_) => 4,
Self::U32(_) => 5,
Self::String(_) => 2,
Self::PascallString(_) => 18,
Self::CString(_) => 19,
}
}
pub fn truncated_result(&self) -> String {
let print_ele = self.get_num_ele().min(100) as usize;
match self {
Self::U8(v) => format!("{:?}", &v[..print_ele]),
Self::U16(v) => format!("{:?}", &v[..print_ele]),
Self::U32(v) => format!("{:?}", &v[..print_ele]),
Self::String(v) => v.to_string(),
Self::PascallString(v) => v.to_string().unwrap(),
Self::CString(v) => v.to_str().unwrap().to_string(),
}
}
}
fn parse_tag_data(elem_code: u16, _elem_num: usize, data: &[u8]) -> io::Result<Data> {
match elem_code {
2 => Ok(Data::String(
std::str::from_utf8(data).unwrap_or("").to_string(),
)),
4 => {
let as_u16 = data
.chunks_exact(2)
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
.collect();
Ok(Data::U16(as_u16))
}
5 => {
let as_u32 = data
.chunks_exact(4)
.map(|chunk| u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
Ok(Data::U32(as_u32))
}
18 => Ok(Data::PascallString(PascalString::new(data.to_vec()))),
19 => Ok(Data::CString(unsafe {
CStr::from_ptr(data.as_ptr() as *const i8).to_owned()
})),
_ => {
Err(io::Error::new(
ErrorKind::InvalidData,
format!("Invalid data type in AB1 file: {elem_code}"),
))
}
}
}
pub fn import_ab1(path: &Path) -> anyhow::Result<Vec<TaggedData>> {
let file = File::open(path)?;
let mut iterator = AbiFileReader::new(file)?;
iterator.extract_tagged_datas()?;
Ok(iterator.get_tagged_datas().to_vec())
}
pub fn build_ploc1(data: Vec<u16>) -> TaggedData {
TaggedData {
tag: format!("PLOC"),
tag_number: 1,
data: Data::U16(data),
}
}
pub fn build_pcon1(data: Vec<u8>) -> TaggedData {
TaggedData {
tag: format!("PCON"),
tag_number: 1,
data: Data::U8(data),
}
}
pub fn build_fwo_1() -> TaggedData {
TaggedData {
tag: format!("FWO_"),
tag_number: 1,
data: Data::String(format!("GATC")),
}
}
pub fn build_lane() -> TaggedData {
TaggedData {
tag: format!("LANE"),
tag_number: 1,
data: Data::U16(vec![42]),
}
}
pub fn build_snr() -> TaggedData {
TaggedData {
tag: format!("S/N%"),
tag_number: 1,
data: Data::U16(vec![53, 75, 79, 48]),
}
}
pub fn build_smpl_1(name: String) -> TaggedData {
TaggedData {
tag: format!("SMPL"),
tag_number: 1,
data: Data::PascallString(
name.as_str()
.try_into()
.expect("string -> pascal string error"),
),
}
}
pub fn build_pbas_1(bases: String) -> TaggedData {
TaggedData {
tag: format!("PBAS"),
tag_number: 1,
data: Data::String(bases),
}
}
pub fn build_cmnt_1(comment: String) -> TaggedData {
TaggedData {
tag: format!("CMNT"),
tag_number: 1,
data: Data::PascallString(
comment
.as_str()
.try_into()
.expect("string -> pascal string error"),
),
}
}
pub fn build_pdmf_1(fname: String) -> TaggedData {
TaggedData {
tag: format!("PDMF"),
tag_number: 1,
data: Data::PascallString(
fname
.as_str()
.try_into()
.expect("string -> pascal string error"),
),
}
}
pub fn build_pdmf_2(fname: String) -> TaggedData {
TaggedData {
tag: format!("PDMF"),
tag_number: 2,
data: Data::PascallString(
fname
.as_str()
.try_into()
.expect("string -> pascal string error"),
),
}
}
pub fn build_data(tag_number: u32, data: Vec<u16>) -> TaggedData {
TaggedData {
tag: format!("DATA"),
tag_number: tag_number,
data: Data::U16(data),
}
}
#[cfg(test)]
mod test {
use std::{io::Write, path};
use crate::ab1::ab1_file::AbiFileReader;
#[test]
fn test_read_ab1() {
let fpath = "./test-data/Group_0_Adaptor-barcode295-2.sort.ab1";
let p = path::Path::new(fpath);
let records = super::import_ab1(p).expect("import ab1 error");
records.iter().for_each(|tagged_data| {
println!(
"{}{}:{}->{}",
tagged_data.tag,
tagged_data.tag_number,
tagged_data.data.get_num_ele(),
tagged_data.data.truncated_result()
)
});
}
#[test]
fn test_read_and_write_ab1() {
let fpath = "./test-data/S22509070002-Epi5A-1.ab1";
let file = std::fs::File::open(fpath).unwrap();
let mut reader = AbiFileReader::new(file).unwrap();
reader.extract_tagged_datas().unwrap();
let tagged_datas = reader.get_tagged_datas();
let header = reader.header();
let mut ab1_file = super::AbiFile::new(header.file_version);
tagged_datas.iter().for_each(|td| {
ab1_file.push_tagged_data(td.clone());
});
let ab1_filebytes = ab1_file.to_bytes();
let mut newfile = std::fs::File::create("./test-data/out.ab1").unwrap();
newfile.write_all(&ab1_filebytes).unwrap();
drop(newfile);
let records = super::import_ab1(&path::Path::new("./test-data/out.ab1")).unwrap();
records.iter().for_each(|tagged_data| {
println!(
"{}{}:{}->{}",
tagged_data.tag,
tagged_data.tag_number,
tagged_data.data.get_num_ele(),
tagged_data.data.truncated_result()
)
});
}
}