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)?,
"toc" => report_toc(&data)?,
other => {
eprintln!("Unknown field report: {}. Supported: anchors, toc", 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 TocEntryInfo {
label: String,
target_id: Option<i64>,
target_offset: Option<i64>,
target_text: Option<String>, target_type: Option<String>, depth: usize,
}
fn report_toc(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_toc(
&value,
&extended_symbols,
base_symbol_count,
&content_map,
&fragment_content_map,
&container_type_map,
);
}
Ok(())
}
fn extract_and_print_toc(
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 is_toc = false;
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 {
let sym_name =
resolve_symbol(*sym_id, extended_symbols, base_symbol_count);
if sym_name == "toc" {
is_toc = true;
}
}
}
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 is_toc {
println!("=== Table of Contents ({}) ===\n", container_name);
if let Some(entry_list) = entries {
let mut toc_entries = Vec::new();
extract_toc_entries(
entry_list,
extended_symbols,
base_symbol_count,
content_map,
fragment_content_map,
container_type_map,
0,
&mut toc_entries,
);
for entry in &toc_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 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()
};
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, entry.label, position, type_info, preview, ellipsis
);
} else {
println!(
"{}{:<35} {:>12}{}",
indent, entry.label, position, type_info
);
}
}
println!("\nTotal entries: {}", toc_entries.len());
}
}
}
}
}
#[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)]
fn extract_toc_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<TocEntryInfo>,
) {
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 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::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() {
result.push(TocEntryInfo {
label: if label.is_empty() {
"(untitled)".to_string()
} else {
label
},
target_id,
target_offset,
target_text,
target_type,
depth,
});
}
if let Some(child_entries) = children {
extract_toc_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
}