// IsoImage:对外入口。打开映像、按路径查目录、列目录。
//
// F1 的范围是「结构 + 目录树」:卷描述符、目录记录、路径解析、目录列举。
// 文件内容读取、Joliet、Rock Ridge、写入分别在后续功能里加。
///|
pub struct IsoImage {
data : Bytes
volume_id : String
block_size : Int32
volume_space_size : Int32
root : DirectoryRecord
/// Joliet 补充卷描述符的根目录;存在时默认使用它的名字(UCS-2)。
joliet_root : DirectoryRecord?
joliet_level : Int32
/// 映像是否带 Rock Ridge 扩展(优先于 Joliet 使用它的名字与属性)。
rock_ridge : Bool
}
///|
/// 打开一个 ISO 9660 映像(内存中的完整字节流)。
pub fn IsoImage::open(data : Bytes) -> IsoImage raise IsoError {
let descriptors = scan_volume_descriptors(data)
let mut pvd_lba = -1
for d in descriptors {
if d.0 == 1 {
pvd_lba = d.1
break
}
}
if pvd_lba < 0 {
raise BadDescriptor(0)
}
let pvd = parse_primary_volume_descriptor(data, pvd_lba)
// 声明容量必须落在实际数据范围内,避免后续按越界 extent 读取
if pvd.root.extent < 0 ||
(pvd.root.extent + 1) * pvd.logical_block_size > data.length() {
raise Truncated
}
let joliet = find_joliet_descriptor(data)
let joliet_level = match joliet {
Some(d) => d.level
None => 0
}
let rock_ridge = detect_rock_ridge(data, pvd.logical_block_size, pvd.root)
IsoImage::{
data,
volume_id: pvd.volume_id,
block_size: pvd.logical_block_size,
volume_space_size: pvd.volume_space_size,
root: pvd.root,
joliet_root: match joliet {
Some(d) => Some(d.root)
None => None
},
joliet_level,
rock_ridge,
}
}
///|
/// Rock Ridge 的 SP 条目只出现在根目录的「.」记录里,用它判断映像是否带 RR 扩展。
fn detect_rock_ridge(
data : Bytes,
block_size : Int32,
root : DirectoryRecord,
) -> Bool raise IsoError {
let reader = ByteReader::new(data)
reader.seek(root.extent * block_size)
let (first, _) = parse_directory_record_in(reader, false)
let info = parse_rock_ridge(data, block_size, first.system_use)
info.present
}
///|
pub fn IsoImage::volume_id(self : IsoImage) -> String {
self.volume_id
}
///|
pub fn IsoImage::block_size(self : IsoImage) -> Int32 {
self.block_size
}
///|
pub fn IsoImage::volume_space_size(self : IsoImage) -> Int32 {
self.volume_space_size
}
///|
pub fn IsoImage::root(self : IsoImage) -> DirectoryRecord {
self.root
}
///|
/// 是否使用 Joliet 名字(存在补充卷描述符时为真)。
pub fn IsoImage::uses_joliet(self : IsoImage) -> Bool {
self.joliet_root is Some(_)
}
///|
/// El Torito 启动信息;映像不可启动时为 None。
pub fn IsoImage::boot_info(self : IsoImage) -> BootInfo? raise IsoError {
match find_boot_catalog_lba(self.data) {
Some(lba) => Some(parse_boot_catalog(self.data, lba))
None => None
}
}
///|
/// 映像是否带 Rock Ridge 扩展。
pub fn IsoImage::has_rock_ridge(self : IsoImage) -> Bool {
self.rock_ridge
}
///|
/// Joliet 级别(1/2/3),无 Joliet 时为 0。
pub fn IsoImage::joliet_level(self : IsoImage) -> Int32 {
self.joliet_level
}
///|
/// 当前生效的根目录:有 Joliet 用 Joliet 树,否则用主卷树。
fn IsoImage::active_root(self : IsoImage) -> DirectoryRecord {
// Rock Ridge 的名字挂在主卷记录上,优先级高于 Joliet
if self.rock_ridge {
return self.root
}
match self.joliet_root {
Some(root) => root
None => self.root
}
}
///|
/// 名字匹配:先精确,再退化为 ASCII 大小写不敏感。
fn name_matches(a : String, b : String) -> Bool {
a == b || ascii_eq_ignore_case(a, b)
}
///|
/// ASCII 大小写不敏感比较:ISO 9660 的文件名一律大写,但用户输入通常是混合大小写。
fn ascii_eq_ignore_case(a : String, b : String) -> Bool {
if a.length() != b.length() {
return false
}
let av = a.to_array()
let bv = b.to_array()
for i = 0; i < av.length(); i = i + 1 {
let mut x = av[i].to_int()
let mut y = bv[i].to_int()
if x >= 97 && x <= 122 {
x = x - 32
}
if y >= 97 && y <= 122 {
y = y - 32
}
if x != y {
return false
}
}
true
}
///|
/// 读取一条目录记录所指向的目录数据,返回其中的全部记录(含 . 与 ..)。
fn IsoImage::read_directory(
self : IsoImage,
dir : DirectoryRecord,
) -> Array[DirectoryRecord] raise IsoError {
self.read_directory_in(
dir,
self.uses_joliet() && !self.rock_ridge,
self.rock_ridge,
)
}
///|
/// 读取目录数据;`joliet` 为真时按 UCS-2 解码名字。
fn IsoImage::read_directory_in(
self : IsoImage,
dir : DirectoryRecord,
joliet : Bool,
rr : Bool,
) -> Array[DirectoryRecord] raise IsoError {
if !dir.is_dir() {
raise NotDirectory(dir.name)
}
let start = dir.extent * self.block_size
let end = start + dir.size
if start < 0 || end > self.data.length() {
raise Truncated
}
let reader = ByteReader::new(self.data)
reader.seek(start)
let out : Array[DirectoryRecord] = []
while reader.position() < end {
let (parsed, len) = parse_directory_record_in(reader, joliet)
let record = parsed
if len > 0 && rr {
// Rock Ridge:替代名字、POSIX 属性、符号链接
let info = parse_rock_ridge(self.data, self.block_size, record.system_use)
match info.name {
Some(alt) => record.name = alt
None => ()
}
record.posix_mode = info.mode
record.symlink_target = info.symlink
}
if len == 0 {
// 填充:跳到下一个扇区边界
let next = (reader.position() / SECTOR_SIZE + 1) * SECTOR_SIZE
reader.seek(if next > end { end } else { next })
} else {
out.push(record)
}
}
out
}
///|
/// 列出目录内容(默认过滤掉 . 与 ..),保持映像里的物理顺序。
pub fn IsoImage::list_dir(
self : IsoImage,
path : String,
) -> Array[DirectoryRecord] raise IsoError {
let dir = self.find_record(path)
let records = self.read_directory(dir)
let out : Array[DirectoryRecord] = []
let mut i = 0
while i < records.length() {
let r = records[i]
if r.is_special() {
i += 1
continue
}
if r.is_multi_extent() {
// 多段文件在列举时合并成一条,大小取各段之和
let mut total = r.size
let mut j = i + 1
while j < records.length() {
let next = records[j]
total = total + next.size
let more = next.is_multi_extent()
j += 1
if !more {
break
}
}
out.push(DirectoryRecord::{
extent: r.extent,
size: total,
flags: r.flags & 0x7F,
name: r.name,
date: r.date,
system_use: r.system_use,
posix_mode: r.posix_mode,
symlink_target: r.symlink_target,
})
i = j
} else {
out.push(r)
i += 1
}
}
out
}
///|
/// 按路径查找「组成同一实体的连续记录」。
///
/// ISO 9660 的多段文件(flags bit 7 = 1 表示后面还有一段)用同名连续记录描述,
/// 目录永远只有一条记录。
pub fn IsoImage::find_records(
self : IsoImage,
path : String,
) -> Array[DirectoryRecord] raise IsoError {
let mut current = self.active_root()
let segments : Array[String] = []
for segment in path.split("/") {
let seg = segment.to_owned()
if seg != "" {
segments.push(seg)
}
}
if segments.length() == 0 {
return [current]
}
for i = 0; i < segments.length(); i = i + 1 {
let seg = segments[i]
if !current.is_dir() {
raise NotDirectory(current.name)
}
let records = self.read_directory(current)
let matched : Array[DirectoryRecord] = []
let mut started = false
for r in records {
if r.is_special() {
continue
}
if name_matches(r.name, seg) {
matched.push(r)
started = true
} else if started {
break
}
}
if matched.length() == 0 {
raise NotFound(path)
}
let is_last = i == segments.length() - 1
if is_last {
return matched
}
if matched.length() > 1 {
raise NotDirectory(seg)
}
current = matched[0]
}
[current]
}
///|
/// 按路径查找一条记录(多段文件返回第一段)。
pub fn IsoImage::find_record(
self : IsoImage,
path : String,
) -> DirectoryRecord raise IsoError {
self.find_records(path)[0]
}
///|
/// 读取文件内容:多段文件按记录顺序拼接,目录以 IsDirectory 失败。
/// 全程做边界检查,越界一律 Truncated,不会读到映像之外。
pub fn IsoImage::read_file(
self : IsoImage,
path : String,
) -> Bytes raise IsoError {
let records = self.find_records(path)
if records[0].is_dir() {
raise IsDirectory(path)
}
if records[0].is_symlink() {
raise IsSymlink(path)
}
let mut total = 0
for r in records {
total = total + r.size
}
let out = FixedArray::make(total, b'\x00')
let mut written = 0
for r in records {
let start = r.extent * self.block_size
if r.size < 0 || start < 0 || start + r.size > self.data.length() {
raise Truncated
}
for i = 0; i < r.size; i = i + 1 {
out[written + i] = self.data[start + i]
}
written = written + r.size
}
Bytes::from_array(out[:])
}
///|
/// 遍历结果的一项。
pub struct WalkEntry {
path : String
is_dir : Bool
is_symlink : Bool
size : Int32
}
///|
/// 递归遍历目录树(路径形如 /Docs/note.md,按物理顺序)。
pub fn IsoImage::walk(
self : IsoImage,
path : String,
) -> Array[WalkEntry] raise IsoError {
let out : Array[WalkEntry] = []
self.walk_into(path, "", out)
out
}
///|
fn IsoImage::walk_into(
self : IsoImage,
path : String,
prefix : String,
out : Array[WalkEntry],
) -> Unit raise IsoError {
for entry in self.list_dir(path) {
let child = prefix + "/" + entry.name
out.push(WalkEntry::{
path: child,
is_dir: entry.is_dir(),
is_symlink: entry.is_symlink(),
size: entry.size,
})
if entry.is_dir() {
self.walk_into(child, child, out)
}
}
}
///|
/// 结构校验结果。
pub struct ValidationReport {
directories : Int32
files : Int32
symlinks : Int32
total_bytes : Int32
problems : Array[String]
}
///|
/// 遍历整棵目录树做结构校验:记录能否解析、extent 是否越界、文件能否读完。
/// 返回全部问题而不是遇错即停,方便一次性给出体检结论。
pub fn IsoImage::validate(self : IsoImage) -> ValidationReport {
let problems : Array[String] = []
let mut directories = 0
let mut files = 0
let mut symlinks = 0
let mut total_bytes = 0
let stack : Array[String] = ["/"]
while stack.length() > 0 {
let dir = stack.pop().unwrap()
directories = directories + 1
let entries = self.list_dir(dir) catch {
_ => {
problems.push("无法列出目录: " + dir)
continue
}
}
for entry in entries {
let child = if dir == "/" {
"/" + entry.name
} else {
dir + "/" + entry.name
}
if entry.is_dir() {
stack.push(child)
} else if entry.is_symlink() {
// 符号链接没有自身内容,只计数
symlinks = symlinks + 1
} else {
files = files + 1
total_bytes = total_bytes + entry.size
let bytes = Some(self.read_file(child)) catch { _ => None }
match bytes {
Some(data) =>
if data.length() != entry.size {
problems.push("读取长度不符: " + child)
}
None => problems.push("读取失败: " + child)
}
}
}
}
ValidationReport::{ directories, files, symlinks, total_bytes, problems, }
}
///|
/// 文件大小(多段文件为各段之和)。
pub fn IsoImage::file_size(
self : IsoImage,
path : String,
) -> Int32 raise IsoError {
let mut total = 0
for r in self.find_records(path) {
total = total + r.size
}
total
}