use boko::kfx::symbols::KFX_SYMBOL_TABLE;
use clap::Parser;
use ion_rs::{
AnyEncoding, Decoder, ElementReader, IonResult, MapCatalog, Reader, SharedSymbolTable,
};
use std::collections::HashMap;
use std::fs;
const ION_BVM: [u8; 4] = [0xE0, 0x01, 0x00, 0xEA];
#[derive(Parser, Debug)]
#[command(name = "kfx-dump")]
#[command(
about = "Dumps KFX/KDF/Ion files. Supports KFX container files (.kfx) and raw Ion binary files (.kdf, .ion)"
)]
struct Args {
file: String,
#[arg(short, long)]
resolve: bool,
#[arg(short, long)]
stat: bool,
#[arg(short = 'f', long = "field")]
field: Vec<String>,
}
#[derive(Debug, Clone)]
struct EntityInfo {
entity_type: String,
name: Option<String>,
}
fn build_symbol_table_preamble() -> Vec<u8> {
use boko::kfx::symbols::KFX_MAX_SYMBOL_ID;
build_symbol_table_preamble_with_max_id(KFX_MAX_SYMBOL_ID as i64)
}
fn build_symbol_table_preamble_with_max_id(max_id: i64) -> Vec<u8> {
use ion_rs::v1_0::Binary;
use ion_rs::{
Element, ElementWriter, IntoAnnotatedElement, WriteConfig, Writer, ion_list, ion_struct,
};
let import = ion_struct! {
"name": "YJ_symbols",
"version": 10i64,
"max_id": max_id,
};
let symbol_table: Element = ion_struct! {
"imports": ion_list![import],
}
.with_annotations(["$ion_symbol_table"]);
let buffer = Vec::new();
let mut writer = Writer::new(WriteConfig::<Binary>::new(), buffer).unwrap();
writer.write_element(&symbol_table).unwrap();
writer.close().unwrap()
}
fn main() -> IonResult<()> {
let args = Args::parse();
let data = fs::read(&args.file).expect("Failed to read file");
if !args.field.is_empty() {
if data.len() < 4 || &data[0..4] != b"CONT" {
eprintln!("Field reports require a KFX container file");
std::process::exit(1);
}
for field in &args.field {
match field.as_str() {
"anchors" => report_anchors(&data)?,
"container" => report_container(&data)?,
"features" => report_features(&data)?,
"document" => report_document(&data)?,
"metadata" => report_metadata(&data)?,
"navigation" => report_navigation(&data)?,
"reading_orders" => report_reading_orders(&data)?,
"resources" => report_resources(&data)?,
"sections" => report_sections(&data)?,
"storylines" => report_storylines(&data)?,
"locations" => report_locations(&data)?,
"positions" => report_positions(&data)?,
"content" => report_content(&data)?,
"dependencies" => report_dependencies(&data)?,
other => {
eprintln!(
"Unknown field report: {}. Supported: anchors, container, content, dependencies, document, features, locations, metadata, navigation, positions, reading_orders, resources, sections, storylines",
other
);
std::process::exit(1);
}
}
}
return Ok(());
}
if data.len() >= 4 && &data[0..4] == b"CONT" {
if args.stat {
dump_kfx_stats(&data)?;
} else {
eprintln!("Detected KFX container format");
dump_kfx_container(&data, args.resolve)?;
}
} else if data.len() >= 4 && data[0..4] == ION_BVM {
if args.stat {
eprintln!("Stats not supported for raw Ion files");
std::process::exit(1);
}
eprintln!("Detected raw Ion binary format");
dump_ion_data(&data)?;
} else {
eprintln!("Unknown file format. First 16 bytes:");
for byte in data.iter().take(16) {
eprint!("{:02X} ", byte);
}
eprintln!();
std::process::exit(1);
}
Ok(())
}
fn read_u16_le(data: &[u8], offset: usize) -> Option<u16> {
data.get(offset..offset + 2)?
.try_into()
.ok()
.map(u16::from_le_bytes)
}
fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
data.get(offset..offset + 4)?
.try_into()
.ok()
.map(u32::from_le_bytes)
}
fn read_u64_le(data: &[u8], offset: usize) -> Option<u64> {
data.get(offset..offset + 8)?
.try_into()
.ok()
.map(u64::from_le_bytes)
}
fn dump_kfx_container(data: &[u8], resolve: bool) -> IonResult<()> {
if data.len() < 18 {
eprintln!("Container too short: {} bytes", data.len());
return Ok(());
}
let Some(version) = read_u16_le(data, 4) else {
eprintln!("Failed to read container version");
return Ok(());
};
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
eprintln!("Container version: {}", version);
eprintln!("Header length: {}", header_len);
eprintln!(
"Container info: offset={}, length={}",
container_info_offset, container_info_length
);
eprintln!();
let mut extended_symbols: Vec<String> = Vec::new();
if container_info_offset + container_info_length <= data.len() {
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
eprintln!("=== Container Info ===");
if let Err(e) = dump_ion_data(container_info_data) {
eprintln!("Error parsing container info: {}", e);
}
eprintln!();
if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
eprintln!(
"Document symbols: offset={}, length={}",
doc_sym_offset, doc_sym_length
);
if doc_sym_offset + doc_sym_length <= data.len() {
let doc_sym_data = &data[doc_sym_offset..doc_sym_offset + doc_sym_length];
extended_symbols = extract_doc_symbols(doc_sym_data);
eprintln!(
"Extracted {} document-specific symbols",
extended_symbols.len()
);
eprintln!();
}
}
let index_info = parse_container_info_for_index(container_info_data);
if let Some((index_offset, index_length)) = index_info {
eprintln!(
"Index table: offset={}, length={}",
index_offset, index_length
);
let entry_size = 24;
let num_entries = index_length / entry_size;
eprintln!("Number of entities: {}", num_entries);
eprintln!();
let maps = if resolve {
build_maps(
data,
header_len,
index_offset,
num_entries,
&extended_symbols,
)
} else {
ResolutionMaps {
entity_map: HashMap::new(),
fragment_map: HashMap::new(),
}
};
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(id_idnum) = read_u32_le(data, entry_offset) else {
continue;
};
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize)
else {
continue;
};
let Some(entity_len) = read_u64_le(data, entry_offset + 16).map(|v| v as usize)
else {
continue;
};
let id_name = if (id_idnum as usize) < KFX_SYMBOL_TABLE.len() {
KFX_SYMBOL_TABLE[id_idnum as usize]
} else {
"?"
};
let type_name = if (type_idnum as usize) < KFX_SYMBOL_TABLE.len() {
KFX_SYMBOL_TABLE[type_idnum as usize]
} else {
"?"
};
eprintln!("=== Entity {} ===", i);
if let Some(info) = maps.entity_map.get(&(id_idnum as u64)) {
if let Some(name) = &info.name {
eprintln!(
" ID: ${} ({}) [{}:{}]",
id_idnum, id_name, info.entity_type, name
);
} else {
eprintln!(" ID: ${} ({}) [{}]", id_idnum, id_name, info.entity_type);
}
} else {
eprintln!(" ID: ${} ({})", id_idnum, id_name);
}
eprintln!(" Type: ${} ({})", type_idnum, type_name);
eprintln!(
" Offset: {} (absolute: {})",
entity_offset,
header_len + entity_offset
);
eprintln!(" Length: {}", entity_len);
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len <= data.len() {
let entity_data = &data[abs_offset..abs_offset + entity_len];
dump_entity(entity_data, &extended_symbols, &maps, resolve)?;
}
eprintln!();
}
}
}
Ok(())
}
fn dump_kfx_stats(data: &[u8]) -> IonResult<()> {
if data.len() < 18 {
eprintln!("Container too short: {} bytes", data.len());
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table in container info");
return Ok(());
};
let entry_size = 24;
let num_entries = index_length / entry_size;
let mut type_counts: HashMap<String, usize> = HashMap::new();
let mut total_size_by_type: HashMap<String, usize> = HashMap::new();
let mut singleton_data: HashMap<String, (usize, usize)> = HashMap::new();
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u64_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
let type_name = if (type_idnum as usize) < KFX_SYMBOL_TABLE.len() {
KFX_SYMBOL_TABLE[type_idnum as usize].to_string()
} else {
format!("${}", type_idnum)
};
let count = type_counts.entry(type_name.clone()).or_insert(0);
*count += 1;
*total_size_by_type.entry(type_name.clone()).or_insert(0) += entity_len;
if *count == 1 {
singleton_data.insert(type_name, (header_len + entity_offset, entity_len));
} else {
singleton_data.remove(&type_name.clone());
}
}
let mut singleton_details: HashMap<String, String> = HashMap::new();
for (type_name, (abs_offset, entity_len)) in &singleton_data {
if abs_offset + entity_len <= data.len() {
let entity_data = &data[*abs_offset..*abs_offset + *entity_len];
if let Some(detail) = extract_singleton_details(entity_data, type_name) {
singleton_details.insert(type_name.clone(), detail);
}
}
}
let mut sorted: Vec<_> = type_counts.iter().collect();
sorted.sort_by(|a, b| b.1.cmp(a.1).then_with(|| a.0.cmp(b.0)));
let total_entities: usize = type_counts.values().sum();
let total_size: usize = total_size_by_type.values().sum();
println!("{:<25} {:>8} {:>12} Details", "Type", "Count", "Size");
println!("{}", "-".repeat(70));
for (type_name, count) in sorted {
let size = total_size_by_type.get(type_name).unwrap_or(&0);
let details = singleton_details
.get(type_name)
.map(|s| s.as_str())
.unwrap_or("");
println!(
"{:<25} {:>8} {:>12} {}",
type_name,
count,
format_size(*size),
details
);
}
println!("{}", "-".repeat(70));
println!(
"{:<25} {:>8} {:>12}",
"TOTAL",
total_entities,
format_size(total_size)
);
println!();
println!("Container size: {}", format_size(data.len()));
Ok(())
}
fn extract_singleton_details(entity_data: &[u8], type_name: &str) -> Option<String> {
use boko::kfx::ion::{IonParser, IonValue};
use boko::kfx::symbols::KfxSymbol;
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
return None;
}
let entity_header_len = read_u32_le(entity_data, 6)? as usize;
if entity_header_len >= entity_data.len() {
return None;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
let value = parser.parse().ok()?;
let inner = match &value {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => &value,
};
match type_name {
"location_map" => {
if let IonValue::List(items) = inner
&& let Some(IonValue::Struct(fields)) = items.first()
{
for (fid, fval) in fields {
if *fid == KfxSymbol::Locations as u64
&& let IonValue::List(locations) = fval
{
return Some(format!("{} locations", locations.len()));
}
}
}
}
"position_id_map" => {
if let IonValue::List(items) = inner {
return Some(format!("{} entries", items.len()));
}
}
"position_map" => {
if let IonValue::List(items) = inner {
let mut total_contains = 0;
for item in items {
if let IonValue::Struct(fields) = item {
for (fid, fval) in fields {
if *fid == KfxSymbol::Contains as u64
&& let IonValue::List(contains) = fval
{
total_contains += contains.len();
}
}
}
}
return Some(format!("{} sections, {} refs", items.len(), total_contains));
}
}
"book_navigation" => {
let nav_struct = match inner {
IonValue::List(items) if !items.is_empty() => match &items[0] {
IonValue::Annotated(_, inner) => inner.as_ref(),
other => other,
},
_ => inner,
};
if let IonValue::Struct(fields) = nav_struct {
for (fid, fval) in fields {
if *fid == 392
&& let IonValue::List(containers) = fval
{
let mut details = Vec::new();
for container in containers {
let container_inner = match container {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => container,
};
if let IonValue::Struct(cfields) = container_inner {
let mut nav_type = None;
let mut entry_count = 0;
for (cfid, cfval) in cfields {
if *cfid == KfxSymbol::NavType as u64
&& let IonValue::Symbol(sym) = cfval
{
nav_type = Some(match *sym as u32 {
s if s == KfxSymbol::Toc as u32 => "toc",
s if s == KfxSymbol::Landmarks as u32 => "landmarks",
s if s == KfxSymbol::PageList as u32 => "pagelist",
s if s == KfxSymbol::Headings as u32 => "headings",
_ => "other",
});
}
if *cfid == KfxSymbol::Entries as u64
&& let IonValue::List(entries) = cfval
{
entry_count = count_nav_entries(entries);
}
}
if let Some(nt) = nav_type {
details.push(format!("{}:{}", nt, entry_count));
}
}
}
return Some(details.join(", "));
}
}
}
}
"container_entity_map" => {
if let IonValue::Struct(fields) = inner {
for (fid, fval) in fields {
if *fid == KfxSymbol::ContainerList as u64
&& let IonValue::List(containers) = fval
{
let mut total_entities = 0;
for container in containers {
if let IonValue::Struct(cfields) = container {
for (cfid, cfval) in cfields {
if *cfid == KfxSymbol::Contains as u64
&& let IonValue::List(names) = cfval
{
total_entities += names.len();
}
}
}
}
return Some(format!("{} entity refs", total_entities));
}
}
}
}
_ => {}
}
None
}
fn count_nav_entries(entries: &[boko::kfx::ion::IonValue]) -> usize {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
let mut count = entries.len();
for entry in entries {
if let IonValue::Struct(fields) = entry {
for (fid, fval) in fields {
if *fid == KfxSymbol::Entries as u64
&& let IonValue::List(children) = fval
{
count += count_nav_entries(children);
}
}
}
}
count
}
fn format_size(bytes: usize) -> String {
if bytes >= 1024 * 1024 {
format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
} else if bytes >= 1024 {
format!("{:.1} KB", bytes as f64 / 1024.0)
} else {
format!("{} B", bytes)
}
}
#[derive(Debug)]
struct AnchorInfo {
name: String,
source_text: Option<String>, destination: AnchorDestination,
}
#[derive(Debug)]
enum AnchorDestination {
Internal {
id: i64,
offset: Option<i64>,
text: Option<String>,
},
External {
uri: String,
},
Target, }
#[derive(Debug, Clone)]
struct LinkToRef {
anchor_name: String,
content_name: String, content_index: i64, offset: i64,
length: i64,
}
fn report_anchors(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len();
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let entry_size = 24;
let num_entries = index_length / entry_size;
let mut content_map: HashMap<String, Vec<String>> = HashMap::new();
let mut content_by_id: HashMap<i64, String> = HashMap::new();
let mut fragment_content_map: HashMap<i64, (String, i64)> = HashMap::new();
let mut link_to_refs: Vec<LinkToRef> = Vec::new();
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(id_idnum) = read_u32_le(data, entry_offset) else {
continue;
};
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u64_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
let value = match parser.parse() {
Ok(v) => v,
Err(_) => continue,
};
if type_idnum == KfxSymbol::Content as u32
&& let Some((name, texts)) =
extract_content_texts(&value, &extended_symbols, base_symbol_count)
{
let full_text = texts.join("");
content_by_id.insert(id_idnum as i64, full_text);
content_map.insert(name, texts);
}
if type_idnum == KfxSymbol::Storyline as u32 {
extract_link_to_refs(
&value,
&extended_symbols,
base_symbol_count,
&mut link_to_refs,
);
let mut _unused_types = HashMap::new();
extract_fragment_content_refs(
&value,
&extended_symbols,
base_symbol_count,
&mut fragment_content_map,
&mut _unused_types,
);
}
}
let mut anchor_source_text: HashMap<String, String> = HashMap::new();
for link_ref in &link_to_refs {
if let Some(content_texts) = content_map.get(&link_ref.content_name)
&& let Some(content_text) = content_texts.get(link_ref.content_index as usize)
{
let start = link_ref.offset as usize;
let text: String = content_text
.chars()
.skip(start)
.take(link_ref.length as usize)
.collect();
anchor_source_text.insert(link_ref.anchor_name.clone(), text);
}
}
let mut anchors: Vec<AnchorInfo> = Vec::new();
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u64_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
if type_idnum != KfxSymbol::Anchor as u32 {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
let value = match parser.parse() {
Ok(v) => v,
Err(_) => continue,
};
if let Some(anchor) = extract_anchor_info(
&value,
&extended_symbols,
base_symbol_count,
&content_map,
&fragment_content_map,
&anchor_source_text,
) {
anchors.push(anchor);
}
}
println!("=== Anchors ({} total) ===\n", anchors.len());
for anchor in &anchors {
let source = anchor.source_text.as_deref().unwrap_or("-");
match &anchor.destination {
AnchorDestination::Internal { id, offset, text } => {
let position = match offset {
Some(off) => format!("{}:{}", id, off),
None => format!("{}", id),
};
if let Some(dest_text) = text {
let dest_preview: String = dest_text.chars().take(40).collect();
let ellipsis = if dest_text.len() > 40 { "..." } else { "" };
println!(
"{:<30} {:>10} → {} \"{}{}\"",
anchor.name,
format!("\"{}\"", source),
position,
dest_preview,
ellipsis
);
} else {
println!(
"{:<30} {:>10} → {}",
anchor.name,
format!("\"{}\"", source),
position
);
}
}
AnchorDestination::External { uri } => {
println!(
"{:<30} {:>10} → {}",
anchor.name,
format!("\"{}\"", source),
uri
);
}
AnchorDestination::Target => {
println!(
"{:<30} {:>10} (target)",
anchor.name,
format!("\"{}\"", source)
);
}
}
}
Ok(())
}
#[derive(Debug)]
struct NavEntryInfo {
label: String,
landmark_type: Option<String>, target_id: Option<i64>,
target_offset: Option<i64>,
target_text: Option<String>, target_type: Option<String>, depth: usize,
}
fn report_navigation(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len();
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let entry_size = 24;
let num_entries = index_length / entry_size;
let mut content_map: HashMap<String, Vec<String>> = HashMap::new();
let mut fragment_content_map: HashMap<i64, (String, i64)> = HashMap::new();
let mut container_type_map: HashMap<i64, String> = HashMap::new();
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u64_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
let value = match parser.parse() {
Ok(v) => v,
Err(_) => continue,
};
if type_idnum == KfxSymbol::Content as u32
&& let Some((name, texts)) =
extract_content_texts(&value, &extended_symbols, base_symbol_count)
{
content_map.insert(name, texts);
}
if type_idnum == KfxSymbol::Storyline as u32 {
extract_fragment_content_refs(
&value,
&extended_symbols,
base_symbol_count,
&mut fragment_content_map,
&mut container_type_map,
);
}
}
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
if type_idnum != KfxSymbol::BookNavigation as u32 {
continue;
}
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u64_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
let value = match parser.parse() {
Ok(v) => v,
Err(_) => continue,
};
extract_and_print_navigation(
&value,
&extended_symbols,
base_symbol_count,
&content_map,
&fragment_content_map,
&container_type_map,
);
}
Ok(())
}
fn extract_and_print_navigation(
value: &boko::kfx::ion::IonValue,
extended_symbols: &[String],
base_symbol_count: usize,
content_map: &HashMap<String, Vec<String>>,
fragment_content_map: &HashMap<i64, (String, i64)>,
container_type_map: &HashMap<i64, String>,
) {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
let inner = match value {
IonValue::Annotated(_, inner) => inner.as_ref(),
IonValue::List(items) if !items.is_empty() => &items[0],
_ => value,
};
let inner = match inner {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => inner,
};
let fields = match inner {
IonValue::Struct(f) => f,
_ => return,
};
for (field_id, field_value) in fields {
if *field_id != KfxSymbol::NavContainers as u64 {
continue;
}
let containers = match field_value {
IonValue::List(items) => items,
_ => continue,
};
for container in containers {
let container_inner = match container {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => container,
};
let container_fields = match container_inner {
IonValue::Struct(f) => f,
_ => continue,
};
let mut nav_type = String::new();
let mut container_name = String::new();
let mut entries: Option<&Vec<IonValue>> = None;
for (cfield_id, cfield_value) in container_fields {
match *cfield_id as u32 {
id if id == KfxSymbol::NavType as u32 => {
if let IonValue::Symbol(sym_id) = cfield_value {
nav_type = resolve_symbol(*sym_id, extended_symbols, base_symbol_count);
}
}
id if id == KfxSymbol::NavContainerName as u32 => {
if let IonValue::Symbol(sym_id) = cfield_value {
container_name =
resolve_symbol(*sym_id, extended_symbols, base_symbol_count);
}
}
id if id == KfxSymbol::Entries as u32 => {
if let IonValue::List(items) = cfield_value {
entries = Some(items);
}
}
_ => {}
}
}
if !nav_type.is_empty() {
let header = match nav_type.as_str() {
"toc" => format!("Table of Contents ({})", container_name),
"headings" => format!("Headings ({})", container_name),
"landmarks" => format!("Landmarks ({})", container_name),
_ => format!("{} ({})", nav_type, container_name),
};
println!("=== {} ===\n", header);
if let Some(entry_list) = entries {
let mut nav_entries = Vec::new();
extract_nav_entries(
entry_list,
extended_symbols,
base_symbol_count,
content_map,
fragment_content_map,
container_type_map,
0,
&mut nav_entries,
);
for entry in &nav_entries {
let indent = " ".repeat(entry.depth);
let position = match (entry.target_id, entry.target_offset) {
(Some(id), Some(off)) if off != 0 => format!("→ {}:{}", id, off),
(Some(id), _) => format!("→ {}", id),
_ => String::new(),
};
let landmark_info = entry
.landmark_type
.as_ref()
.map(|t| format!("[{}] ", t))
.unwrap_or_default();
let type_info = if let Some(t) = &entry.target_type {
format!(" ({})", t)
} else if entry.target_text.is_some() {
String::new()
} else {
" (no content)".to_string()
};
let display_label = if entry.label == "heading-nav-unit"
|| entry.label == "cover-nav-unit"
{
entry
.landmark_type
.clone()
.unwrap_or_else(|| entry.label.clone())
} else {
entry.label.clone()
};
if let Some(text) = &entry.target_text {
let preview: String = text.chars().take(50).collect();
let ellipsis = if text.chars().count() > 50 { "..." } else { "" };
println!(
"{}{}{:<35} {:>12}{} \"{}{}\"",
indent,
landmark_info,
display_label,
position,
type_info,
preview,
ellipsis
);
} else {
println!(
"{}{}{:<35} {:>12}{}",
indent, landmark_info, display_label, position, type_info
);
}
}
println!("\nTotal entries: {}", nav_entries.len());
}
println!();
}
}
}
}
#[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)]
fn extract_nav_entries(
entries: &[boko::kfx::ion::IonValue],
extended_symbols: &[String],
base_symbol_count: usize,
content_map: &HashMap<String, Vec<String>>,
fragment_content_map: &HashMap<i64, (String, i64)>,
container_type_map: &HashMap<i64, String>,
depth: usize,
result: &mut Vec<NavEntryInfo>,
) {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
for entry in entries {
let entry_inner = match entry {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => entry,
};
let fields = match entry_inner {
IonValue::Struct(f) => f,
_ => continue,
};
let mut label = String::new();
let mut landmark_type: Option<String> = None;
let mut target_id: Option<i64> = None;
let mut target_offset: Option<i64> = None;
let mut children: Option<&Vec<IonValue>> = None;
for (field_id, field_value) in fields {
match *field_id as u32 {
id if id == KfxSymbol::LandmarkType as u32 => {
if let IonValue::Symbol(sym_id) = field_value {
landmark_type =
Some(resolve_symbol(*sym_id, extended_symbols, base_symbol_count));
}
}
id if id == KfxSymbol::Representation as u32 => {
if let IonValue::Struct(rep_fields) = field_value {
for (rep_field_id, rep_field_value) in rep_fields {
if *rep_field_id as u32 == KfxSymbol::Label as u32
&& let IonValue::String(s) = rep_field_value
{
label = s.clone();
}
}
}
}
id if id == KfxSymbol::TargetPosition as u32 => {
if let IonValue::Struct(pos_fields) = field_value {
for (pos_field_id, pos_field_value) in pos_fields {
match *pos_field_id as u32 {
pid if pid == KfxSymbol::Id as u32 => {
if let IonValue::Int(i) = pos_field_value {
target_id = Some(*i);
}
}
pid if pid == KfxSymbol::Offset as u32 => {
if let IonValue::Int(i) = pos_field_value {
target_offset = Some(*i);
}
}
_ => {}
}
}
}
}
id if id == KfxSymbol::Entries as u32 => {
if let IonValue::List(items) = field_value {
children = Some(items);
}
}
_ => {}
}
}
let target_text = target_id.and_then(|id| {
fragment_content_map
.get(&id)
.and_then(|(content_name, content_index)| {
content_map.get(content_name).and_then(|texts| {
texts
.get(*content_index as usize)
.map(|text| {
let start = target_offset.unwrap_or(0) as usize;
text.chars().skip(start).take(60).collect::<String>()
})
.filter(|s| !s.is_empty())
})
})
});
let target_type = target_id.and_then(|id| container_type_map.get(&id).cloned());
if !label.is_empty() || target_id.is_some() || landmark_type.is_some() {
result.push(NavEntryInfo {
label: if label.is_empty() {
"(untitled)".to_string()
} else {
label
},
landmark_type,
target_id,
target_offset,
target_text,
target_type,
depth,
});
}
if let Some(child_entries) = children {
extract_nav_entries(
child_entries,
extended_symbols,
base_symbol_count,
content_map,
fragment_content_map,
container_type_map,
depth + 1,
result,
);
}
}
}
fn resolve_symbol(sym_id: u64, extended_symbols: &[String], base_symbol_count: usize) -> String {
let idx = sym_id as usize;
if idx < base_symbol_count {
KFX_SYMBOL_TABLE
.get(idx)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("${}", sym_id))
} else {
let ext_idx = idx - base_symbol_count;
extended_symbols
.get(ext_idx)
.cloned()
.unwrap_or_else(|| format!("${}", sym_id))
}
}
fn extract_content_texts(
value: &boko::kfx::ion::IonValue,
extended_symbols: &[String],
base_symbol_count: usize,
) -> Option<(String, Vec<String>)> {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
let inner = match value {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => value,
};
let fields = match inner {
IonValue::Struct(f) => f,
_ => return None,
};
let mut name: Option<String> = None;
let mut texts: Vec<String> = Vec::new();
for (field_id, field_value) in fields {
if *field_id == KfxSymbol::Name as u64 {
name = ion_value_to_string(field_value, extended_symbols, base_symbol_count);
}
if *field_id == KfxSymbol::ContentList as u64
&& let IonValue::List(items) = field_value
{
for item in items {
if let IonValue::String(s) = item {
texts.push(s.clone());
}
}
}
}
name.map(|n| (n, texts))
}
fn extract_fragment_content_refs(
value: &boko::kfx::ion::IonValue,
extended_symbols: &[String],
base_symbol_count: usize,
refs: &mut HashMap<i64, (String, i64)>,
container_types: &mut HashMap<i64, String>,
) {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
let inner = match value {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => value,
};
let fields = match inner {
IonValue::Struct(f) => f,
_ => return,
};
for (field_id, field_value) in fields {
if *field_id == KfxSymbol::ContentList as u64 {
extract_fragment_content_from_list(
field_value,
extended_symbols,
base_symbol_count,
refs,
container_types,
);
}
}
}
fn extract_fragment_content_from_list(
value: &boko::kfx::ion::IonValue,
extended_symbols: &[String],
base_symbol_count: usize,
refs: &mut HashMap<i64, (String, i64)>,
container_types: &mut HashMap<i64, String>,
) {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
if let IonValue::List(items) = value {
for item in items {
let inner = match item {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => item,
};
if let IonValue::Struct(fields) = inner {
let mut fragment_id: Option<i64> = None;
let mut content_name: Option<String> = None;
let mut content_index: Option<i64> = None;
let mut container_type: Option<String> = None;
for (field_id, field_value) in fields {
if *field_id == KfxSymbol::Id as u64
&& let IonValue::Int(i) = field_value
{
fragment_id = Some(*i);
}
if *field_id == KfxSymbol::Type as u64
&& let IonValue::Symbol(sym_id) = field_value
{
container_type =
Some(resolve_symbol(*sym_id, extended_symbols, base_symbol_count));
}
if *field_id == KfxSymbol::Content as u64
&& let IonValue::Struct(content_fields) = field_value
{
for (cf_id, cf_value) in content_fields {
if *cf_id == KfxSymbol::Name as u64 {
content_name = ion_value_to_string(
cf_value,
extended_symbols,
base_symbol_count,
);
}
if *cf_id == KfxSymbol::Index as u64
&& let IonValue::Int(i) = cf_value
{
content_index = Some(*i);
}
}
}
if *field_id == KfxSymbol::ContentList as u64 {
extract_fragment_content_from_list(
field_value,
extended_symbols,
base_symbol_count,
refs,
container_types,
);
}
}
if let (Some(fid), Some(ctype)) = (fragment_id, container_type) {
container_types.insert(fid, ctype);
}
if let (Some(fid), Some(cname), Some(cindex)) =
(fragment_id, content_name, content_index)
{
refs.insert(fid, (cname, cindex));
}
}
}
}
}
fn extract_link_to_refs(
value: &boko::kfx::ion::IonValue,
extended_symbols: &[String],
base_symbol_count: usize,
refs: &mut Vec<LinkToRef>,
) {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
let inner = match value {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => value,
};
let fields = match inner {
IonValue::Struct(f) => f,
_ => return,
};
for (field_id, field_value) in fields {
if *field_id == KfxSymbol::ContentList as u64 {
extract_link_to_from_content_list(
field_value,
extended_symbols,
base_symbol_count,
refs,
);
}
}
}
fn extract_link_to_from_content_list(
value: &boko::kfx::ion::IonValue,
extended_symbols: &[String],
base_symbol_count: usize,
refs: &mut Vec<LinkToRef>,
) {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
if let IonValue::List(items) = value {
for item in items {
let inner = match item {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => item,
};
if let IonValue::Struct(fields) = inner {
let mut content_name: Option<String> = None;
let mut content_index: Option<i64> = None;
let mut inline_refs: Vec<(String, i64, i64)> = Vec::new();
for (field_id, field_value) in fields {
if *field_id == KfxSymbol::Content as u64
&& let IonValue::Struct(content_fields) = field_value
{
for (cf_id, cf_value) in content_fields {
if *cf_id == KfxSymbol::Name as u64 {
content_name = ion_value_to_string(
cf_value,
extended_symbols,
base_symbol_count,
);
}
if *cf_id == KfxSymbol::Index as u64
&& let IonValue::Int(i) = cf_value
{
content_index = Some(*i);
}
}
}
if *field_id == KfxSymbol::StyleEvents as u64 {
extract_inline_link_to(
field_value,
extended_symbols,
base_symbol_count,
&mut inline_refs,
);
}
if *field_id == KfxSymbol::ContentList as u64 {
extract_link_to_from_content_list(
field_value,
extended_symbols,
base_symbol_count,
refs,
);
}
}
if let (Some(cname), Some(cindex)) = (content_name, content_index) {
for (anchor_name, offset, length) in inline_refs {
refs.push(LinkToRef {
anchor_name,
content_name: cname.clone(),
content_index: cindex,
offset,
length,
});
}
}
}
}
}
}
fn extract_inline_link_to(
value: &boko::kfx::ion::IonValue,
extended_symbols: &[String],
base_symbol_count: usize,
refs: &mut Vec<(String, i64, i64)>,
) {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
if let IonValue::List(items) = value {
for item in items {
let inner = match item {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => item,
};
if let IonValue::Struct(fields) = inner {
let mut link_to: Option<String> = None;
let mut offset: Option<i64> = None;
let mut length: Option<i64> = None;
for (field_id, field_value) in fields {
if *field_id == KfxSymbol::LinkTo as u64 {
link_to =
ion_value_to_string(field_value, extended_symbols, base_symbol_count);
}
if *field_id == KfxSymbol::Offset as u64
&& let IonValue::Int(i) = field_value
{
offset = Some(*i);
}
if *field_id == KfxSymbol::Length as u64
&& let IonValue::Int(i) = field_value
{
length = Some(*i);
}
}
if let (Some(anchor_name), Some(off), Some(len)) = (link_to, offset, length) {
refs.push((anchor_name, off, len));
}
}
}
}
}
fn extract_anchor_info(
value: &boko::kfx::ion::IonValue,
extended_symbols: &[String],
base_symbol_count: usize,
content_map: &HashMap<String, Vec<String>>,
fragment_content_map: &HashMap<i64, (String, i64)>,
anchor_source_text: &HashMap<String, String>,
) -> Option<AnchorInfo> {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
let inner = match value {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => value,
};
let fields = match inner {
IonValue::Struct(f) => f,
_ => return None,
};
let mut anchor_name: Option<String> = None;
let mut position_id: Option<i64> = None;
let mut position_offset: Option<i64> = None;
let mut uri: Option<String> = None;
for (field_id, field_value) in fields {
match *field_id as u32 {
id if id == KfxSymbol::AnchorName as u32 => {
anchor_name = ion_value_to_string(field_value, extended_symbols, base_symbol_count);
}
id if id == KfxSymbol::Position as u32 => {
if let IonValue::Struct(pos_fields) = field_value {
for (pos_field_id, pos_field_value) in pos_fields {
match *pos_field_id as u32 {
pid if pid == KfxSymbol::Id as u32 => {
if let IonValue::Int(i) = pos_field_value {
position_id = Some(*i);
}
}
pid if pid == KfxSymbol::Offset as u32 => {
if let IonValue::Int(i) = pos_field_value {
position_offset = Some(*i);
}
}
_ => {}
}
}
}
}
id if id == KfxSymbol::Uri as u32 => {
if let IonValue::String(s) = field_value {
uri = Some(s.clone());
}
}
_ => {}
}
}
let name = anchor_name.unwrap_or_else(|| "(unnamed)".to_string());
let source_text = anchor_source_text.get(&name).cloned();
let destination = if let Some(u) = uri {
AnchorDestination::External { uri: u }
} else if let Some(id) = position_id {
let text = fragment_content_map
.get(&id)
.and_then(|(content_name, content_index)| {
content_map.get(content_name).and_then(|texts| {
texts
.get(*content_index as usize)
.map(|text| {
let start = position_offset.unwrap_or(0) as usize;
let preview: String = text.chars().skip(start).take(60).collect();
preview
})
.filter(|s| !s.is_empty())
})
});
AnchorDestination::Internal {
id,
offset: position_offset,
text,
}
} else {
AnchorDestination::Target
};
Some(AnchorInfo {
name,
source_text,
destination,
})
}
fn parse_container_info_for_index(data: &[u8]) -> Option<(usize, usize)> {
let mut catalog = MapCatalog::new();
if let Ok(table) =
SharedSymbolTable::new("YJ_symbols", 10, KFX_SYMBOL_TABLE[10..].iter().copied())
{
catalog.insert_table(table);
}
let preamble = build_symbol_table_preamble();
let mut full_data = preamble;
if data.len() >= 4 && data[0..4] == ION_BVM {
full_data.extend_from_slice(&data[4..]);
} else {
full_data.extend_from_slice(data);
}
let reader = Reader::new(AnyEncoding.with_catalog(catalog), &full_data[..]);
if reader.is_err() {
return None;
}
let mut reader = reader.unwrap();
let mut index_offset: Option<usize> = None;
let mut index_length: Option<usize> = None;
for element in reader.elements() {
if let Ok(elem) = element
&& let Some(strukt) = elem.as_struct()
{
for field in strukt.iter() {
let (name, value) = field;
if let Some(field_name) = name.text() {
if field_name == "bcIndexTabOffset"
&& let Some(i) = value.as_i64()
{
index_offset = Some(i as usize);
}
if field_name == "bcIndexTabLength"
&& let Some(i) = value.as_i64()
{
index_length = Some(i as usize);
}
}
}
}
}
match (index_offset, index_length) {
(Some(off), Some(len)) => Some((off, len)),
_ => None,
}
}
fn parse_container_info_for_doc_symbols(data: &[u8]) -> Option<(usize, usize)> {
let mut catalog = MapCatalog::new();
if let Ok(table) =
SharedSymbolTable::new("YJ_symbols", 10, KFX_SYMBOL_TABLE[10..].iter().copied())
{
catalog.insert_table(table);
}
let preamble = build_symbol_table_preamble();
let mut full_data = preamble;
if data.len() >= 4 && data[0..4] == ION_BVM {
full_data.extend_from_slice(&data[4..]);
} else {
full_data.extend_from_slice(data);
}
let reader = Reader::new(AnyEncoding.with_catalog(catalog), &full_data[..]);
if reader.is_err() {
return None;
}
let mut reader = reader.unwrap();
let mut doc_sym_offset: Option<usize> = None;
let mut doc_sym_length: Option<usize> = None;
for element in reader.elements() {
if let Ok(elem) = element
&& let Some(strukt) = elem.as_struct()
{
for field in strukt.iter() {
let (name, value) = field;
if let Some(field_name) = name.text() {
if field_name == "bcDocSymbolOffset"
&& let Some(i) = value.as_i64()
{
doc_sym_offset = Some(i as usize);
}
if field_name == "bcDocSymbolLength"
&& let Some(i) = value.as_i64()
{
doc_sym_length = Some(i as usize);
}
}
}
}
}
match (doc_sym_offset, doc_sym_length) {
(Some(off), Some(len)) if len > 0 => Some((off, len)),
_ => None,
}
}
struct ResolutionMaps {
entity_map: HashMap<u64, EntityInfo>,
fragment_map: HashMap<u64, String>,
}
fn build_maps(
data: &[u8],
header_len: usize,
index_offset: usize,
num_entries: usize,
extended_symbols: &[String],
) -> ResolutionMaps {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
let mut entity_map = HashMap::new();
let mut fragment_map = HashMap::new();
let entry_size = 24;
let base_symbol_count = KFX_SYMBOL_TABLE.len();
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(id_idnum) = read_u32_le(data, entry_offset) else {
continue;
};
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u64_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
let entity_type = if (type_idnum as usize) < KFX_SYMBOL_TABLE.len() {
KFX_SYMBOL_TABLE[type_idnum as usize].to_string()
} else {
format!("${}", type_idnum)
};
let abs_offset = header_len + entity_offset;
let mut name: Option<String> = None;
if abs_offset + entity_len <= data.len() {
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() >= 10
&& &entity_data[0..4] == b"ENTY"
&& let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize)
&& entity_header_len < entity_data.len()
{
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if let Ok(value) = parser.parse() {
name = extract_name_from_ion(&value, extended_symbols, base_symbol_count);
if type_idnum == KfxSymbol::Storyline as u32
&& let Some(story_name) = &name
{
extract_fragment_ids(&value, story_name, &mut fragment_map);
}
}
}
}
entity_map.insert(id_idnum as u64, EntityInfo { entity_type, name });
}
ResolutionMaps {
entity_map,
fragment_map,
}
}
fn extract_fragment_ids(
value: &boko::kfx::ion::IonValue,
story_name: &str,
fragment_map: &mut HashMap<u64, String>,
) {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
let inner = match value {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => value,
};
if let IonValue::Struct(fields) = inner {
for (field_id, field_value) in fields {
if *field_id == KfxSymbol::ContentList as u64 {
extract_fragment_ids_from_list(field_value, story_name, fragment_map);
}
}
}
}
fn extract_fragment_ids_from_list(
value: &boko::kfx::ion::IonValue,
story_name: &str,
fragment_map: &mut HashMap<u64, String>,
) {
use boko::kfx::ion::IonValue;
if let IonValue::List(items) = value {
for item in items {
extract_id_from_content_item(item, story_name, fragment_map);
}
}
}
fn extract_id_from_content_item(
value: &boko::kfx::ion::IonValue,
story_name: &str,
fragment_map: &mut HashMap<u64, String>,
) {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
let inner = match value {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => value,
};
if let IonValue::Struct(fields) = inner {
for (field_id, field_value) in fields {
if *field_id == KfxSymbol::Id as u64
&& let IonValue::Int(id) = field_value
{
fragment_map.insert(*id as u64, story_name.to_string());
}
if *field_id == KfxSymbol::ContentList as u64 {
extract_fragment_ids_from_list(field_value, story_name, fragment_map);
}
}
}
}
fn extract_name_from_ion(
value: &boko::kfx::ion::IonValue,
extended_symbols: &[String],
base_symbol_count: usize,
) -> Option<String> {
use boko::kfx::ion::IonValue;
use boko::kfx::symbols::KfxSymbol;
let inner = match value {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => value,
};
if let IonValue::Struct(fields) = inner {
for (field_id, field_value) in fields {
let field_id = *field_id;
if field_id == KfxSymbol::Id as u64
|| field_id == KfxSymbol::SectionName as u64
|| field_id == KfxSymbol::StoryName as u64
|| field_id == KfxSymbol::AnchorName as u64
{
return ion_value_to_string(field_value, extended_symbols, base_symbol_count);
}
}
}
None
}
fn ion_value_to_string(
value: &boko::kfx::ion::IonValue,
extended_symbols: &[String],
base_symbol_count: usize,
) -> Option<String> {
use boko::kfx::ion::IonValue;
match value {
IonValue::String(s) => Some(s.clone()),
IonValue::Symbol(id) => {
let id = *id as usize;
if id < KFX_SYMBOL_TABLE.len() {
Some(KFX_SYMBOL_TABLE[id].to_string())
} else if id >= base_symbol_count && id - base_symbol_count < extended_symbols.len() {
Some(extended_symbols[id - base_symbol_count].clone())
} else {
Some(format!("${}", id))
}
}
IonValue::Int(i) => Some(i.to_string()),
_ => None,
}
}
fn extract_doc_symbols(data: &[u8]) -> Vec<String> {
use boko::kfx::ion::{IonParser, IonValue};
let mut parser = IonParser::new(data);
let value = match parser.parse() {
Ok(v) => v,
Err(e) => {
eprintln!("DEBUG: Failed to parse Ion: {}", e);
return Vec::new();
}
};
let inner = match &value {
IonValue::Annotated(_, inner) => inner.as_ref(),
_ => &value,
};
if let IonValue::Struct(fields) = inner {
for (field_id, field_value) in fields {
if *field_id == 7
&& let IonValue::List(items) = field_value
{
return items
.iter()
.filter_map(|item| {
if let IonValue::String(s) = item {
Some(s.clone())
} else {
None
}
})
.collect();
}
}
}
Vec::new()
}
fn dump_entity(
data: &[u8],
extended_symbols: &[String],
maps: &ResolutionMaps,
resolve: bool,
) -> IonResult<()> {
if data.len() < 10 {
eprintln!(" Entity too short");
return Ok(());
}
if &data[0..4] != b"ENTY" {
eprintln!(" Not an ENTY (found: {:?})", &data[0..4]);
if data[0..4] == ION_BVM {
eprintln!(" Raw Ion data:");
return dump_ion_data_extended(data, extended_symbols, maps, resolve);
}
return Ok(());
}
let Some(version) = read_u16_le(data, 4) else {
eprintln!(" Failed to read ENTY version");
return Ok(());
};
let Some(entity_header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!(" Failed to read ENTY header length");
return Ok(());
};
eprintln!(" ENTY version: {}", version);
eprintln!(" ENTY header length: {}", entity_header_len);
if entity_header_len < data.len() {
let ion_data = &data[entity_header_len..];
eprintln!(" Ion data ({} bytes):", ion_data.len());
dump_ion_data_extended(ion_data, extended_symbols, maps, resolve)?;
}
Ok(())
}
fn dump_ion_data(data: &[u8]) -> IonResult<()> {
let empty_maps = ResolutionMaps {
entity_map: HashMap::new(),
fragment_map: HashMap::new(),
};
dump_ion_data_extended(data, &[], &empty_maps, false)
}
fn dump_ion_data_extended(
data: &[u8],
extended_symbols: &[String],
maps: &ResolutionMaps,
resolve: bool,
) -> IonResult<()> {
let mut all_symbols: Vec<&str> = KFX_SYMBOL_TABLE[10..].to_vec();
for sym in extended_symbols {
all_symbols.push(sym.as_str());
}
use boko::kfx::symbols::KFX_MAX_SYMBOL_ID;
let max_id = (KFX_MAX_SYMBOL_ID + extended_symbols.len()) as i64;
let mut catalog = MapCatalog::new();
catalog.insert_table(SharedSymbolTable::new(
"YJ_symbols",
10,
all_symbols.iter().copied(),
)?);
let preamble = build_symbol_table_preamble_with_max_id(max_id);
let mut full_data = preamble;
if data.len() >= 4 && data[0..4] == ION_BVM {
full_data.extend_from_slice(&data[4..]);
} else {
full_data.extend_from_slice(data);
}
let mut reader = Reader::new(AnyEncoding.with_catalog(catalog), &full_data[..])?;
let mut count = 0;
for element in reader.elements() {
match element {
Ok(elem) => {
let text = element_to_ion_text(&elem, &all_symbols, maps, resolve);
println!("{}", text);
count += 1;
}
Err(e) => {
if count == 0 {
eprintln!(" Error reading first element: {}", e);
}
break;
}
}
}
if count > 0 {
eprintln!(" ({} top-level elements)", count);
}
Ok(())
}
fn element_to_ion_text(
elem: &ion_rs::Element,
_symbols: &[&str], maps: &ResolutionMaps,
resolve: bool,
) -> String {
element_to_ion_text_inner(elem, maps, resolve, 0, None)
}
fn is_int_entity_ref_field(field_name: &str) -> bool {
matches!(field_name, "reading_order_start" | "reading_order_end")
}
#[derive(Clone, Copy)]
struct FieldContext<'a> {
field_name: Option<&'a str>,
in_target_position: bool,
}
impl<'a> FieldContext<'a> {
fn new() -> Self {
Self {
field_name: None,
in_target_position: false,
}
}
fn with_field(self, name: Option<&'a str>) -> Self {
Self {
field_name: name,
in_target_position: self.in_target_position || name == Some("target_position"),
}
}
}
fn element_to_ion_text_inner(
elem: &ion_rs::Element,
maps: &ResolutionMaps,
resolve: bool,
indent: usize,
ctx: Option<FieldContext<'_>>,
) -> String {
use ion_rs::IonType;
let ctx = ctx.unwrap_or_else(FieldContext::new);
let indent_str = " ".repeat(indent);
let mut result = String::new();
let annotations: Vec<_> = elem.annotations().into_iter().collect();
for ann in &annotations {
if let Some(text) = ann.text() {
result.push_str(text);
} else {
result.push_str("$0"); }
result.push_str("::");
}
match elem.ion_type() {
IonType::Null => result.push_str("null"),
IonType::Bool => {
if let Some(b) = elem.as_bool() {
result.push_str(if b { "true" } else { "false" });
} else {
result.push_str("null.bool");
}
}
IonType::Int => {
if let Some(i) = elem.as_i64() {
result.push_str(&i.to_string());
if resolve {
let field = ctx.field_name.unwrap_or("");
if field == "id" && ctx.in_target_position {
if let Some(story_name) = maps.fragment_map.get(&(i as u64)) {
result.push_str(&format!(" /* {} */", story_name));
}
}
else if is_int_entity_ref_field(field)
&& let Some(info) = maps.entity_map.get(&(i as u64))
{
if let Some(name) = &info.name {
result.push_str(&format!(" /* {}:{} */", info.entity_type, name));
} else {
result.push_str(&format!(" /* {} */", info.entity_type));
}
}
}
} else if let Some(i) = elem.as_int() {
result.push_str(&format!("{}", i));
} else {
result.push_str("null.int");
}
}
IonType::Float => {
if let Some(f) = elem.as_float() {
result.push_str(&format!("{}", f));
} else {
result.push_str("null.float");
}
}
IonType::Decimal => {
if let Some(d) = elem.as_decimal() {
result.push_str(&format!("{}", d));
} else {
result.push_str("null.decimal");
}
}
IonType::Timestamp => {
if let Some(t) = elem.as_timestamp() {
result.push_str(&format!("{}", t));
} else {
result.push_str("null.timestamp");
}
}
IonType::Symbol => {
if let Some(sym) = elem.as_symbol() {
if let Some(text) = sym.text() {
if text.chars().all(|c| c.is_alphanumeric() || c == '_') && !text.is_empty() {
result.push_str(text);
} else {
result.push('\'');
result.push_str(&text.replace('\'', "\\'"));
result.push('\'');
}
} else {
result.push_str("$0"); }
} else {
result.push_str("null.symbol");
}
}
IonType::String => {
if let Some(s) = elem.as_string() {
result.push('"');
result.push_str(
&s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n"),
);
result.push('"');
} else {
result.push_str("null.string");
}
}
IonType::Clob => {
result.push_str("{{/* clob */}}");
}
IonType::Blob => {
if let Some(blob) = elem.as_blob() {
use base64::Engine;
let b64 = base64::engine::general_purpose::STANDARD.encode(blob);
if b64.len() > 60 {
result.push_str(&format!("{{{{{}...}}}}", &b64[..60]));
} else {
result.push_str(&format!("{{{{{}}}}}", b64));
}
} else {
result.push_str("null.blob");
}
}
IonType::List => {
if let Some(list) = elem.as_list() {
let items: Vec<_> = list.iter().collect();
if items.is_empty() {
result.push_str("[]");
} else if items.len() == 1
&& !matches!(items[0].ion_type(), IonType::Struct | IonType::List)
{
result.push('[');
result.push_str(&element_to_ion_text_inner(
items[0],
maps,
resolve,
0,
Some(ctx),
));
result.push(']');
} else {
result.push_str("[\n");
let inner_indent = " ".repeat(indent + 1);
for (i, item) in items.iter().enumerate() {
result.push_str(&inner_indent);
result.push_str(&element_to_ion_text_inner(
item,
maps,
resolve,
indent + 1,
Some(ctx),
));
if i < items.len() - 1 {
result.push(',');
}
result.push('\n');
}
result.push_str(&indent_str);
result.push(']');
}
} else {
result.push_str("null.list");
}
}
IonType::SExp => {
if let Some(sexp) = elem.as_sexp() {
result.push('(');
let items: Vec<_> = sexp
.iter()
.map(|e| element_to_ion_text_inner(e, maps, resolve, 0, None))
.collect();
result.push_str(&items.join(" "));
result.push(')');
} else {
result.push_str("null.sexp");
}
}
IonType::Struct => {
if let Some(strukt) = elem.as_struct() {
let fields: Vec<_> = strukt.iter().collect();
if fields.is_empty() {
result.push_str("{}");
} else {
result.push_str("{\n");
let inner_indent = " ".repeat(indent + 1);
for (i, (name, value)) in fields.iter().enumerate() {
let field_name_str = if let Some(text) = name.text() {
if text.chars().all(|c| c.is_alphanumeric() || c == '_')
&& !text.is_empty()
{
text.to_string()
} else {
format!("'{}'", text.replace('\'', "\\'"))
}
} else {
"$0".to_string() };
result.push_str(&inner_indent);
result.push_str(&field_name_str);
result.push_str(": ");
let field_ctx = ctx.with_field(name.text());
result.push_str(&element_to_ion_text_inner(
value,
maps,
resolve,
indent + 1,
Some(field_ctx),
));
if i < fields.len() - 1 {
result.push(',');
}
result.push('\n');
}
result.push_str(&indent_str);
result.push('}');
}
} else {
result.push_str("null.struct");
}
}
}
result
}
fn report_container(data: &[u8]) -> IonResult<()> {
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(version) = read_u16_le(data, 4) else {
eprintln!("Failed to read container version");
return Ok(());
};
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
println!("=== Container Header (Binary) ===\n");
println!("magic: CONT");
println!("version: {}", version);
println!("header_len: {}", header_len);
println!("container_info_offset: {}", container_info_offset);
println!("container_info_length: {}", container_info_length);
println!();
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let mut catalog = MapCatalog::new();
if let Ok(table) =
SharedSymbolTable::new("YJ_symbols", 10, KFX_SYMBOL_TABLE[10..].iter().copied())
{
catalog.insert_table(table);
}
let preamble = build_symbol_table_preamble();
let mut full_data = preamble;
if container_info_data.len() >= 4 && container_info_data[0..4] == ION_BVM {
full_data.extend_from_slice(&container_info_data[4..]);
} else {
full_data.extend_from_slice(container_info_data);
}
let reader = Reader::new(AnyEncoding.with_catalog(catalog), &full_data[..]);
if reader.is_err() {
eprintln!("Failed to parse container info Ion");
return Ok(());
}
let mut reader = reader.unwrap();
println!("=== Container Info (Ion) ===\n");
for element in reader.elements() {
if let Ok(elem) = element
&& let Some(strukt) = elem.as_struct()
{
for field in strukt.iter() {
let (name, value) = field;
let field_name = name.text().unwrap_or("?");
let value_str = if let Some(i) = value.as_i64() {
format!("{}", i)
} else if let Some(s) = value.as_string() {
format!("\"{}\"", s)
} else if let Some(b) = value.as_blob() {
format!("<blob {} bytes>", b.len())
} else if value.is_null() {
"null".to_string()
} else {
format!("{:?}", value.ion_type())
};
println!("{:<25} {}", format!("{}:", field_name), value_str);
}
}
}
Ok(())
}
fn report_features(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len() as u64;
let entry_size = 24;
let num_entries = index_length / entry_size;
let content_features_type = KfxSymbol::ContentFeatures as u32;
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
if type_idnum != content_features_type {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if let Ok(value) = parser.parse() {
println!("=== Content Features ===\n");
let resolve_sym = |id: u64| -> &str {
if id < base_symbol_count {
KFX_SYMBOL_TABLE.get(id as usize).copied().unwrap_or("?")
} else {
let ext_idx = (id as usize) - (base_symbol_count as usize);
extended_symbols
.get(ext_idx)
.map(|s| s.as_str())
.unwrap_or("?")
}
};
if let boko::kfx::ion::IonValue::Struct(fields) = &value {
for (field_id, field_value) in fields {
let field_name = resolve_sym(*field_id);
if field_name == "features"
&& let boko::kfx::ion::IonValue::List(features) = field_value
{
for (idx, feature) in features.iter().enumerate() {
if let boko::kfx::ion::IonValue::Struct(ffields) = feature {
let mut namespace = String::new();
let mut key = String::new();
let mut major = 0i64;
let mut minor = 0i64;
for (fid, fval) in ffields {
let fname = resolve_sym(*fid);
match fname {
"namespace" => {
if let boko::kfx::ion::IonValue::String(s) = fval {
namespace = s.clone();
}
}
"key" => {
if let boko::kfx::ion::IonValue::String(s) = fval {
key = s.clone();
}
}
"version_info" => {
if let boko::kfx::ion::IonValue::Struct(vi) = fval {
for (vid, vval) in vi {
let vname = resolve_sym(*vid);
if vname == "version"
&& let boko::kfx::ion::IonValue::Struct(ver) =
vval
{
for (verid, verval) in ver {
let vername = resolve_sym(*verid);
if vername == "major_version"
&& let boko::kfx::ion::IonValue::Int(
v,
) = verval
{
major = *v;
}
if vername == "minor_version"
&& let boko::kfx::ion::IonValue::Int(
v,
) = verval
{
minor = *v;
}
}
}
}
}
}
_ => {}
}
}
println!(
"{:2}. {}.{} v{}.{}",
idx + 1,
namespace,
key,
major,
minor
);
}
}
}
}
}
}
return Ok(());
}
eprintln!("No content_features entity found");
Ok(())
}
fn report_metadata(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len() as u64;
let entry_size = 24;
let num_entries = index_length / entry_size;
let book_metadata_type = KfxSymbol::BookMetadata as u32;
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
if type_idnum != book_metadata_type {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if let Ok(value) = parser.parse() {
let resolve_sym = |id: u64| -> &str {
if id < base_symbol_count {
KFX_SYMBOL_TABLE.get(id as usize).copied().unwrap_or("?")
} else {
let ext_idx = (id as usize) - (base_symbol_count as usize);
extended_symbols
.get(ext_idx)
.map(|s| s.as_str())
.unwrap_or("?")
}
};
if let boko::kfx::ion::IonValue::Struct(fields) = &value {
for (field_id, field_value) in fields {
let field_name = resolve_sym(*field_id);
if field_name == "categorised_metadata"
&& let boko::kfx::ion::IonValue::List(categories) = field_value
{
for category in categories {
if let boko::kfx::ion::IonValue::Struct(cat_fields) = category {
let mut cat_name = String::new();
let mut metadata_list = Vec::new();
for (cid, cval) in cat_fields {
let cname = resolve_sym(*cid);
match cname {
"category" => {
if let boko::kfx::ion::IonValue::String(s) = cval {
cat_name = s.clone();
}
}
"metadata" => {
if let boko::kfx::ion::IonValue::List(items) = cval {
for item in items {
if let boko::kfx::ion::IonValue::Struct(
item_fields,
) = item
{
let mut key = String::new();
let mut val = String::new();
for (iid, ival) in item_fields {
let iname = resolve_sym(*iid);
match iname {
"key" => {
if let boko::kfx::ion::IonValue::String(s) = ival {
key = s.clone();
}
}
"value" => {
val = match ival {
boko::kfx::ion::IonValue::String(s) => s.clone(),
boko::kfx::ion::IonValue::Int(i) => i.to_string(),
boko::kfx::ion::IonValue::Bool(b) => b.to_string(),
_ => format!("{:?}", ival),
};
}
_ => {}
}
}
if !key.is_empty() {
metadata_list.push((key, val));
}
}
}
}
}
_ => {}
}
}
if !cat_name.is_empty() {
println!("=== {} ===\n", cat_name);
for (key, val) in &metadata_list {
let display_val = if val.len() > 60 {
format!("{}...", &val[..60])
} else {
val.clone()
};
println!("{:<25} {}", format!("{}:", key), display_val);
}
println!();
}
}
}
}
}
}
}
return Ok(());
}
eprintln!("No book_metadata entity found");
Ok(())
}
fn report_reading_orders(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len() as u64;
let entry_size = 24;
let num_entries = index_length / entry_size;
let metadata_type = KfxSymbol::Metadata as u32;
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
if type_idnum != metadata_type {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if let Ok(value) = parser.parse() {
let resolve_sym = |id: u64| -> &str {
if id < base_symbol_count {
KFX_SYMBOL_TABLE.get(id as usize).copied().unwrap_or("?")
} else {
let ext_idx = (id as usize) - (base_symbol_count as usize);
extended_symbols
.get(ext_idx)
.map(|s| s.as_str())
.unwrap_or("?")
}
};
println!("=== Reading Orders ===\n");
if let boko::kfx::ion::IonValue::Struct(fields) = &value {
for (field_id, field_value) in fields {
let field_name = resolve_sym(*field_id);
if field_name == "reading_orders"
&& let boko::kfx::ion::IonValue::List(orders) = field_value
{
for (idx, order) in orders.iter().enumerate() {
if let boko::kfx::ion::IonValue::Struct(order_fields) = order {
let mut order_name = String::new();
let mut sections: Vec<String> = Vec::new();
for (oid, oval) in order_fields {
let oname = resolve_sym(*oid);
match oname {
"reading_order_name" => {
if let boko::kfx::ion::IonValue::Symbol(s) = oval {
order_name = resolve_sym(*s).to_string();
} else if let boko::kfx::ion::IonValue::String(s) = oval
{
order_name = s.clone();
}
}
"sections" => {
if let boko::kfx::ion::IonValue::List(secs) = oval {
for sec in secs {
if let boko::kfx::ion::IonValue::Symbol(s) = sec
{
sections.push(resolve_sym(*s).to_string());
}
}
}
}
_ => {}
}
}
println!(
"{}. {} ({} sections)",
idx + 1,
order_name,
sections.len()
);
for (sidx, sec) in sections.iter().enumerate() {
println!(" {:3}. {}", sidx + 1, sec);
}
println!();
}
}
}
}
}
}
return Ok(());
}
eprintln!("No metadata entity found");
Ok(())
}
fn report_document(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len() as u64;
let entry_size = 24;
let num_entries = index_length / entry_size;
let document_data_type = KfxSymbol::DocumentData as u32;
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
if type_idnum != document_data_type {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if let Ok(value) = parser.parse() {
let resolve_sym = |id: u64| -> String {
if id < base_symbol_count {
KFX_SYMBOL_TABLE
.get(id as usize)
.copied()
.unwrap_or("?")
.to_string()
} else {
let ext_idx = (id as usize) - (base_symbol_count as usize);
extended_symbols
.get(ext_idx)
.cloned()
.unwrap_or_else(|| "?".to_string())
}
};
println!("=== Document Data ===\n");
if let boko::kfx::ion::IonValue::Struct(fields) = &value {
for (field_id, field_value) in fields {
let field_name = resolve_sym(*field_id);
if field_name == "reading_orders" {
continue;
}
let value_str = format_ion_value_simple(field_value, &resolve_sym);
println!("{:<25} {}", format!("{}:", field_name), value_str);
}
}
}
return Ok(());
}
eprintln!("No document_data entity found");
Ok(())
}
fn format_ion_value_simple<F>(value: &boko::kfx::ion::IonValue, resolve_sym: &F) -> String
where
F: Fn(u64) -> String,
{
use boko::kfx::ion::IonValue;
match value {
IonValue::Null => "null".to_string(),
IonValue::Bool(b) => b.to_string(),
IonValue::Int(i) => i.to_string(),
IonValue::Float(f) => format!("{}", f),
IonValue::Decimal(d) => d.clone(),
IonValue::String(s) => format!("\"{}\"", s),
IonValue::Symbol(s) => resolve_sym(*s).to_string(),
IonValue::Blob(b) => format!("<blob {} bytes>", b.len()),
IonValue::List(items) => {
let parts: Vec<String> = items
.iter()
.take(5)
.map(|v| format_ion_value_simple(v, resolve_sym))
.collect();
if items.len() > 5 {
format!("[{}, ... ({} more)]", parts.join(", "), items.len() - 5)
} else {
format!("[{}]", parts.join(", "))
}
}
IonValue::Struct(fields) => {
let parts: Vec<String> = fields
.iter()
.take(3)
.map(|(k, v)| {
format!(
"{}: {}",
resolve_sym(*k),
format_ion_value_simple(v, resolve_sym)
)
})
.collect();
if fields.len() > 3 {
format!("{{ {}, ... }}", parts.join(", "))
} else {
format!("{{ {} }}", parts.join(", "))
}
}
IonValue::Annotated(_, inner) => format_ion_value_simple(inner, resolve_sym),
}
}
fn report_sections(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len() as u64;
let entry_size = 24;
let num_entries = index_length / entry_size;
let section_type = KfxSymbol::Section as u32;
println!("=== Sections ===\n");
let mut section_count = 0;
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
if type_idnum != section_type {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if let Ok(value) = parser.parse() {
let resolve_sym = |id: u64| -> String {
if id < base_symbol_count {
KFX_SYMBOL_TABLE
.get(id as usize)
.copied()
.unwrap_or("?")
.to_string()
} else {
let ext_idx = (id as usize) - (base_symbol_count as usize);
extended_symbols
.get(ext_idx)
.cloned()
.unwrap_or_else(|| "?".to_string())
}
};
if let boko::kfx::ion::IonValue::Struct(fields) = &value {
let mut section_name = String::new();
let mut templates: Vec<String> = Vec::new();
for (field_id, field_value) in fields {
let field_name = resolve_sym(*field_id);
match field_name.as_str() {
"section_name" => {
if let boko::kfx::ion::IonValue::Symbol(s) = field_value {
section_name = resolve_sym(*s);
}
}
"page_templates" => {
if let boko::kfx::ion::IonValue::List(tpls) = field_value {
for tpl in tpls {
if let boko::kfx::ion::IonValue::Struct(tpl_fields) = tpl {
let mut tpl_type = String::new();
let mut story_name = String::new();
let mut dims = String::new();
for (tid, tval) in tpl_fields {
let tname = resolve_sym(*tid);
match tname.as_str() {
"type" => {
if let boko::kfx::ion::IonValue::Symbol(s) =
tval
{
tpl_type = resolve_sym(*s);
}
}
"story_name" => {
if let boko::kfx::ion::IonValue::Symbol(s) =
tval
{
story_name = resolve_sym(*s);
}
}
"fixed_width" => {
if let boko::kfx::ion::IonValue::Int(w) = tval {
dims = format!("{}x", w);
}
}
"fixed_height" => {
if let boko::kfx::ion::IonValue::Int(h) = tval {
dims = format!("{}{}", dims, h);
}
}
_ => {}
}
}
let tpl_desc = if !dims.is_empty() {
format!("{} ({}, {})", story_name, tpl_type, dims)
} else {
format!("{} ({})", story_name, tpl_type)
};
templates.push(tpl_desc);
}
}
}
}
_ => {}
}
}
if !section_name.is_empty() {
section_count += 1;
let templates_str = if templates.is_empty() {
String::new()
} else {
format!(" → {}", templates.join(", "))
};
println!("{:<15}{}", section_name, templates_str);
}
}
}
}
println!("\nTotal sections: {}", section_count);
Ok(())
}
fn report_resources(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len() as u64;
let entry_size = 24;
let num_entries = index_length / entry_size;
let resource_type = KfxSymbol::ExternalResource as u32;
println!("=== External Resources ===\n");
let mut resource_count = 0;
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
if type_idnum != resource_type {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if let Ok(value) = parser.parse() {
let resolve_sym = |id: u64| -> String {
if id < base_symbol_count {
KFX_SYMBOL_TABLE
.get(id as usize)
.copied()
.unwrap_or("?")
.to_string()
} else {
let ext_idx = (id as usize) - (base_symbol_count as usize);
extended_symbols
.get(ext_idx)
.cloned()
.unwrap_or_else(|| "?".to_string())
}
};
if let boko::kfx::ion::IonValue::Struct(fields) = &value {
let mut resource_name = String::new();
let mut format = String::new();
let mut location = String::new();
let mut width: Option<i64> = None;
let mut height: Option<i64> = None;
for (field_id, field_value) in fields {
let field_name = resolve_sym(*field_id);
match field_name.as_str() {
"resource_name" => {
if let boko::kfx::ion::IonValue::Symbol(s) = field_value {
resource_name = resolve_sym(*s);
}
}
"format" => {
if let boko::kfx::ion::IonValue::Symbol(s) = field_value {
format = resolve_sym(*s);
}
}
"location" => {
if let boko::kfx::ion::IonValue::String(s) = field_value {
location = s.clone();
}
}
"resource_width" => {
if let boko::kfx::ion::IonValue::Int(w) = field_value {
width = Some(*w);
}
}
"resource_height" => {
if let boko::kfx::ion::IonValue::Int(h) = field_value {
height = Some(*h);
}
}
_ => {}
}
}
if !resource_name.is_empty() {
resource_count += 1;
let dims = match (width, height) {
(Some(w), Some(h)) => format!(" {}x{}", w, h),
_ => String::new(),
};
println!("{:<10} {:<6}{} → {}", resource_name, format, dims, location);
}
}
}
}
println!("\nTotal resources: {}", resource_count);
Ok(())
}
fn report_storylines(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len() as u64;
let entry_size = 24;
let num_entries = index_length / entry_size;
let storyline_type = KfxSymbol::Storyline as u32;
println!("=== Storylines ===\n");
let mut storyline_count = 0;
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
if type_idnum != storyline_type {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if let Ok(value) = parser.parse() {
let resolve_sym = |id: u64| -> String {
if id < base_symbol_count {
KFX_SYMBOL_TABLE
.get(id as usize)
.copied()
.unwrap_or("?")
.to_string()
} else {
let ext_idx = (id as usize) - (base_symbol_count as usize);
extended_symbols
.get(ext_idx)
.cloned()
.unwrap_or_else(|| "?".to_string())
}
};
if let boko::kfx::ion::IonValue::Struct(fields) = &value {
let mut story_name = String::new();
let mut stats = ContentStats::default();
for (field_id, field_value) in fields {
let field_name = resolve_sym(*field_id);
match field_name.as_str() {
"story_name" => {
if let boko::kfx::ion::IonValue::Symbol(s) = field_value {
story_name = resolve_sym(*s);
}
}
"content_list" => {
if let boko::kfx::ion::IonValue::List(items) = field_value {
collect_content_stats(items, &resolve_sym, &mut stats);
}
}
_ => {}
}
}
if !story_name.is_empty() {
storyline_count += 1;
println!("--- {} ---", story_name);
let mut type_parts = Vec::new();
if stats.text_count > 0 {
type_parts.push(format!("{} text items", stats.text_count));
}
if stats.image_count > 0 {
type_parts.push(format!("{} images", stats.image_count));
}
if stats.heading_count > 0 {
type_parts.push(format!("{} headings", stats.heading_count));
}
if stats.link_count > 0 {
type_parts.push(format!("{} links", stats.link_count));
}
println!(" Content: {}", type_parts.join(", "));
if !stats.content_refs.is_empty() {
if stats.content_refs.len() <= 5 {
println!(" Content chunks: {}", stats.content_refs.join(", "));
} else {
println!(
" Content chunks ({}): {}, ..., {}",
stats.content_refs.len(),
stats.content_refs[..2].join(", "),
stats.content_refs.last().unwrap()
);
}
}
if !stats.resource_refs.is_empty() {
println!(" Resources: {}", stats.resource_refs.join(", "));
}
if let Some(ref text) = stats.first_text {
let display = if text.len() >= 55 {
format!("{}...", text)
} else {
text.clone()
};
println!(" Sample: \"{}\"", display.replace('\n', " "));
}
println!();
}
}
}
}
println!("\nTotal storylines: {}", storyline_count);
Ok(())
}
#[derive(Default)]
struct ContentStats {
text_count: usize,
image_count: usize,
heading_count: usize,
link_count: usize,
content_refs: Vec<String>,
resource_refs: Vec<String>,
first_text: Option<String>,
}
fn collect_content_stats<F>(
items: &[boko::kfx::ion::IonValue],
resolve_sym: &F,
stats: &mut ContentStats,
) where
F: Fn(u64) -> String,
{
for item in items {
if let boko::kfx::ion::IonValue::Struct(fields) = item {
let mut is_image = false;
let mut is_heading = false;
let mut has_links = false;
for (field_id, field_value) in fields {
let field_name = resolve_sym(*field_id);
match field_name.as_str() {
"type" => {
if let boko::kfx::ion::IonValue::Symbol(s) = field_value {
let type_name = resolve_sym(*s);
if type_name == "image" {
is_image = true;
}
}
}
"yj.semantics.heading_level" => {
is_heading = true;
}
"style_events" => {
if let boko::kfx::ion::IonValue::List(events) = field_value {
for event in events {
if let boko::kfx::ion::IonValue::Struct(event_fields) = event {
for (eid, _) in event_fields {
if resolve_sym(*eid) == "link_to" {
has_links = true;
}
}
}
}
}
}
"content_list" => {
if let boko::kfx::ion::IonValue::List(nested) = field_value {
collect_content_stats(nested, resolve_sym, stats);
}
}
"name" => {
if let boko::kfx::ion::IonValue::Symbol(s) = field_value {
let name = resolve_sym(*s);
if name.starts_with("content_") && !stats.content_refs.contains(&name) {
stats.content_refs.push(name);
}
}
}
"resource_name" => {
if let boko::kfx::ion::IonValue::Symbol(s) = field_value {
let name = resolve_sym(*s);
if !stats.resource_refs.contains(&name) {
stats.resource_refs.push(name);
}
}
}
"content" => {
if stats.first_text.is_none()
&& let boko::kfx::ion::IonValue::Struct(content_fields) = field_value
{
for (cid, cval) in content_fields {
if resolve_sym(*cid) == "text"
&& let boko::kfx::ion::IonValue::String(t) = cval
{
let sample: String = t.chars().take(60).collect();
if !sample.trim().is_empty() {
stats.first_text = Some(sample);
}
}
}
}
}
_ => {}
}
}
if is_image {
stats.image_count += 1;
} else {
stats.text_count += 1;
}
if is_heading {
stats.heading_count += 1;
}
if has_links {
stats.link_count += 1;
}
}
}
}
#[derive(Debug, Clone)]
struct ContentInfo {
content_type: String,
content_ref: Option<String>,
content_index: Option<i64>,
}
fn report_locations(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
use std::collections::HashMap;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len() as u64;
let resolve_sym = |id: u64| -> String {
if id < base_symbol_count {
KFX_SYMBOL_TABLE
.get(id as usize)
.copied()
.unwrap_or("?")
.to_string()
} else {
let ext_idx = (id as usize) - (base_symbol_count as usize);
extended_symbols
.get(ext_idx)
.cloned()
.unwrap_or_else(|| "?".to_string())
}
};
let entry_size = 24;
let num_entries = index_length / entry_size;
let location_map_type = KfxSymbol::LocationMap as u32;
let position_map_type = KfxSymbol::PositionMap as u32;
let storyline_type = KfxSymbol::Storyline as u32;
let content_type_id = KfxSymbol::Content as u32;
let mut position_to_section: HashMap<i64, String> = HashMap::new();
let mut position_to_content: HashMap<i64, ContentInfo> = HashMap::new();
let mut content_texts: HashMap<String, Vec<String>> = HashMap::new();
fn extract_content_items(
value: &boko::kfx::ion::IonValue,
_story_name: &str,
resolve_sym: &dyn Fn(u64) -> String,
result: &mut HashMap<i64, ContentInfo>,
) {
if let boko::kfx::ion::IonValue::Struct(fields) = value {
let mut item_id: Option<i64> = None;
let mut item_type = String::new();
let mut content_ref: Option<String> = None;
let mut content_index: Option<i64> = None;
let mut nested_content: Option<&boko::kfx::ion::IonValue> = None;
for (fid, fval) in fields {
let fname = resolve_sym(*fid);
match fname.as_str() {
"id" => {
if let boko::kfx::ion::IonValue::Int(id) = fval {
item_id = Some(*id);
}
}
"type" => {
if let boko::kfx::ion::IonValue::Symbol(s) = fval {
item_type = resolve_sym(*s);
}
}
"content" => {
if let boko::kfx::ion::IonValue::Struct(cf) = fval {
for (cid, cval) in cf {
let cname = resolve_sym(*cid);
match cname.as_str() {
"name" => {
if let boko::kfx::ion::IonValue::Symbol(s) = cval {
content_ref = Some(resolve_sym(*s));
}
}
"index" => {
if let boko::kfx::ion::IonValue::Int(idx) = cval {
content_index = Some(*idx);
}
}
_ => {}
}
}
}
}
"content_list" => {
nested_content = Some(fval);
}
_ => {}
}
}
if let Some(id) = item_id {
result.insert(
id,
ContentInfo {
content_type: if item_type.is_empty() {
"unknown".to_string()
} else {
item_type
},
content_ref,
content_index,
},
);
}
if let Some(boko::kfx::ion::IonValue::List(items)) = nested_content {
for item in items {
extract_content_items(item, _story_name, resolve_sym, result);
}
}
}
}
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if type_idnum == position_map_type {
if let Ok(value) = parser.parse() {
let inner = match &value {
boko::kfx::ion::IonValue::Annotated(_, inner) => inner.as_ref(),
_ => &value,
};
if let boko::kfx::ion::IonValue::List(sections) = inner {
for section in sections {
if let boko::kfx::ion::IonValue::Struct(sec_fields) = section {
let mut sec_name = String::new();
let mut contains: Vec<i64> = Vec::new();
for (sid, sval) in sec_fields {
let fname = resolve_sym(*sid);
match fname.as_str() {
"section_name" => {
if let boko::kfx::ion::IonValue::Symbol(s) = sval {
sec_name = resolve_sym(*s);
}
}
"contains" => {
if let boko::kfx::ion::IonValue::List(refs) = sval {
for r in refs {
if let boko::kfx::ion::IonValue::Int(pid) = r {
contains.push(*pid);
}
}
}
}
_ => {}
}
}
for pid in contains {
position_to_section.insert(pid, sec_name.clone());
}
}
}
}
}
} else if type_idnum == storyline_type
&& let Ok(value) = parser.parse()
&& let boko::kfx::ion::IonValue::Struct(fields) = &value
{
let mut story_name = String::new();
let mut content_list: Option<&boko::kfx::ion::IonValue> = None;
for (fid, fval) in fields {
let fname = resolve_sym(*fid);
match fname.as_str() {
"story_name" => {
if let boko::kfx::ion::IonValue::Symbol(s) = fval {
story_name = resolve_sym(*s);
}
}
"content_list" => {
content_list = Some(fval);
}
_ => {}
}
}
if let Some(boko::kfx::ion::IonValue::List(items)) = content_list {
for item in items {
extract_content_items(
item,
&story_name,
&resolve_sym,
&mut position_to_content,
);
}
}
} else if type_idnum == content_type_id
&& let Ok(value) = parser.parse()
&& let boko::kfx::ion::IonValue::Struct(fields) = &value
{
let mut content_name = String::new();
let mut texts: Vec<String> = Vec::new();
for (fid, fval) in fields {
let fname = resolve_sym(*fid);
match fname.as_str() {
"name" => {
if let boko::kfx::ion::IonValue::Symbol(s) = fval {
content_name = resolve_sym(*s);
}
}
"content_list" => {
if let boko::kfx::ion::IonValue::List(items) = fval {
for item in items {
if let boko::kfx::ion::IonValue::String(s) = item {
texts.push(s.clone());
}
}
}
}
_ => {}
}
}
if !content_name.is_empty() && !texts.is_empty() {
content_texts.insert(content_name, texts);
}
}
}
println!("=== Location Map ===\n");
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
if type_idnum != location_map_type {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if let Ok(value) = parser.parse() {
println!("Maps Kindle locations (indices) to position IDs in the content.\n");
#[derive(Debug)]
struct LocationEntry {
index: usize,
id: i64,
offset: i64,
}
let mut all_locations: Vec<LocationEntry> = Vec::new();
let mut location_index = 0usize;
if let boko::kfx::ion::IonValue::List(items) = &value {
for item in items {
if let boko::kfx::ion::IonValue::Struct(fields) = item {
for (fid, field_value) in fields {
let fname = resolve_sym(*fid);
if fname == "locations"
&& let boko::kfx::ion::IonValue::List(locations) = field_value
{
for loc in locations {
if let boko::kfx::ion::IonValue::Struct(loc_fields) = loc {
let mut id = 0i64;
let mut offset = 0i64;
for (lid, lval) in loc_fields {
let lname = resolve_sym(*lid);
match lname.as_str() {
"id" => {
if let boko::kfx::ion::IonValue::Int(v) = lval {
id = *v;
}
}
"offset" => {
if let boko::kfx::ion::IonValue::Int(v) = lval {
offset = *v;
}
}
_ => {}
}
}
all_locations.push(LocationEntry {
index: location_index,
id,
offset,
});
location_index += 1;
}
}
}
}
}
}
}
let unique_ids: std::collections::HashSet<i64> =
all_locations.iter().map(|e| e.id).collect();
println!("Total location entries: {}", all_locations.len());
println!("Unique position IDs: {}", unique_ids.len());
println!("Position→Section mappings: {}", position_to_section.len());
println!("Position→Content mappings: {}", position_to_content.len());
if !all_locations.is_empty() {
let min_id = all_locations.iter().map(|e| e.id).min().unwrap_or(0);
let max_id = all_locations.iter().map(|e| e.id).max().unwrap_or(0);
println!("Position ID range: {}..{}", min_id, max_id);
println!();
let format_entry = |entry: &LocationEntry| -> String {
let mut result = format!("loc {:>5} → pos {}", entry.index, entry.id);
if entry.offset != 0 {
result.push_str(&format!(":{}", entry.offset));
}
if let Some(sec_name) = position_to_section.get(&entry.id) {
result.push_str(&format!(" [{}]", sec_name));
}
if let Some(info) = position_to_content.get(&entry.id) {
result.push_str(&format!(" {}", info.content_type));
if let Some(ref content_ref) = info.content_ref
&& let Some(texts) = content_texts.get(content_ref)
{
let idx = info.content_index.unwrap_or(0) as usize;
if let Some(text) = texts.get(idx) {
let offset = entry.offset as usize;
let snippet = if offset < text.len() {
let start = offset;
let end = (offset + 40).min(text.len());
let s: String =
text.chars().skip(start).take(end - start).collect();
s.replace('\n', " ")
} else {
let end = 40.min(text.len());
let s: String = text.chars().take(end).collect();
s.replace('\n', " ")
};
result.push_str(&format!(" \"{}...\"", snippet.trim()));
}
}
}
result
};
println!("Sample entries (first 15):");
for entry in all_locations.iter().take(15) {
println!(" {}", format_entry(entry));
}
if all_locations.len() > 15 {
println!(" ...");
println!("\nLast 5 entries:");
for entry in all_locations
.iter()
.rev()
.take(5)
.collect::<Vec<_>>()
.into_iter()
.rev()
{
println!(" {}", format_entry(entry));
}
}
let entries_with_text: Vec<_> = all_locations
.iter()
.filter(|e| {
if let Some(info) = position_to_content.get(&e.id)
&& let Some(ref content_ref) = info.content_ref
&& let Some(idx) = info.content_index
&& let Some(texts) = content_texts.get(content_ref)
{
return texts.get(idx as usize).is_some();
}
false
})
.collect();
if !entries_with_text.is_empty() {
println!("\nSample entries with text (first 10):");
for entry in entries_with_text.iter().take(10) {
println!(" {}", format_entry(entry));
}
println!(" ({} total entries with text)", entries_with_text.len());
}
println!("\n--- Section breakdown ---");
let mut section_counts: HashMap<String, usize> = HashMap::new();
for entry in &all_locations {
if let Some(sec_name) = position_to_section.get(&entry.id) {
*section_counts.entry(sec_name.clone()).or_insert(0) += 1;
}
}
let mut section_list: Vec<_> = section_counts.iter().collect();
section_list.sort_by_key(|(name, _)| name.as_str());
for (sec_name, count) in section_list {
println!(" {}: {} locations", sec_name, count);
}
println!("\n--- Content type breakdown ---");
let mut type_counts: HashMap<String, usize> = HashMap::new();
for entry in &all_locations {
if let Some(info) = position_to_content.get(&entry.id) {
*type_counts.entry(info.content_type.clone()).or_insert(0) += 1;
}
}
let mut type_list: Vec<_> = type_counts.iter().collect();
type_list.sort_by(|(_, a), (_, b)| b.cmp(a)); for (content_type, count) in type_list {
println!(" {}: {} locations", content_type, count);
}
}
}
return Ok(());
}
eprintln!("No location_map entity found");
Ok(())
}
fn report_positions(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len() as u64;
let entry_size = 24;
let num_entries = index_length / entry_size;
let position_map_type = KfxSymbol::PositionMap as u32;
let position_id_map_type = KfxSymbol::PositionIdMap as u32;
println!("=== Position Maps ===\n");
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
let is_position_map = type_idnum == position_map_type;
let is_position_id_map = type_idnum == position_id_map_type;
if !is_position_map && !is_position_id_map {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if let Ok(value) = parser.parse() {
let resolve_sym_inner = |id: u64| -> String {
if id < base_symbol_count {
KFX_SYMBOL_TABLE
.get(id as usize)
.copied()
.unwrap_or("?")
.to_string()
} else {
let ext_idx = (id as usize) - (base_symbol_count as usize);
extended_symbols
.get(ext_idx)
.cloned()
.unwrap_or_else(|| "?".to_string())
}
};
let inner = match &value {
boko::kfx::ion::IonValue::Annotated(_, inner) => inner.as_ref(),
_ => &value,
};
if is_position_map {
println!("--- position_map ---");
println!("Maps sections to the EIDs they contain.\n");
if let boko::kfx::ion::IonValue::List(sections) = inner {
let mut total_refs = 0usize;
for section in sections.iter() {
if let boko::kfx::ion::IonValue::Struct(sec_fields) = section {
let mut section_name = String::new();
let mut contains: Vec<i64> = Vec::new();
for (sid, sval) in sec_fields {
let sname = resolve_sym_inner(*sid);
match sname.as_str() {
"section_name" => {
if let boko::kfx::ion::IonValue::Symbol(sym_id) = sval {
section_name = resolve_sym_inner(*sym_id);
}
}
"contains" => {
if let boko::kfx::ion::IonValue::List(refs) = sval {
for r in refs {
if let boko::kfx::ion::IonValue::Int(eid) = r {
contains.push(*eid);
}
}
}
}
_ => {}
}
}
total_refs += contains.len();
if !contains.is_empty() {
let min_eid = contains.iter().min().copied().unwrap_or(0);
let max_eid = contains.iter().max().copied().unwrap_or(0);
println!(
"Section {} ({} EIDs: {}..{})",
section_name,
contains.len(),
min_eid,
max_eid
);
if contains.len() <= 10 {
println!(" EIDs: {:?}", contains);
} else {
println!(" first 5: {:?}", &contains[..5]);
println!(" last 5: {:?}", &contains[contains.len() - 5..]);
}
println!();
}
}
}
println!("Total sections: {}", sections.len());
println!("Total EID refs: {}", total_refs);
}
println!();
} else if is_position_id_map {
println!("--- position_id_map ---");
println!("Maps cumulative positions (PIDs) to EID + offset.\n");
if let boko::kfx::ion::IonValue::List(entries) = inner {
let mut mappings: Vec<(i64, i64, i64)> = Vec::new();
for entry in entries {
if let boko::kfx::ion::IonValue::Struct(fields) = entry {
let mut pid = 0i64;
let mut eid = 0i64;
let mut offset = 0i64;
for (fid, fval) in fields {
let fname = resolve_sym_inner(*fid);
match fname.as_str() {
"pid" => {
if let boko::kfx::ion::IonValue::Int(v) = fval {
pid = *v;
}
}
"eid" => {
if let boko::kfx::ion::IonValue::Int(v) = fval {
eid = *v;
}
}
"offset" => {
if let boko::kfx::ion::IonValue::Int(v) = fval {
offset = *v;
}
}
_ => {}
}
}
mappings.push((pid, eid, offset));
}
}
println!("Total entries: {}", mappings.len());
if !mappings.is_empty() {
let min_pid = mappings.iter().map(|(p, _, _)| *p).min().unwrap_or(0);
let max_pid = mappings.iter().map(|(p, _, _)| *p).max().unwrap_or(0);
let min_eid = mappings.iter().map(|(_, e, _)| *e).min().unwrap_or(0);
let max_eid = mappings.iter().map(|(_, e, _)| *e).max().unwrap_or(0);
let nonzero_offsets = mappings.iter().filter(|(_, _, o)| *o != 0).count();
println!("PID range: {}..{}", min_pid, max_pid);
println!("EID range: {}..{}", min_eid, max_eid);
println!("Entries with non-zero offset: {}", nonzero_offsets);
println!();
println!("Sample mappings (first 15):");
for (pid, eid, offset) in mappings.iter().take(15) {
if *offset == 0 {
println!(" pid {:>6} → eid {}", pid, eid);
} else {
println!(" pid {:>6} → eid {}:{}", pid, eid, offset);
}
}
if mappings.len() > 15 {
println!(" ...");
}
}
}
println!();
}
}
}
Ok(())
}
fn report_content(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len() as u64;
let entry_size = 24;
let num_entries = index_length / entry_size;
let content_type = KfxSymbol::Content as u32;
println!("=== Content Chunks ===\n");
let mut content_count = 0;
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(entity_id) = read_u32_le(data, entry_offset) else {
continue;
};
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
if type_idnum != content_type {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let payload_data = &entity_data[entity_header_len..];
let payload_len = payload_data.len();
let mut parser = IonParser::new(payload_data);
let entity_name = if (entity_id as u64) < base_symbol_count {
KFX_SYMBOL_TABLE
.get(entity_id as usize)
.copied()
.unwrap_or("?")
.to_string()
} else {
let ext_idx = entity_id as usize - base_symbol_count as usize;
extended_symbols
.get(ext_idx)
.cloned()
.unwrap_or_else(|| format!("${}", entity_id))
};
content_count += 1;
if let Ok(value) = parser.parse() {
let resolve_sym = |id: u64| -> String {
if id < base_symbol_count {
KFX_SYMBOL_TABLE
.get(id as usize)
.copied()
.unwrap_or("?")
.to_string()
} else {
let ext_idx = (id as usize) - (base_symbol_count as usize);
extended_symbols
.get(ext_idx)
.cloned()
.unwrap_or_else(|| "?".to_string())
}
};
let inner = match &value {
boko::kfx::ion::IonValue::Annotated(_, inner) => inner.as_ref(),
_ => &value,
};
if let boko::kfx::ion::IonValue::Struct(fields) = inner {
let mut content_type_str = String::new();
let mut content_len = 0usize;
for (field_id, field_value) in fields {
let field_name = resolve_sym(*field_id);
match field_name.as_str() {
"type" => {
if let boko::kfx::ion::IonValue::Symbol(s) = field_value {
content_type_str = resolve_sym(*s);
}
}
"content" => match field_value {
boko::kfx::ion::IonValue::String(s) => content_len = s.len(),
boko::kfx::ion::IonValue::Blob(b) => content_len = b.len(),
_ => {}
},
_ => {}
}
}
if content_len == 0 {
content_len = payload_len;
}
println!(
"{:<20} {:<12} {} bytes",
entity_name, content_type_str, content_len
);
} else {
println!("{:<20} {:<12} {} bytes (raw)", entity_name, "", payload_len);
}
} else {
println!("{:<20} {:<12} {} bytes (raw)", entity_name, "", payload_len);
}
}
println!("\nTotal content chunks: {}", content_count);
Ok(())
}
fn report_dependencies(data: &[u8]) -> IonResult<()> {
use boko::kfx::ion::IonParser;
use boko::kfx::symbols::KfxSymbol;
if data.len() < 18 || &data[0..4] != b"CONT" {
eprintln!("Not a KFX container");
return Ok(());
}
let Some(header_len) = read_u32_le(data, 6).map(|v| v as usize) else {
eprintln!("Failed to read header length");
return Ok(());
};
let Some(container_info_offset) = read_u32_le(data, 10).map(|v| v as usize) else {
eprintln!("Failed to read container info offset");
return Ok(());
};
let Some(container_info_length) = read_u32_le(data, 14).map(|v| v as usize) else {
eprintln!("Failed to read container info length");
return Ok(());
};
if container_info_offset + container_info_length > data.len() {
eprintln!("Container info out of bounds");
return Ok(());
}
let container_info_data =
&data[container_info_offset..container_info_offset + container_info_length];
let Some((index_offset, index_length)) = parse_container_info_for_index(container_info_data)
else {
eprintln!("Could not find index table");
return Ok(());
};
let extended_symbols = if let Some((doc_sym_offset, doc_sym_length)) =
parse_container_info_for_doc_symbols(container_info_data)
{
if doc_sym_offset + doc_sym_length <= data.len() {
extract_doc_symbols(&data[doc_sym_offset..doc_sym_offset + doc_sym_length])
} else {
Vec::new()
}
} else {
Vec::new()
};
let base_symbol_count = KFX_SYMBOL_TABLE.len() as u64;
let entry_size = 24;
let num_entries = index_length / entry_size;
let container_entity_map_type = KfxSymbol::ContainerEntityMap as u32;
println!("=== Entity Dependencies ===\n");
for i in 0..num_entries {
let entry_offset = index_offset + i * entry_size;
if entry_offset + entry_size > data.len() {
break;
}
let Some(type_idnum) = read_u32_le(data, entry_offset + 4) else {
continue;
};
let Some(entity_offset) = read_u64_le(data, entry_offset + 8).map(|v| v as usize) else {
continue;
};
let Some(entity_len) = read_u32_le(data, entry_offset + 16).map(|v| v as usize) else {
continue;
};
if type_idnum != container_entity_map_type {
continue;
}
let abs_offset = header_len + entity_offset;
if abs_offset + entity_len > data.len() {
continue;
}
let entity_data = &data[abs_offset..abs_offset + entity_len];
if entity_data.len() < 10 || &entity_data[0..4] != b"ENTY" {
continue;
}
let Some(entity_header_len) = read_u32_le(entity_data, 6).map(|v| v as usize) else {
continue;
};
if entity_header_len >= entity_data.len() {
continue;
}
let ion_data = &entity_data[entity_header_len..];
let mut parser = IonParser::new(ion_data);
if let Ok(value) = parser.parse() {
let resolve_sym = |id: u64| -> String {
if id < base_symbol_count {
KFX_SYMBOL_TABLE
.get(id as usize)
.copied()
.unwrap_or("?")
.to_string()
} else {
let ext_idx = (id as usize) - (base_symbol_count as usize);
extended_symbols
.get(ext_idx)
.cloned()
.unwrap_or_else(|| "?".to_string())
}
};
let inner = match &value {
boko::kfx::ion::IonValue::Annotated(_, inner) => inner.as_ref(),
_ => &value,
};
println!("Lists all entities in the KFX container.\n");
if let boko::kfx::ion::IonValue::Struct(fields) = inner {
for (field_id, field_value) in fields {
let field_name = resolve_sym(*field_id);
if field_name == "container_list"
&& let boko::kfx::ion::IonValue::List(containers) = field_value
{
let mut total_entities = 0usize;
for container in containers {
if let boko::kfx::ion::IonValue::Struct(c_fields) = container {
let mut container_name = String::new();
let mut entity_names: Vec<String> = Vec::new();
for (cid, cval) in c_fields {
let cname = resolve_sym(*cid);
match cname.as_str() {
"id" => {
if let boko::kfx::ion::IonValue::String(s) = cval {
container_name = s.clone();
}
}
"contains" => {
if let boko::kfx::ion::IonValue::List(names) = cval {
for name in names {
if let boko::kfx::ion::IonValue::Symbol(s) =
name
{
entity_names.push(resolve_sym(*s));
}
}
}
}
_ => {}
}
}
total_entities += entity_names.len();
if !entity_names.is_empty() {
println!("Container: {}", container_name);
println!("Entities ({}):", entity_names.len());
let mut by_type: std::collections::HashMap<
String,
Vec<String>,
> = std::collections::HashMap::new();
for name in &entity_names {
let type_prefix = if name.starts_with("content_") {
"content"
} else if name.starts_with("style_")
|| name.starts_with('s')
&& name
.chars()
.nth(1)
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
"style"
} else if name.starts_with("anchor_")
|| name.starts_with('a') && name.len() > 1
{
"anchor"
} else if name.starts_with('l') && name.len() > 1 {
"storyline"
} else if name.starts_with('c') && name.len() > 1 {
"section"
} else if name.starts_with('e') && name.len() > 1 {
"resource"
} else {
"other"
};
by_type
.entry(type_prefix.to_string())
.or_default()
.push(name.clone());
}
let mut types: Vec<_> = by_type.keys().collect();
types.sort();
for type_name in types {
let names = &by_type[type_name];
if names.len() <= 8 {
println!(
" {} ({}): {}",
type_name,
names.len(),
names.join(", ")
);
} else {
println!(
" {} ({}): {}, ... {}",
type_name,
names.len(),
names[..3].join(", "),
names[names.len() - 2..].join(", ")
);
}
}
println!();
}
}
}
println!("Total containers: {}", containers.len());
println!("Total entity refs: {}", total_entities);
}
}
}
}
return Ok(());
}
eprintln!("No container_entity_map found");
Ok(())
}