use std::io::{Read, Seek, SeekFrom};
use crate::sector::SectorMode;
use crate::IsoError;
const MAX_CHUNK: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NrgVersion {
V1,
V2,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NrgTrack {
pub number: u8,
pub mode_code: u8,
pub start_offset: u64,
pub size: u64,
pub sector_size: u16,
pub start_lba: u32,
pub isrc: Option<String>,
}
impl NrgTrack {
#[must_use]
pub fn sector_mode(&self) -> Option<SectorMode> {
sector_mode_for(self.mode_code)
}
#[must_use]
pub fn is_data(&self) -> bool {
self.sector_mode().is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NrgImage {
pub version: NrgVersion,
pub catalog: Option<String>,
pub tracks: Vec<NrgTrack>,
}
impl NrgImage {
#[must_use]
pub fn track_count(&self) -> usize {
self.tracks.len()
}
#[must_use]
pub fn data_track(&self) -> Option<&NrgTrack> {
self.tracks.iter().find(|t| t.is_data())
}
}
#[must_use]
pub fn sector_mode_for(mode_code: u8) -> Option<SectorMode> {
match mode_code {
0x00 | 0x02 => Some(SectorMode::Iso2048), 0x03 => Some(SectorMode::Mode2_2336), 0x05 => Some(SectorMode::Raw2352), 0x06 => Some(SectorMode::Raw2352Mode2), 0x0F => Some(SectorMode::Raw2448), 0x11 => Some(SectorMode::Raw2448Mode2), _ => None, }
}
fn sector_size_for(mode_code: u8) -> u16 {
match mode_code {
0x00 | 0x02 => 2048,
0x03 => 2336,
0x0F..=0x11 => 2448,
_ => 2352, }
}
pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<NrgImage, IsoError> {
let file_size = reader.seek(SeekFrom::End(0))?;
let (version, trailer_offset) = read_footer(reader, file_size)?;
if trailer_offset >= file_size {
return Err(IsoError::NotAnIso);
}
let mut catalog = None;
let mut tracks = Vec::new();
let mut pos = trailer_offset;
reader.seek(SeekFrom::Start(pos))?;
let footer_len = if version == NrgVersion::V2 { 12 } else { 8 };
while pos + 8 <= file_size - footer_len {
let mut head = [0u8; 8];
if reader.read_exact(&mut head).is_err() {
break;
}
let id = [head[0], head[1], head[2], head[3]];
let size = u32::from_be_bytes([head[4], head[5], head[6], head[7]]) as usize;
pos += 8;
if &id == b"END!" {
break;
}
if size > MAX_CHUNK || pos + size as u64 > file_size {
break;
}
let mut data = vec![0u8; size];
reader.read_exact(&mut data)?;
pos += size as u64;
match &id {
b"DAOX" => parse_dao(&data, true, &mut catalog, &mut tracks),
b"DAOI" => parse_dao(&data, false, &mut catalog, &mut tracks),
b"ETN2" => parse_etn(&data, true, &mut tracks),
b"ETNF" => parse_etn(&data, false, &mut tracks),
_ => {} }
}
Ok(NrgImage { version, catalog, tracks })
}
fn read_footer<R: Read + Seek>(
reader: &mut R,
file_size: u64,
) -> Result<(NrgVersion, u64), IsoError> {
if file_size >= 12 {
reader.seek(SeekFrom::End(-12))?;
let mut buf = [0u8; 12];
reader.read_exact(&mut buf)?;
if &buf[0..4] == b"NER5" {
let off = u64::from_be_bytes(buf[4..12].try_into().unwrap());
return Ok((NrgVersion::V2, off));
}
}
if file_size >= 8 {
reader.seek(SeekFrom::End(-8))?;
let mut buf = [0u8; 8];
reader.read_exact(&mut buf)?;
if &buf[0..4] == b"NERO" {
let off = u32::from_be_bytes(buf[4..8].try_into().unwrap());
return Ok((NrgVersion::V1, u64::from(off)));
}
}
Err(IsoError::NotAnIso)
}
fn parse_dao(data: &[u8], wide: bool, catalog: &mut Option<String>, tracks: &mut Vec<NrgTrack>) {
if data.len() < 22 {
return;
}
if catalog.is_none() {
if let Some(mcn) = ascii_field(&data[0..13]) {
*catalog = Some(mcn);
}
}
let sub_len = if wide { 42 } else { 30 };
for sub in data[22..].chunks_exact(sub_len) {
let sector_size = u16::from_be_bytes([sub[12], sub[13]]);
let mode_code = sub[14];
let (start, end) = if wide {
(be64(&sub[26..34]), be64(&sub[34..42]))
} else {
(u64::from(be32(&sub[22..26])), u64::from(be32(&sub[26..30])))
};
let number = (tracks.len() + 1) as u8;
tracks.push(NrgTrack {
number,
mode_code,
start_offset: start,
size: end.saturating_sub(start),
sector_size,
start_lba: 0,
isrc: ascii_field(&sub[0..12]),
});
}
}
fn parse_etn(data: &[u8], wide: bool, tracks: &mut Vec<NrgTrack>) {
let sub_len = if wide { 32 } else { 20 };
for sub in data.chunks_exact(sub_len) {
let (offset, size, mode_code, lba) = if wide {
(be64(&sub[0..8]), be64(&sub[8..16]), sub[19], be32(&sub[20..24]))
} else {
(u64::from(be32(&sub[0..4])), u64::from(be32(&sub[4..8])), sub[11], be32(&sub[12..16]))
};
let number = (tracks.len() + 1) as u8;
tracks.push(NrgTrack {
number,
mode_code,
start_offset: offset,
size,
sector_size: sector_size_for(mode_code),
start_lba: lba,
isrc: None,
});
}
}
fn ascii_field(bytes: &[u8]) -> Option<String> {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
let s: String = bytes[..end].iter().map(|&b| b as char).collect();
let trimmed = s.trim_end_matches([' ', '\0']);
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
fn be32(b: &[u8]) -> u32 {
u32::from_be_bytes([b[0], b[1], b[2], b[3]])
}
fn be64(b: &[u8]) -> u64 {
u64::from_be_bytes(b[0..8].try_into().unwrap())
}