Files
ODIAN_Launcher/src-tauri/src/market.rs
T
Kelorastus 73a8f2c30c updtae
2026-09-28 11:19:02 +02:00

629 lines
27 KiB
Rust

//! The marketplace: the tile sets and source files of a version an operator may choose.
//!
//! The DHP decides what is optional: a manifest item with `"optional": true` (and a title and
//! description) is left out of Install and offered here; everything else is part of every
//! install. A manifest from before the marketplace flags nothing, and the same rule is applied
//! to it here instead (see `pre_catalog`). What the operator chose is
//! `LauncherState::extras`, a set of item keys that outlive versions (`tiles:<style>/<dataset>`,
//! `src:<path>`), so an update refreshes exactly the chosen items.
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use odian_packs::manifest::{Manifest, SrcFile, TileSet};
use serde::Serialize;
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Kind {
Tiles,
Src,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Presence {
/// Every byte of this version's item is on the PC.
Installed,
/// Part of it is: a download that stopped, or an older version's copy. Installing resumes.
Partial,
Absent,
}
/// One item as the marketplace shows it.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Item {
pub key: String,
pub kind: Kind,
pub title: String,
pub description: String,
/// What installing it downloads, in full.
pub bytes: u64,
/// How many files it is made of: what a removal counts down.
pub files: u64,
/// Part of every install: shown, never offered for removal.
pub required: bool,
pub presence: Presence,
/// Chosen: installed with the version and kept by updates (required items always are).
pub selected: bool,
/// False when the DHP does not carry this item's bytes: it is listed so an operator knows
/// it exists, but only an ODIAN drive can install it.
pub on_dhp: bool,
}
/// The map a version from BEFORE the marketplace opens on. Those versions flag nothing, so
/// the catalog's rule is applied to them here - and what they shipped as their one certain
/// basemap was `dark`. Current versions say it themselves (`catalog.json` now keeps
/// `dark_v2` required instead), and this is not read for them.
const ALWAYS_INSTALLED_STYLE: &str = "dark";
/// A manifest from before the marketplace (pack 0.61.0) flags nothing optional, so everything
/// in it would be part of every install: copying such a version from a drive would offer no
/// choice at all, and the operator would face the whole 228 GB or nothing. The catalog's own
/// rule is applied to it instead - the app, the database and the dark map come with every
/// install, the rest is offered - so an older version is chosen from like a current one. A
/// manifest that flags anything is the DHP's to decide and is taken exactly as it stands.
fn pre_catalog(manifest: &Manifest) -> bool {
!manifest.tiles.iter().flat_map(|tiles| &tiles.sets).any(|set| set.optional)
&& !manifest.src.iter().flat_map(|src| &src.files).any(|file| file.optional)
}
fn tiles_optional(set: &TileSet, pre_catalog: bool) -> bool {
set.optional || (pre_catalog && set.style != ALWAYS_INSTALLED_STYLE)
}
fn src_optional(file: &SrcFile, pre_catalog: bool) -> bool {
file.optional || pre_catalog
}
pub fn tiles_key(set: &TileSet) -> String {
format!("tiles:{}/{}", set.style, set.dataset)
}
pub fn src_key(file: &SrcFile) -> String {
format!("src:{}", file.path)
}
fn tile_set_dir(data_dir: &Path, set: &TileSet) -> PathBuf {
data_dir.join("tiles").join(&set.style).join(&set.dataset)
}
fn src_path(data_dir: &Path, file: &SrcFile) -> PathBuf {
file.path.split('/').fold(data_dir.join("src"), |path, part| path.join(part))
}
fn with_suffix(path: &Path, suffix: &str) -> PathBuf {
let mut name = path.as_os_str().to_owned();
name.push(suffix);
PathBuf::from(name)
}
/// A tile set is complete when the installer's `.blocks-done.json` names this index and lists
/// all its blocks; it is partial when its folder exists at all.
fn tile_set_presence(data_dir: &Path, set: &TileSet) -> Presence {
let dir = tile_set_dir(data_dir, set);
let done = std::fs::read(dir.join(".blocks-done.json"))
.ok()
.and_then(|raw| serde_json::from_slice::<serde_json::Value>(&raw).ok())
.filter(|done| done["index_sha256"].as_str() == Some(set.index_sha256.as_str()))
.and_then(|done| done["blocks"].as_array().map(Vec::len));
match done {
Some(blocks) if blocks as u64 >= set.blocks => Presence::Installed,
_ if dir.is_dir() => Presence::Partial,
_ => Presence::Absent,
}
}
/// One item weighed against what the version says it holds.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Check {
pub key: String,
pub found: u64,
pub expected: u64,
/// True when at least everything the version names is on disk. MORE than that is normal
/// and not a fault: a PC bakes tiles of its own into the same folders, and the tile sync
/// brings in what other operators baked.
pub whole: bool,
}
/// What a tile set's folder really holds. Dot-files are the installer's own markers
/// (`.blocks-done.json`) and the tile server's negative ones (`.missing`), never tiles.
fn count_tiles(dir: &Path) -> u64 {
let Ok(entries) = std::fs::read_dir(dir) else { return 0 };
let mut found = 0;
for entry in entries.flatten() {
let name = entry.file_name();
if name.to_string_lossy().starts_with('.') {
continue;
}
match entry.file_type() {
Ok(kind) if kind.is_dir() => found += count_tiles(&entry.path()),
Ok(kind) if kind.is_file() => found += 1,
_ => {}
}
}
found
}
/// Count what is on disk for every tile set a marker calls installed.
///
/// This is the slow, true answer - a walk of hundreds of thousands of files - so it is
/// never on the path that opens the marketplace. The window asks for it after, and moves
/// the badge when it comes back.
pub fn check_installed(manifest: &Manifest, data_dir: &Path, extras: &BTreeSet<String>) -> Vec<Check> {
let mut out = Vec::new();
for set in manifest.tiles.iter().flat_map(|tiles| &tiles.sets) {
if tile_set_presence(data_dir, set) != Presence::Installed {
continue;
}
let found = count_tiles(&tile_set_dir(data_dir, set));
out.push(Check { key: tiles_key(set), found, expected: set.files, whole: found >= set.files });
}
let _ = extras;
out
}
/// A source file is complete when its `.ok` marker names its chunk list and the size is right
/// (the installer's own test); partial when the file exists.
fn src_presence(data_dir: &Path, file: &SrcFile) -> Presence {
let path = src_path(data_dir, file);
let marker_ok = std::fs::read_to_string(with_suffix(&path, ".ok")).is_ok_and(|text| text.trim() == file.chunks_sha256);
let size_ok = std::fs::metadata(&path).is_ok_and(|md| md.is_file() && md.len() == file.size);
match (marker_ok && size_ok, path.exists()) {
(true, _) => Presence::Installed,
(false, true) => Presence::Partial,
(false, false) => Presence::Absent,
}
}
/// Every tile set and source file of `manifest`, required ones first, as the marketplace shows
/// them. `extras` is the operator's choice.
pub fn items(manifest: &Manifest, data_dir: &Path, extras: &BTreeSet<String>) -> Vec<Item> {
items_from(manifest, data_dir, None, extras)
}
/// The same, for an install that copies another one: an item the source does not carry cannot
/// be offered, whatever the version's manifest lists. `presence` stays what THIS PC has.
pub fn items_from(
manifest: &Manifest,
data_dir: &Path,
source_data: Option<&Path>,
extras: &BTreeSet<String>,
) -> Vec<Item> {
let mut out = all_items(manifest, data_dir, extras);
if let Some(source) = source_data {
let carried: BTreeSet<String> = all_items(manifest, source, extras)
.into_iter()
.filter(|item| item.presence != Presence::Absent)
.map(|item| item.key)
.collect();
out.retain(|item| carried.contains(&item.key));
}
out
}
fn all_items(manifest: &Manifest, data_dir: &Path, extras: &BTreeSet<String>) -> Vec<Item> {
let old = pre_catalog(manifest);
let mut out = Vec::new();
for set in manifest.tiles.iter().flat_map(|tiles| &tiles.sets) {
let key = tiles_key(set);
let optional = tiles_optional(set, old);
let presence = tile_set_presence(data_dir, set);
out.push(Item {
selected: !optional || extras.contains(&key),
title: set.title.clone().unwrap_or_else(|| format!("{} map ({})", set.style, set.dataset)),
description: set.description.clone().unwrap_or_default(),
bytes: set.bytes,
files: set.files,
required: !optional,
presence,
on_dhp: set.on_dhp,
kind: Kind::Tiles,
key,
});
}
for file in manifest.src.iter().filter(|src| src.enabled).flat_map(|src| &src.files) {
let key = src_key(file);
let optional = src_optional(file, old);
let presence = src_presence(data_dir, file);
out.push(Item {
selected: !optional || extras.contains(&key),
title: file.title.clone().unwrap_or_else(|| file.path.clone()),
description: file.description.clone().unwrap_or_default(),
bytes: file.size,
// A source is one file (plus its small markers, not worth counting).
files: 1,
required: !optional,
presence,
on_dhp: file.on_dhp,
kind: Kind::Src,
key,
});
}
out.sort_by_key(|item| !item.required);
out
}
/// What installing `manifest` fetches: every required item and the chosen optional ones, as
/// the installer's `tile_sets` (`style/dataset`) and `src_paths` filters.
///
/// `from_dhp` drops what the store does not carry. The same choice is honoured when the
/// bytes come from a drive - that is where such an item comes from - but asked of the DHP
/// it would be a 404 per chunk and a failed install, for an item nobody could have got that
/// way. It is dropped here rather than refused at the checkbox because the choice outlives
/// the version it was made against: a PC that took the height map off a drive keeps it in
/// `extras`, and its next update from the DHP must simply leave it alone.
pub fn install_filter(
manifest: &Manifest,
extras: &BTreeSet<String>,
from_dhp: bool,
) -> (BTreeSet<String>, BTreeSet<String>) {
let old = pre_catalog(manifest);
let tile_sets = manifest
.tiles
.iter()
.flat_map(|tiles| &tiles.sets)
.filter(|set| set.on_dhp || !from_dhp)
.filter(|set| !tiles_optional(set, old) || extras.contains(&tiles_key(set)))
.map(|set| format!("{}/{}", set.style, set.dataset))
.collect();
let src_paths = manifest
.src
.iter()
.flat_map(|src| &src.files)
.filter(|file| file.on_dhp || !from_dhp)
.filter(|file| !src_optional(file, old) || extras.contains(&src_key(file)))
.map(|file| file.path.clone())
.collect();
(tile_sets, src_paths)
}
/// The optional items this PC already has, complete or not. A PC installed before the
/// marketplace has all of them: its first choice starts from what is on disk, so nothing is
/// removed or downloaded again behind the operator's back.
pub fn present_extras(manifest: &Manifest, data_dir: &Path) -> BTreeSet<String> {
items(manifest, data_dir, &BTreeSet::new())
.into_iter()
.filter(|item| !item.required && item.presence != Presence::Absent)
.map(|item| item.key)
.collect()
}
/// Keep only the keys of optional items `manifest` offers: a stale or required key means nothing.
pub fn optional_keys(manifest: &Manifest, keys: impl IntoIterator<Item = String>) -> BTreeSet<String> {
let offered: BTreeSet<String> =
items(manifest, Path::new(""), &BTreeSet::new()).into_iter().filter(|item| !item.required).map(|item| item.key).collect();
keys.into_iter().filter(|key| offered.contains(key)).collect()
}
/// The optional items on this PC the operator no longer wants.
pub fn to_remove(manifest: &Manifest, data_dir: &Path, extras: &BTreeSet<String>) -> Vec<Item> {
items(manifest, data_dir, extras)
.into_iter()
.filter(|item| !item.required && !item.selected && item.presence != Presence::Absent)
.collect()
}
/// What this PC has settled on: what the operator chose, plus whatever is actually here.
///
/// `extras` records answers about OPTIONAL items, so an item that is HERE and not in it was
/// never a choice the operator could have made - required by the version that installed it
/// (Dark, until Dark V2 took its place), off a drive, or not downloadable at all. Applying
/// `extras` alone as a selection therefore reads as "remove every one of those".
pub fn held_keys(manifest: &Manifest, data_dir: &Path, extras: &BTreeSet<String>) -> BTreeSet<String> {
items(manifest, data_dir, extras)
.into_iter()
.filter(|item| !item.required && (item.selected || item.presence != Presence::Absent))
.map(|item| item.key)
.collect()
}
/// The chosen items that are not all here, weighed against a count when one has been taken.
///
/// `to_add` answers from the installers' markers alone, which is what the marketplace opens
/// on. A set whose marker says "installed" while the disk holds half of it is exactly what
/// `check_installed` is for, so anything it found short is counted as unfinished here too.
pub fn unfinished(
manifest: &Manifest,
data_dir: &Path,
extras: &BTreeSet<String>,
checked: &[Check],
) -> Vec<Item> {
let short: BTreeSet<&str> = checked.iter().filter(|c| !c.whole).map(|c| c.key.as_str()).collect();
items(manifest, data_dir, extras)
.into_iter()
.filter(|item| !item.required && item.selected)
.filter(|item| item.presence != Presence::Installed || short.contains(item.key.as_str()))
.collect()
}
/// Whether the chosen optional items still need downloading.
pub fn to_add(manifest: &Manifest, data_dir: &Path, extras: &BTreeSet<String>) -> Vec<Item> {
items(manifest, data_dir, extras)
.into_iter()
// An item the store does not carry is never downloaded, so it is never announced as
// one: `install_filter` drops it, and a line saying it is being fetched would be a
// promise nothing keeps.
.filter(|item| item.on_dhp)
.filter(|item| !item.required && item.selected && item.presence != Presence::Installed)
.collect()
}
/// Remove an item's files from the PC: a tile set's folder, or a source file with its markers.
pub fn remove(manifest: &Manifest, data_dir: &Path, key: &str) -> Result<(), String> {
remove_counting(manifest, data_dir, key, &mut |_| {})
}
/// The same, calling `removed` with each file as it goes, so a caller can show progress.
pub fn remove_counting(
manifest: &Manifest,
data_dir: &Path,
key: &str,
removed: &mut dyn FnMut(u64),
) -> Result<(), String> {
for set in manifest.tiles.iter().flat_map(|tiles| &tiles.sets) {
if tiles_key(set) == key {
let dir = tile_set_dir(data_dir, set);
return if dir.exists() { crate::prune::remove_folder_counting(&dir, removed) } else { Ok(()) };
}
}
for file in manifest.src.iter().flat_map(|src| &src.files) {
if src_key(file) == key {
let path = src_path(data_dir, file);
for extra in ["", ".ok", ".chunks.json", ".part"] {
let target = with_suffix(&path, extra);
if target.exists() {
std::fs::remove_file(&target).map_err(|e| format!("cannot remove {}: {e}", target.display()))?;
if extra.is_empty() {
removed(1);
}
}
}
return Ok(());
}
}
Err(format!("{key} is not in this version"))
}
#[cfg(test)]
mod tests {
use super::*;
fn manifest() -> Manifest {
serde_json::from_value(serde_json::json!({
"schema": 1, "product": "odian", "version": "0.61.0", "released_at": "2026-09-22T12:00:00Z",
"app": {"files": []},
"tiles": {"block_shift": 6, "sets": [
{"style": "dark", "dataset": "ds", "journal_seq": 1, "blocks": 2, "files": 3, "bytes": 100,
"index": "tiles/dark.ds.index.json", "index_sha256": "a".repeat(64), "title": "Dark map"},
{"style": "dark_v2", "dataset": "ds", "journal_seq": 1, "blocks": 2, "files": 3, "bytes": 200,
"index": "tiles/dark_v2.ds.index.json", "index_sha256": "b".repeat(64), "optional": true,
"title": "Dark V2 map", "description": "Buildings."}
]},
"src": {"enabled": true, "chunk_bytes": 10, "files": [
{"path": "osm/x.pmtiles", "size": 4, "chunks": ["c".repeat(64)], "chunks_sha256": "d".repeat(64), "optional": true}
]}
}))
.unwrap()
}
/// A version from before the catalog: nothing in it is flagged.
fn old_manifest() -> Manifest {
let mut manifest = manifest();
manifest.version = "0.60.0".to_string();
for set in manifest.tiles.iter_mut().flat_map(|tiles| &mut tiles.sets) {
set.optional = false;
set.description = None;
}
for file in manifest.src.iter_mut().flat_map(|src| &mut src.files) {
file.optional = false;
}
manifest
}
fn data_dir() -> PathBuf {
std::env::temp_dir().join(format!("odian-market-test-{}", uuid::Uuid::new_v4().simple()))
}
fn complete_tile_set(data: &Path, style: &str, index: char, blocks: usize) {
let dir = data.join("tiles").join(style).join("ds");
std::fs::create_dir_all(&dir).unwrap();
let list: Vec<String> = (0..blocks).map(|i| format!("7/{i}/0:x")).collect();
let done = serde_json::json!({"index_sha256": index.to_string().repeat(64), "blocks": list});
std::fs::write(dir.join(".blocks-done.json"), done.to_string()).unwrap();
}
#[test]
fn required_items_come_first_and_the_choice_marks_the_rest() {
let data = data_dir();
let listed = items(&manifest(), &data, &["src:osm/x.pmtiles".to_string()].into_iter().collect());
let summary: Vec<(&str, bool, bool)> = listed.iter().map(|i| (i.key.as_str(), i.required, i.selected)).collect();
assert_eq!(summary, [("tiles:dark/ds", true, true), ("tiles:dark_v2/ds", false, false), ("src:osm/x.pmtiles", false, true)]);
assert_eq!((listed[1].title.as_str(), listed[1].description.as_str(), listed[1].bytes), ("Dark V2 map", "Buildings.", 200));
assert_eq!(listed[2].title, "osm/x.pmtiles", "no title: the path");
assert!(listed.iter().all(|i| i.presence == Presence::Absent));
}
#[test]
fn an_item_the_store_does_not_carry_is_listed_but_never_asked_of_it() {
// The world height map source is 705 GB and arrives on a drive. Leaving it out of the
// catalogue hid the one thing an operator needs to know - that it exists at all - and
// listing it without this flag would have the installer ask the DHP for 2629 chunks it
// has never had.
let mut carrying = manifest();
carrying.src.as_mut().unwrap().files.push(
serde_json::from_value(serde_json::json!({
"path": "planet_heightmap.pmtiles", "size": 8, "chunks": ["e".repeat(64)],
"chunks_sha256": "f".repeat(64), "optional": true, "on_dhp": false, "title": "Terrain source"
}))
.unwrap(),
);
let dir = std::env::temp_dir().join(format!("odian-market-{}", uuid::Uuid::new_v4().simple()));
let items = items(&carrying, &dir, &BTreeSet::new());
let terrain = items.iter().find(|item| item.key == "src:planet_heightmap.pmtiles").expect("it is listed");
assert!(!terrain.on_dhp, "and it says where it does NOT come from");
assert_eq!(terrain.title, "Terrain source");
assert!(items.iter().all(|item| item.key == "src:planet_heightmap.pmtiles" || item.on_dhp));
// Chosen - off a drive, say - it is still left alone when the DHP is the source.
let chosen: BTreeSet<String> = ["src:planet_heightmap.pmtiles".to_string()].into_iter().collect();
let (_, from_dhp) = install_filter(&carrying, &chosen, true);
assert!(!from_dhp.contains("planet_heightmap.pmtiles"), "the DHP is never asked for it");
let (_, from_drive) = install_filter(&carrying, &chosen, false);
assert!(from_drive.contains("planet_heightmap.pmtiles"), "a drive is exactly where it comes from");
}
#[test]
fn finishing_a_download_takes_nothing_away() {
// Dark V2 is installed and NOT in `extras`: it was required by the version that put it
// there, so no choice about it was ever recorded (which is exactly what happened when
// Dark V2 and Dark swapped places). Applying `extras` as a selection would read as
// "remove Dark V2", which is what Finish did before `held_keys`.
let data = data_dir();
complete_tile_set(&data, "dark_v2", 'b', 2);
let extras: BTreeSet<String> = ["src:osm/x.pmtiles".to_string()].into_iter().collect();
let keep = held_keys(&manifest(), &data, &extras);
let dropped = to_remove(&manifest(), &data, &keep);
std::fs::remove_dir_all(&data).unwrap();
assert!(keep.contains("tiles:dark_v2/ds"), "what is on disk stays chosen");
assert!(keep.contains("src:osm/x.pmtiles"), "and so does what was asked for");
assert!(dropped.is_empty(), "so finishing removes nothing: {dropped:?}");
}
#[test]
fn untick_still_means_remove_even_for_what_only_a_drive_replaces() {
// `selected` means "the operator asked for this", and `to_remove` finds what to delete
// by looking for present-and-not-selected. An item that always read as selected - which
// is what "installed counts as chosen" did to it - could never be removed at all.
let root = std::env::temp_dir().join(format!("odian-market-{}", uuid::Uuid::new_v4().simple()));
let src = root.join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("planet_heightmap.pmtiles"), b"12345678").unwrap();
std::fs::write(src.join("planet_heightmap.pmtiles.ok"), "f".repeat(64)).unwrap();
let mut carrying = manifest();
carrying.src.as_mut().unwrap().files.push(
serde_json::from_value(serde_json::json!({
"path": "planet_heightmap.pmtiles", "size": 8, "chunks": ["e".repeat(64)],
"chunks_sha256": "f".repeat(64), "optional": true, "on_dhp": false, "title": "Terrain source"
}))
.unwrap(),
);
let dropped = to_remove(&carrying, &root, &BTreeSet::new());
std::fs::remove_dir_all(&root).unwrap();
assert_eq!(
dropped.iter().map(|item| item.key.as_str()).collect::<Vec<_>>(),
["src:planet_heightmap.pmtiles"],
"unticked and on disk: it goes"
);
}
#[test]
fn an_install_fetches_the_required_items_and_the_chosen_ones() {
let (sets, paths) = install_filter(&manifest(), &BTreeSet::new(), true);
assert_eq!((sets.into_iter().collect::<Vec<_>>(), paths.len()), (vec!["dark/ds".to_string()], 0));
let (sets, paths) = install_filter(&manifest(), &["tiles:dark_v2/ds".to_string()].into_iter().collect(), true);
assert_eq!(sets.len(), 2);
assert!(paths.is_empty());
}
#[test]
fn presence_follows_the_installers_own_markers() {
let data = data_dir();
let m = manifest();
complete_tile_set(&data, "dark_v2", 'b', 2);
complete_tile_set(&data, "dark", 'z', 2); // an older index: it resumes, it is not done
let src = data.join("src").join("osm").join("x.pmtiles");
std::fs::create_dir_all(src.parent().unwrap()).unwrap();
std::fs::write(&src, b"PMTi").unwrap();
let presence = |key: &str| items(&m, &data, &BTreeSet::new()).into_iter().find(|i| i.key == key).unwrap().presence;
assert_eq!(presence("tiles:dark_v2/ds"), Presence::Installed);
assert_eq!(presence("tiles:dark/ds"), Presence::Partial);
assert_eq!(presence("src:osm/x.pmtiles"), Presence::Partial, "no .ok marker yet");
std::fs::write(with_suffix(&src, ".ok"), "d".repeat(64)).unwrap();
assert_eq!(presence("src:osm/x.pmtiles"), Presence::Installed);
// A PC from before the marketplace keeps what it has.
assert_eq!(present_extras(&m, &data).into_iter().collect::<Vec<_>>(), ["src:osm/x.pmtiles", "tiles:dark_v2/ds"]);
std::fs::remove_dir_all(&data).unwrap();
}
#[test]
fn unchosen_items_are_removed_and_chosen_missing_ones_added() {
let data = data_dir();
let m = manifest();
complete_tile_set(&data, "dark_v2", 'b', 2);
let src = data.join("src").join("osm").join("x.pmtiles");
let chosen: BTreeSet<String> = ["src:osm/x.pmtiles".to_string()].into_iter().collect();
let keys = |items: Vec<Item>| items.into_iter().map(|i| i.key).collect::<Vec<_>>();
assert_eq!(keys(to_remove(&m, &data, &chosen)), ["tiles:dark_v2/ds"]);
assert_eq!(keys(to_add(&m, &data, &chosen)), ["src:osm/x.pmtiles"]);
remove(&m, &data, "tiles:dark_v2/ds").unwrap();
assert!(!data.join("tiles").join("dark_v2").join("ds").exists());
std::fs::create_dir_all(src.parent().unwrap()).unwrap();
std::fs::write(&src, b"PMTi").unwrap();
std::fs::write(with_suffix(&src, ".chunks.json"), b"{}").unwrap();
remove(&m, &data, "src:osm/x.pmtiles").unwrap();
assert!(!src.exists() && !with_suffix(&src, ".chunks.json").exists());
assert!(remove(&m, &data, "tiles:nope/ds").is_err());
std::fs::remove_dir_all(&data).unwrap();
}
/// The marketplace a real published manifest gives a real install, after checking its
/// signature with the release key. Reads only. `ODIAN_MARKET_MANIFEST=<manifest.json>
/// ODIAN_MARKET_DATA=C:\ODIAN\data cargo test -p odian-launcher real_manifest -- --ignored --nocapture`
#[test]
#[ignore = "reads a real manifest named by ODIAN_MARKET_MANIFEST"]
fn show_the_marketplace_of_a_real_manifest() {
let raw = std::fs::read(std::env::var("ODIAN_MARKET_MANIFEST").unwrap()).unwrap();
let verified = odian_packs::VerifiedManifest::from_bytes(raw, odian_packs::bundled_public_key()).unwrap();
let data = std::env::var("ODIAN_MARKET_DATA").map(PathBuf::from).unwrap_or_default();
let manifest = verified.manifest();
for item in items(manifest, &data, &present_extras(manifest, &data)) {
println!(
"{:>7.1} GB {:<9} required={:<5} selected={:<5} {}",
item.bytes as f64 / 1e9,
format!("{:?}", item.presence),
item.required,
item.selected,
item.title
);
}
let (sets, paths) = install_filter(manifest, &BTreeSet::new(), true);
println!("a new PC installs: {sets:?} {paths:?}");
}
#[test]
fn only_offered_optional_keys_are_kept() {
let keys = ["tiles:dark/ds", "tiles:dark_v2/ds", "src:gone.pmtiles"].map(str::to_string);
assert_eq!(optional_keys(&manifest(), keys).into_iter().collect::<Vec<_>>(), ["tiles:dark_v2/ds"]);
}
#[test]
fn a_version_from_before_the_catalog_is_still_chosen_from() {
let items = items(&old_manifest(), Path::new(""), &BTreeSet::new());
let required: Vec<&str> = items.iter().filter(|item| item.required).map(|item| item.key.as_str()).collect();
// Only the map ODIAN opens on comes with every install; the rest is the operator's to pick.
assert_eq!(required, vec!["tiles:dark/ds"]);
let offered: Vec<&str> = items.iter().filter(|item| !item.required).map(|item| item.key.as_str()).collect();
assert_eq!(offered, vec!["tiles:dark_v2/ds", "src:osm/x.pmtiles"]);
// Unchosen, an install of it brings the dark map alone.
let (tiles, src) = install_filter(&old_manifest(), &BTreeSet::new(), true);
assert_eq!(tiles, BTreeSet::from(["dark/ds".to_string()]));
assert!(src.is_empty());
// Chosen, they come along.
let chosen = BTreeSet::from(["src:osm/x.pmtiles".to_string()]);
let (_, src) = install_filter(&old_manifest(), &chosen, true);
assert_eq!(src, BTreeSet::from(["osm/x.pmtiles".to_string()]));
}
#[test]
fn a_manifest_that_flags_anything_is_taken_as_it_stands() {
// The DHP decides for a catalog version: `dark_v2` is optional there, and nothing else moves.
let items = items(&manifest(), Path::new(""), &BTreeSet::new());
let required: Vec<&str> = items.iter().filter(|item| item.required).map(|item| item.key.as_str()).collect();
assert_eq!(required, vec!["tiles:dark/ds"]);
assert!(!pre_catalog(&manifest()) && pre_catalog(&old_manifest()));
}
}