use super::{format_ref, is_internal, parse_ref, Link, Links};
use crate::config::Layout;
use crate::error::{Error, Result};
use links_notation::binary::{DecodeLimits, LinksPacket, Reference};
use links_notation::LiNo;
use std::collections::HashMap;
use std::fmt::Write;
pub const DOUBLETS_RECORD: usize = 64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Format {
Archive,
Doublets,
Lino,
}
impl Format {
pub const ALL: [Self; 3] = [Self::Archive, Self::Doublets, Self::Lino];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Archive => "binary",
Self::Doublets => "doublets",
Self::Lino => "lino",
}
}
pub fn parse(text: &str) -> Result<Self> {
Self::ALL
.into_iter()
.find(|format| format.name() == text)
.ok_or_else(|| {
Error::invalid(format!(
"unknown links format {text:?} (binary, doublets or lino)"
))
})
}
#[must_use]
pub fn detect(bytes: &[u8]) -> Self {
let text = !bytes.contains(&0)
&& std::str::from_utf8(bytes).is_ok_and(|text| {
text.trim_start()
.chars()
.next()
.is_some_and(|c| c == '(' || c == '#' || c.is_ascii_digit())
});
if text {
Self::Lino
} else if read_archive(bytes).is_ok() {
Self::Archive
} else {
Self::Doublets
}
}
#[must_use]
pub fn of_path(path: &std::path::Path) -> Option<Self> {
match path.extension()?.to_str()? {
"links" => Some(Self::Doublets),
"lino" | "txt" => Some(Self::Lino),
"lnb" | "bin" => Some(Self::Archive),
_ => None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Archive {
pub links: Vec<Link>,
pub names: Vec<(u64, String)>,
}
fn binary_error(error: impl std::fmt::Display) -> Error {
Error::invalid(format!("binary links notation: {error}"))
}
pub fn write_archive(links: &[Link], names: &[(u64, String)]) -> Result<Vec<u8>> {
let doublets: Vec<(u64, Vec<Reference>)> = links
.iter()
.map(|link| {
(
link.index,
vec![
Reference::Internal(link.source),
Reference::Internal(link.target),
],
)
})
.collect();
let labels: Vec<(u64, Vec<Reference>)> = names
.iter()
.enumerate()
.map(|(index, (address, name))| {
let refs = std::iter::once(*address)
.chain(name.chars().map(u64::from))
.map(Reference::External)
.collect();
(index as u64 + 1, refs)
})
.collect();
let mut out = LinksPacket::pack(false, &doublets, true)
.and_then(|packet| packet.to_bytes())
.map_err(binary_error)?;
out.extend(
LinksPacket::pack(true, &labels, true)
.and_then(|packet| packet.to_bytes())
.map_err(binary_error)?,
);
Ok(out)
}
pub fn read_archive(bytes: &[u8]) -> Result<Archive> {
let mut reader = bytes;
let limits = DecodeLimits::unlimited();
let mut next = || -> Result<LinksPacket> {
LinksPacket::read_from(&mut reader, &limits)
.map_err(binary_error)?
.ok_or_else(|| Error::invalid("the store archive is cut short"))
};
let links_packet = next()?;
let names_packet = next()?;
if !reader.is_empty() {
return Err(Error::invalid("bytes after the store archive"));
}
let mut archive = Archive::default();
for (index, refs) in links_packet.links() {
let [Reference::Internal(source), Reference::Internal(target)] = refs else {
return Err(Error::invalid(format!(
"archive link {index} is not a doublet of internal references"
)));
};
archive.links.push(Link {
index,
source: *source,
target: *target,
});
}
for (_, refs) in names_packet.links() {
let mut values = refs.iter().map(|reference| match reference {
Reference::External(value) => Ok(*value),
Reference::Internal(_) => Err(Error::invalid("a name holds an internal reference")),
});
let address = values.next().transpose()?.unwrap_or_default();
let name = values
.map(|value| {
value.and_then(|value| {
u32::try_from(value)
.ok()
.and_then(char::from_u32)
.ok_or_else(|| Error::invalid(format!("bad code point {value} in a name")))
})
})
.collect::<Result<String>>()?;
archive.names.push((address, name));
}
Ok(archive)
}
pub fn write_doublets(links: &[Link]) -> Result<Vec<u8>> {
let mut united = Links::new(Layout::United, 4096);
united.place_all(links)?;
let top = united.allocated();
let mut out = Vec::with_capacity((top as usize + 1) * DOUBLETS_RECORD);
for index in 0..=top {
out.extend_from_slice(united.records.data.record(index));
}
out[8..16].copy_from_slice(&top.to_le_bytes());
Ok(out)
}
pub fn read_doublets(bytes: &[u8]) -> Result<Vec<Link>> {
if bytes.len() < DOUBLETS_RECORD || !bytes.len().is_multiple_of(DOUBLETS_RECORD) {
return Err(Error::invalid(
"a doublets file is a whole number of 64-byte records",
));
}
let cell = |record: u64, index: usize| {
let offset = record as usize * DOUBLETS_RECORD + index * 8;
u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap_or_default())
};
let allocated = cell(0, 0);
let first_free = cell(0, 3);
let records = (bytes.len() / DOUBLETS_RECORD) as u64;
if allocated >= records {
return Err(Error::invalid(format!(
"the doublets header claims {allocated} links but the file holds {}",
records - 1
)));
}
Ok((1..=allocated)
.filter(|&index| index != first_free && (cell(index, 4) != 0 || cell(index, 0) == 0))
.map(|index| Link {
index,
source: cell(index, 0),
target: cell(index, 1),
})
.collect())
}
#[must_use]
pub fn write_lino(links: &[Link]) -> String {
let mut out = String::new();
for link in links {
let _ = writeln!(
out,
"({}: {} {})",
link.index,
format_ref(link.source),
format_ref(link.target)
);
}
out
}
fn lino_ref(value: &LiNo<String>) -> Result<u64> {
match value {
LiNo::Ref(text) => parse_ref(text),
LiNo::Link { .. } => Err(Error::invalid("nested links are not doublets")),
}
}
pub fn read_lino(text: &str) -> Result<Vec<Link>> {
let document = links_notation::parse_lino_to_links(text)
.map_err(|error| Error::invalid(format!("links notation: {error}")))?;
let mut links = Vec::with_capacity(document.len());
for item in &document {
let LiNo::Link {
id: Some(id),
values,
} = item
else {
return Err(Error::invalid(format!(
"expected (index: source target), found {item:?}"
)));
};
let [source, target] = values.as_slice() else {
return Err(Error::invalid(format!("link {id} is not a doublet")));
};
let index = parse_ref(id)?;
if !is_internal(index) {
return Err(Error::invalid(format!("bad link index {id}")));
}
links.push(Link {
index,
source: lino_ref(source)?,
target: lino_ref(target)?,
});
}
links.sort_unstable_by_key(|link| link.index);
if links.windows(2).any(|pair| pair[0].index == pair[1].index) {
return Err(Error::invalid("a link index appears twice"));
}
Ok(links)
}
pub fn read(format: Format, bytes: &[u8]) -> Result<Vec<Link>> {
match format {
Format::Archive => read_archive(bytes).map(|archive| archive.links),
Format::Doublets => read_doublets(bytes),
Format::Lino => read_lino(
std::str::from_utf8(bytes).map_err(|_| Error::invalid("links text is not UTF-8"))?,
),
}
}
pub fn write(format: Format, links: &[Link], names: &[(u64, String)]) -> Result<Vec<u8>> {
match format {
Format::Archive => write_archive(links, names),
Format::Doublets => write_doublets(links),
Format::Lino => Ok(write_lino(links).into_bytes()),
}
}
fn components(links: &[Link], position: &HashMap<u64, usize>) -> Vec<Vec<usize>> {
const NONE: usize = usize::MAX;
let deps = |node: usize| {
let link = links[node];
[link.source, link.target]
.into_iter()
.filter_map(|end| position.get(&end).copied())
};
let count = links.len();
let (mut index, mut low) = (vec![NONE; count], vec![0; count]);
let mut on_stack = vec![false; count];
let (mut stack, mut out, mut next) = (Vec::new(), Vec::new(), 0);
for root in 0..count {
if index[root] != NONE {
continue;
}
let mut work = vec![(root, 0)];
while let Some((node, edge)) = work.pop() {
if edge == 0 {
index[node] = next;
low[node] = next;
next += 1;
stack.push(node);
on_stack[node] = true;
}
if let Some(dep) = deps(node).nth(edge) {
work.push((node, edge + 1));
if index[dep] == NONE {
work.push((dep, 0));
} else if on_stack[dep] {
low[node] = low[node].min(index[dep]);
}
continue;
}
if let Some(&(parent, _)) = work.last() {
low[parent] = low[parent].min(low[node]);
}
if low[node] == index[node] {
let mut component = Vec::new();
while let Some(member) = stack.pop() {
on_stack[member] = false;
component.push(member);
if member == node {
break;
}
}
out.push(component);
}
}
}
out
}
pub fn import_remapped(
store: &mut Links,
links: &[Link],
fixed: &HashMap<u64, u64, impl std::hash::BuildHasher>,
) -> Result<HashMap<u64, u64>> {
let position: HashMap<u64, usize> = links
.iter()
.enumerate()
.map(|(position, link)| (link.index, position))
.collect();
if position.len() != links.len() {
return Err(Error::invalid("a link index appears twice"));
}
let mut pairs = std::collections::HashSet::with_capacity(links.len());
for link in links {
if link.source == 0 && link.target == 0 {
return Err(Error::invalid("a link needs a non-null end"));
}
if !pairs.insert((link.source, link.target)) {
return Err(Error::invalid(format!(
"the pair ({} {}) appears twice",
format_ref(link.source),
format_ref(link.target)
)));
}
for end in [link.source, link.target] {
if is_internal(end) && !position.contains_key(&end) {
return Err(Error::invalid(format!(
"link {} refers to link {end}, which is not imported",
link.index
)));
}
}
}
for (&from, &to) in fixed {
if !position.contains_key(&from) || !store.exists(to) {
return Err(Error::invalid(format!(
"cannot map link {from} onto link {to}"
)));
}
}
let mut map: HashMap<u64, u64> = HashMap::with_capacity(links.len());
map.extend(fixed.iter().map(|(&from, &to)| (from, to)));
let translate = |map: &HashMap<u64, u64>, end: u64| {
if is_internal(end) {
map.get(&end).copied().unwrap_or(end)
} else {
end
}
};
for mut component in components(links, &position) {
component.retain(|&member| !fixed.contains_key(&links[member].index));
let Some(&first) = component.first() else {
continue;
};
let link = links[first];
let cyclic = component.len() > 1 || link.source == link.index || link.target == link.index;
if !cyclic {
let (source, target) = (translate(&map, link.source), translate(&map, link.target));
map.insert(link.index, store.get_or_create(source, target)?);
continue;
}
let mut next = store.allocated();
for &member in &component {
next += 1;
map.insert(links[member].index, next);
}
let mut placed: Vec<Link> = component
.iter()
.map(|&member| {
let link = links[member];
Link {
index: map[&link.index],
source: translate(&map, link.source),
target: translate(&map, link.target),
}
})
.collect();
placed.sort_unstable_by_key(|link| link.index);
store.place_all(&placed)?;
}
Ok(map)
}
#[cfg(test)]
mod tests;