From f512872e35f03d7869a6045da06c1d8b902e8d53 Mon Sep 17 00:00:00 2001 From: ExPikaPaka Date: Tue, 7 Jul 2026 08:48:32 +0200 Subject: [PATCH] Rvert from per-verndor to single cache file Replace N per-vendor .cache files with a single system_presets.cache that holds all vendors and presets in one serialized blob. Cache load is now all-or-nothing: on hit all vendors are applied from the bundle (sub-second); on miss all vendors are parsed from JSON and a fresh bundle is written to the user cache dir. Invalidation is driven by bundle_key - a sorted concatenation of all vendor JSON version strings. Any vendor update invalidates the whole cache and triggers re-parse on next launch. Guide wizard (WebGuideDialog) loads the bundled cache into a plain PresetBundle instead of a separate VendorGuideData struct, removing the duplicate data model. generate_system_cache simplified from a per-vendor loop to a single save_system_presets_cache() call producing one output file. --- src/dev-utils/generate_system_cache.cpp | 68 +--- src/libslic3r/PresetBundle.cpp | 448 +++++++++++------------ src/libslic3r/PresetBundle.hpp | 81 ++--- src/slic3r/GUI/WebGuideDialog.cpp | 144 ++++---- tests/libslic3r/test_vendor_cache.cpp | 461 +++++++++++------------- 5 files changed, 521 insertions(+), 681 deletions(-) diff --git a/src/dev-utils/generate_system_cache.cpp b/src/dev-utils/generate_system_cache.cpp index 42d79b85d5..e86a920c7b 100644 --- a/src/dev-utils/generate_system_cache.cpp +++ b/src/dev-utils/generate_system_cache.cpp @@ -45,12 +45,9 @@ int main(int argc, char* argv[]) } set_logging_level(log_level); - // In validation_mode, load_system_presets_from_json uses data_dir() directly - // (no /system/ suffix), so point data_dir at the profiles directory. set_data_dir(profiles_path); set_resources_dir(fs::path(profiles_path).parent_path().make_preferred().string()); - // load_presets creates user preset dirs under data_dir(). const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR; if (!fs::exists(user_dir)) fs::create_directories(user_dir); @@ -71,56 +68,21 @@ int main(int argc, char* argv[]) return 1; } - // Collect all vendor names from JSON files in the profiles directory. - std::vector vendor_names; - for (const auto& e : fs::directory_iterator(profiles_path)) { - if (e.path().extension() == ".json") - vendor_names.push_back(e.path().stem().string()); + const std::string output_path = + (fs::path(profiles_path) / "system_presets.cache").make_preferred().string(); + + std::cout << "Saving single-bundle cache to: " << output_path << "\n"; + + const auto stats = preset_bundle->save_system_presets_cache(profiles_path, output_path); + + if (!stats.ok) { + std::cerr << "ERROR: verification failed\n"; + return 1; } - // Sort: PresetBundle::ORCA_FILAMENT_LIBRARY first, rest alphabetical. - std::sort(vendor_names.begin(), vendor_names.end(), - [](const std::string& a, const std::string& b) { - if (a == PresetBundle::ORCA_FILAMENT_LIBRARY) return true; - if (b == PresetBundle::ORCA_FILAMENT_LIBRARY) return false; - return a < b; - }); - - size_t total_print = 0, total_filament = 0, total_printer = 0; - int saved = 0, failed = 0; - - for (const auto& vendor_name : vendor_names) { - try { - const std::string json_path = (fs::path(profiles_path) / (vendor_name + ".json")).string(); - const std::string cache_path = (fs::path(profiles_path) / (vendor_name + ".cache")).make_preferred().string(); - const bool is_orca_lib = (vendor_name == PresetBundle::ORCA_FILAMENT_LIBRARY); - - const auto stats = preset_bundle->save_bundled_vendor_cache(vendor_name, json_path, is_orca_lib, cache_path); - - if (!stats.ok) { - std::cerr << "ERROR: " << vendor_name << ": verification failed\n"; - ++failed; - } else { - std::cout << " [ok] " << vendor_name << ".cache" - << " (" << stats.print_presets << " print, " - << stats.filament_presets << " filament, " - << stats.printer_presets << " printer)\n"; - total_print += stats.print_presets; - total_filament += stats.filament_presets; - total_printer += stats.printer_presets; - ++saved; - } - } catch (const std::exception& ex) { - std::cerr << "ERROR: " << vendor_name << ": " << ex.what() << "\n"; - ++failed; - } - } - - std::cout << "\nDone: " << saved << " cache(s) written"; - if (failed) std::cout << ", " << failed << " FAILED"; - std::cout << "\n" - << " Total print presets: " << total_print << "\n" - << " Total filament presets: " << total_filament << "\n" - << " Total printer presets: " << total_printer << "\n"; - return failed ? 1 : 0; + std::cout << "[ok] system_presets.cache\n" + << " Total print presets: " << stats.print_presets << "\n" + << " Total filament presets: " << stats.filament_presets << "\n" + << " Total printer presets: " << stats.printer_presets << "\n"; + return 0; } diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 03f2b595f8..6e1c065e89 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -2220,71 +2220,26 @@ std::pair PresetBundle::load_system_pre if (validation_mode) dir = (boost::filesystem::path(data_dir())).make_preferred(); - // Per-vendor binary cache: try user cache, then bundled cache, then JSON parse. - // Vendors that hit a cache are applied immediately; misses go through JSON parsing below. - std::set cache_miss_vendors; - std::map vendor_json_versions; // vendor_id → version string on disk - + // Single-bundle cache: try user cache, then bundled cache, then JSON parse. + bool loaded_from_cache = false; if (!validation_mode) { const auto t0 = std::chrono::steady_clock::now(); - this->reset(false); - bool all_from_cache = true; - - // Collect all vendor names from the system directory. - std::vector all_vendor_names; - try { - for (const auto& e : boost::filesystem::directory_iterator(dir)) { - if (Slic3r::is_json_file(e.path().string())) - all_vendor_names.push_back(e.path().stem().string()); - } - } catch (const std::exception& ex) { - BOOST_LOG_TRIVIAL(warning) << "PresetBundle: cannot scan system dir: " << ex.what(); - } - - // Load ORCA_FILAMENT_LIBRARY first (other vendors' filaments inherit from it). - // Then load remaining vendors from per-vendor caches. - auto try_vendor_cache = [&](const std::string& vendor_name) -> bool { - const std::string json_path = (dir / (vendor_name + ".json")).string(); - const std::string ver_str = get_vendor_cache_key(json_path); - vendor_json_versions[vendor_name] = ver_str; - return try_load_and_apply_vendor_cache(vendor_name, ver_str); - }; - - // Sort: ORCA_FILAMENT_LIBRARY goes first, rest alphabetical. - std::sort(all_vendor_names.begin(), all_vendor_names.end(), - [](const std::string& a, const std::string& b) { - if (a == ORCA_FILAMENT_LIBRARY) return true; - if (b == ORCA_FILAMENT_LIBRARY) return false; - return a < b; - }); - - for (const auto& name : all_vendor_names) { - if (!try_vendor_cache(name)) { - cache_miss_vendors.insert(name); - all_from_cache = false; - } - } - - if (all_from_cache && !all_vendor_names.empty()) { + const std::string expected_key = compute_system_presets_cache_key(dir.string()); + if (try_load_system_presets_from_cache(expected_key)) { update_system_maps(); const auto ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - t0).count(); - BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from per-vendor cache in " << ms << " ms"; + BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from single-bundle cache in " << ms << " ms"; return {PresetsConfigSubstitutions{}, ""}; } - if (!cache_miss_vendors.empty()) - BOOST_LOG_TRIVIAL(info) << "PresetBundle: " << cache_miss_vendors.size() - << " vendor(s) need JSON parse: " - << [&]{ std::string s; for (auto& v : cache_miss_vendors) s += v + " "; return s; }(); + loaded_from_cache = false; // cache miss — fall through to JSON parse } const auto json_load_t0 = std::chrono::steady_clock::now(); PresetsConfigSubstitutions substitutions; std::string errors_cummulative; std::set errored_vendors; // vendors whose JSON parse failed — skip their cache save - // first = true means no vendor has been loaded yet (from cache or JSON). - // false = at least one vendor was applied from the per-vendor cache above. - bool first = validation_mode || cache_miss_vendors.size() == vendor_json_versions.size(); + bool first = true; std::vector vendor_names; // store all vendor names in vendor_names for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) { @@ -2306,9 +2261,6 @@ std::pair PresetBundle::load_system_pre std::vector other_vendors; other_vendors.reserve(vendor_names.size()); for (auto& vn : vendor_names) { - // Skip vendors already loaded from the per-vendor cache. - if (!validation_mode && !cache_miss_vendors.count(vn)) - continue; if (vn == ORCA_FILAMENT_LIBRARY) orca_lib_vendor = vn; else if (!(validation_mode && !vendor_to_validate.empty() && vn != vendor_to_validate)) @@ -2397,22 +2349,19 @@ std::pair PresetBundle::load_system_pre BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from JSON in " << json_ms << " ms"; } - // Save per-vendor binary caches for vendors that parsed successfully. - // Vendors whose JSON produced a parse error are skipped individually so one - // bad vendor does not block caching of all others. - if (!validation_mode && !cache_miss_vendors.empty()) { + // Save single-bundle cache after successful JSON parse. + if (!validation_mode && errored_vendors.empty()) { const auto save_t0 = std::chrono::steady_clock::now(); - for (const auto& vendor_name : cache_miss_vendors) { - if (errored_vendors.count(vendor_name)) - continue; - const bool is_orca_lib = (vendor_name == ORCA_FILAMENT_LIBRARY); - const std::string ver_str = vendor_json_versions.count(vendor_name) - ? vendor_json_versions.at(vendor_name) : ""; - write_vendor_cache(vendor_cache_user_path(vendor_name), vendor_name, ver_str, is_orca_lib); - } + const auto stats = save_system_presets_cache(dir.string(), user_system_presets_cache_path()); const auto save_ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - save_t0).count(); - BOOST_LOG_TRIVIAL(info) << "PresetBundle: per-vendor caches saved in " << save_ms << " ms"; + if (stats.ok) + BOOST_LOG_TRIVIAL(info) << "PresetBundle: single-bundle cache saved in " << save_ms << " ms" + << " (print=" << stats.print_presets + << " filament=" << stats.filament_presets + << " printer=" << stats.printer_presets << ")"; + else + BOOST_LOG_TRIVIAL(warning) << "PresetBundle: single-bundle cache save failed"; } //BBS: add config related logs @@ -5904,10 +5853,7 @@ bool BundleMetadata::save_to_json(const std::string& path) const return false; } } -// ---- VendorCache implementation (PresetBundle methods) ------------------ -// The VendorCacheHeader struct holds only metadata. Preset collections are -// serialized directly from/into PresetBundle's own prints/filaments/printers/… -// to avoid ever duplicating those vectors in memory. +// ---- System presets single-bundle cache implementation ------------------ namespace { @@ -5921,23 +5867,77 @@ struct CacheFileHeader { #pragma pack(pop) static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes"); +// Presets for one vendor, grouped so vendor pointers can be re-wired on load. +struct VendorPresetGroup { + std::vector prints, filaments, printers, sla_prints, sla_materials; + template + void serialize(Archive& ar) { ar(prints, filaments, printers, sla_prints, sla_materials); } +}; + +struct SystemPresetsCache { + static constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ" + static constexpr uint32_t CACHE_VERSION = 1; + + uint32_t cache_version = CACHE_VERSION; + uint32_t config_options_count = 0; + std::string bundle_key; + + VendorMap vendors; + std::map preset_groups; + std::map config_maps; + std::map filament_id_maps; + + template + void serialize(Archive& ar) + { + ar(cache_version, config_options_count, bundle_key, + vendors, preset_groups, config_maps, filament_id_maps); + } + + bool is_valid(const std::string& expected_key) const + { + return cache_version == CACHE_VERSION + && config_options_count == static_cast(print_config_def.options.size()) + && bundle_key == expected_key; + } +}; + } // anonymous namespace // static -std::string PresetBundle::vendor_cache_user_path(const std::string& vendor_id) +std::string PresetBundle::bundled_system_presets_cache_path() { - return (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / (vendor_id + ".cache")) + return (boost::filesystem::path(resources_dir()) / "profiles" / "system_presets.cache") .make_preferred().string(); } // static -std::string PresetBundle::vendor_cache_bundled_path(const std::string& vendor_id) +std::string PresetBundle::user_system_presets_cache_path() { - return (boost::filesystem::path(resources_dir()) / "profiles" / (vendor_id + ".cache")) + return (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / "system_presets.cache") .make_preferred().string(); } -// static — raw file I/O + magic/version/CRC check, shared by all cache readers. +// static +std::string PresetBundle::compute_system_presets_cache_key(const std::string& system_dir) +{ + // Sorted map so key is stable regardless of directory iteration order. + std::map keys; + try { + for (const auto& e : boost::filesystem::directory_iterator(system_dir)) { + if (Slic3r::is_json_file(e.path().string())) + keys[e.path().stem().string()] = get_vendor_cache_key(e.path().string()); + } + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: cannot scan " << system_dir << ": " << ex.what(); + } + std::string combined; + for (const auto& [name, key] : keys) + combined += name + ":" + key + ";"; + return combined; +} + +// static bool PresetBundle::read_cache_blob(const std::string& path, std::string& out_blob) { try { @@ -5947,7 +5947,7 @@ bool PresetBundle::read_cache_blob(const std::string& path, std::string& out_blo CacheFileHeader fhdr; if (!ifs.read(reinterpret_cast(&fhdr), sizeof(fhdr))) return false; - if (fhdr.magic != VendorCacheHeader::CACHE_MAGIC || fhdr.version != VendorCacheHeader::CACHE_VERSION) + if (fhdr.magic != SystemPresetsCache::CACHE_MAGIC) return false; if (fhdr.data_size == 0 || fhdr.data_size > 512u * 1024u * 1024u) return false; @@ -5957,197 +5957,177 @@ bool PresetBundle::read_cache_blob(const std::string& path, std::string& out_blo boost::crc_32_type crc; crc.process_bytes(out_blob.data(), out_blob.size()); if (crc.checksum() != fhdr.crc32) { - BOOST_LOG_TRIVIAL(warning) << "VendorCache: CRC mismatch: " << path; + BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: CRC mismatch: " << path; return false; } return true; } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "VendorCache: read failed (" << path << "): " << e.what(); + BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: read failed (" << path << "): " << e.what(); return false; } } -// Serialize PresetBundle's preset collections for vendor_id directly to file. -// Uses cereal's size-tag protocol so the binary layout matches what a -// std::vector archive would produce — no intermediate vector copy. -void PresetBundle::write_vendor_cache(const std::string& path, - const std::string& vendor_id, - const std::string& json_ver, - bool capture_filament_maps) const +// static +void PresetBundle::write_cache_blob(const std::string& path, const std::string& blob) { - std::ostringstream oss; - { - cereal::BinaryOutputArchive ar(oss); - - VendorCacheHeader hdr; - hdr.cache_version = VendorCacheHeader::CACHE_VERSION; - hdr.config_options_count = static_cast(print_config_def.options.size()); - hdr.vendor_json_version = json_ver; - auto vp_it = vendors.find(vendor_id); - if (vp_it != vendors.end()) - hdr.profile = vp_it->second; - ar(hdr); - - // Write each collection filtered to vendor_id without copying into a temp vector. - auto write_col = [&](const PresetCollection& coll) { - std::vector ptrs; - for (const Preset& p : coll()) - if (p.is_system && p.vendor && p.vendor->id == vendor_id) - ptrs.push_back(&p); - ar(cereal::make_size_tag(static_cast(ptrs.size()))); - for (const Preset* p : ptrs) - ar(*p); - }; - write_col(prints); - write_col(filaments); - write_col(printers); - write_col(sla_prints); - write_col(sla_materials); - - if (capture_filament_maps) { - ar(m_config_maps, m_filament_id_maps); - } else { - const std::map empty_cm; - const std::map empty_fi; - ar(empty_cm, empty_fi); - } - } - const std::string blob = oss.str(); boost::crc_32_type crc; crc.process_bytes(blob.data(), blob.size()); try { boost::filesystem::create_directories(boost::filesystem::path(path).parent_path()); boost::nowide::ofstream ofs(path, std::ios::binary | std::ios::trunc); if (!ofs.is_open()) { - BOOST_LOG_TRIVIAL(warning) << "VendorCache: cannot open for writing: " << path; + BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: cannot open for writing: " << path; return; } CacheFileHeader fhdr; - fhdr.magic = VendorCacheHeader::CACHE_MAGIC; - fhdr.version = VendorCacheHeader::CACHE_VERSION; + fhdr.magic = SystemPresetsCache::CACHE_MAGIC; + fhdr.version = SystemPresetsCache::CACHE_VERSION; fhdr.data_size = static_cast(blob.size()); fhdr.crc32 = crc.checksum(); ofs.write(reinterpret_cast(&fhdr), sizeof(fhdr)); ofs.write(blob.data(), static_cast(blob.size())); } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "VendorCache: write failed (" << path << "): " << e.what(); + BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: write failed (" << path << "): " << e.what(); } } -// Read a cache file and — if valid — apply its presets directly into PresetBundle's -// collections. Deserializes all 5 collections into temp vectors first so that -// PresetBundle is not modified if deserialization fails partway through. -bool PresetBundle::load_and_apply_vendor_cache(const std::string& path, const std::string& json_ver) +// Apply one VendorPresetGroup from a loaded cache into this PresetBundle's collections. +static void apply_vendor_preset_group(VendorPresetGroup& grp, + const VendorProfile* vp, + PresetCollection& prints_coll, + PresetCollection& filaments_coll, + PrinterPresetCollection& printers_coll, + PresetCollection& sla_prints_coll, + PresetCollection& sla_materials_coll) { - std::string blob; - if (!read_cache_blob(path, blob)) - return false; - try { - std::istringstream iss(blob); - cereal::BinaryInputArchive ar(iss); - - VendorCacheHeader hdr; - ar(hdr); - if (!hdr.is_valid(json_ver)) - return false; - - // Deserialize all collections before touching PresetBundle (rollback safety). - std::vector col_print, col_filament, col_printer, col_sla_print, col_sla_material; - std::map config_maps; - std::map filament_id_maps; - ar(col_print, col_filament, col_printer, col_sla_print, col_sla_material); - ar(config_maps, filament_id_maps); - - // Full deserialization succeeded — apply to PresetBundle. - vendors.emplace(hdr.profile.id, hdr.profile); - const VendorProfile* vp = &vendors.at(hdr.profile.id); - - auto apply_col = [&](std::vector& cached, PresetCollection& coll, bool is_filaments) { - for (Preset& cp : cached) { - DynamicPrintConfig config = cp.config; - Preset& p = coll.load_preset(cp.file, cp.name, std::move(config), /*select=*/false, cp.version); - p.is_system = true; - p.is_visible = cp.is_visible; - p.alias = cp.alias; - p.renamed_from = cp.renamed_from; - p.filament_id = cp.filament_id; - p.setting_id = cp.setting_id; - p.description = cp.description; - p.m_from_orca_filament_lib = cp.m_from_orca_filament_lib; - p.m_excluded_from = cp.m_excluded_from; - p.vendor = vp; - if (is_filaments) - coll.set_printer_hold_alias(p.alias, p); - } - }; - apply_col(col_print, prints, false); - apply_col(col_filament, filaments, true); - apply_col(col_printer, printers, false); - apply_col(col_sla_print, sla_prints, false); - apply_col(col_sla_material, sla_materials, false); - - if (!config_maps.empty()) { - m_config_maps = std::move(config_maps); - m_filament_id_maps = std::move(filament_id_maps); + auto apply = [&](std::vector& cached, PresetCollection& coll, bool is_filaments) { + for (Preset& cp : cached) { + DynamicPrintConfig config = cp.config; + Preset& p = coll.load_preset(cp.file, cp.name, std::move(config), false, cp.version); + p.is_system = true; + p.is_visible = cp.is_visible; + p.alias = cp.alias; + p.renamed_from = cp.renamed_from; + p.filament_id = cp.filament_id; + p.setting_id = cp.setting_id; + p.description = cp.description; + p.m_from_orca_filament_lib = cp.m_from_orca_filament_lib; + p.m_excluded_from = cp.m_excluded_from; + p.vendor = vp; + if (is_filaments) + coll.set_printer_hold_alias(p.alias, p); } - return true; - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "VendorCache: load failed (" << path << "): " << e.what(); - return false; - } + }; + apply(grp.prints, prints_coll, false); + apply(grp.filaments, filaments_coll, true); + apply(grp.printers, printers_coll, false); + apply(grp.sla_prints, sla_prints_coll, false); + apply(grp.sla_materials, sla_materials_coll, false); } -bool PresetBundle::try_load_and_apply_vendor_cache(const std::string& vendor_id, - const std::string& json_ver) +bool PresetBundle::try_load_system_presets_from_cache(const std::string& expected_key) { - if (load_and_apply_vendor_cache(vendor_cache_user_path(vendor_id), json_ver)) + auto try_path = [&](const std::string& path) -> bool { + std::string blob; + if (!read_cache_blob(path, blob)) + return false; + try { + std::istringstream iss(blob); + cereal::BinaryInputArchive ar(iss); + SystemPresetsCache cache; + ar(cache); + if (!cache.is_valid(expected_key)) + return false; + + this->reset(false); + vendors = std::move(cache.vendors); + + for (auto& [vendor_id, grp] : cache.preset_groups) { + auto it = vendors.find(vendor_id); + if (it == vendors.end()) continue; + apply_vendor_preset_group(grp, &it->second, + prints, filaments, printers, + sla_prints, sla_materials); + } + m_config_maps = std::move(cache.config_maps); + m_filament_id_maps = std::move(cache.filament_id_maps); + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: apply failed (" << path << "): " << e.what(); + return false; + } + }; + + if (try_path(user_system_presets_cache_path())) return true; - if (load_and_apply_vendor_cache(vendor_cache_bundled_path(vendor_id), json_ver)) { - // Promote bundled cache to user cache so future loads skip this path. - write_vendor_cache(vendor_cache_user_path(vendor_id), vendor_id, json_ver, - vendor_id == ORCA_FILAMENT_LIBRARY); + if (try_path(bundled_system_presets_cache_path())) { + // Promote bundled cache to user path so future loads skip JSON parse. + const std::string user_path = user_system_presets_cache_path(); + const std::string bundled_path = bundled_system_presets_cache_path(); + try { + boost::filesystem::create_directories( + boost::filesystem::path(user_path).parent_path()); + boost::filesystem::copy_file(bundled_path, user_path, + boost::filesystem::copy_options::overwrite_existing); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: promote failed: " << e.what(); + } return true; } return false; } -PresetBundle::VendorCacheStats PresetBundle::save_bundled_vendor_cache( - const std::string& vendor_id, const std::string& json_path, - bool capture_filament_maps, const std::string& output_path) const +PresetBundle::SaveCacheResult PresetBundle::save_system_presets_cache( + const std::string& profiles_dir, const std::string& output_path) const { - const std::string json_ver = get_vendor_cache_key(json_path); - write_vendor_cache(output_path, vendor_id, json_ver, capture_filament_maps); + SystemPresetsCache cache; + cache.config_options_count = static_cast(print_config_def.options.size()); + cache.bundle_key = compute_system_presets_cache_key(profiles_dir); + cache.vendors = vendors; + cache.config_maps = m_config_maps; + cache.filament_id_maps = m_filament_id_maps; - VendorCacheStats stats; - auto count_col = [&](const PresetCollection& coll) -> size_t { - size_t n = 0; + auto fill = [&](const PresetCollection& coll, + std::vector VendorPresetGroup::*field) { for (const Preset& p : coll()) - if (p.is_system && p.vendor && p.vendor->id == vendor_id) ++n; - return n; + if (p.is_system && p.vendor) + (cache.preset_groups[p.vendor->id].*field).push_back(p); }; - stats.print_presets = count_col(prints); - stats.filament_presets = count_col(filaments); - stats.printer_presets = count_col(printers); + fill(prints, &VendorPresetGroup::prints); + fill(filaments, &VendorPresetGroup::filaments); + fill(sla_prints, &VendorPresetGroup::sla_prints); + fill(sla_materials, &VendorPresetGroup::sla_materials); + // PrinterPresetCollection is a PresetCollection subclass; iterate directly. + for (const Preset& p : printers()) + if (p.is_system && p.vendor) + cache.preset_groups[p.vendor->id].printers.push_back(p); - // Verify: read back header and check validity. - if (std::string blob; read_cache_blob(output_path, blob)) { + std::ostringstream oss; + { cereal::BinaryOutputArchive ar(oss); ar(cache); } + const std::string blob = oss.str(); + write_cache_blob(output_path, blob); + + SaveCacheResult result; + // Verify: read back and check validity. + if (std::string vblob; read_cache_blob(output_path, vblob)) { try { - std::istringstream iss(blob); + std::istringstream iss(vblob); cereal::BinaryInputArchive ar(iss); - VendorCacheHeader hdr; ar(hdr); - stats.ok = hdr.is_valid(json_ver); + SystemPresetsCache v; ar(v); + result.ok = v.is_valid(cache.bundle_key); } catch (...) {} } - return stats; + for (const auto& [vid, grp] : cache.preset_groups) { + result.print_presets += grp.prints.size(); + result.filament_presets += grp.filaments.size(); + result.printer_presets += grp.printers.size(); + } + return result; } // static -bool PresetBundle::load_vendor_cache_for_guide(const std::string& cache_path, - const std::string& json_ver, - VendorProfile& out_profile, - std::vector& out_printer_presets, - std::vector& out_filament_presets, - std::vector& out_print_presets) +bool PresetBundle::load_system_presets_cache_for_guide(const std::string& cache_path, + PresetBundle& out_bundle) { std::string blob; if (!read_cache_blob(cache_path, blob)) @@ -6155,19 +6135,25 @@ bool PresetBundle::load_vendor_cache_for_guide(const std::string& cache_path, try { std::istringstream iss(blob); cereal::BinaryInputArchive ar(iss); - VendorCacheHeader hdr; ar(hdr); - if (!hdr.is_valid(json_ver)) + SystemPresetsCache cache; + ar(cache); + if (cache.cache_version != SystemPresetsCache::CACHE_VERSION) return false; - out_profile = std::move(hdr.profile); - // Must read in serialization order: print, filament, printer, sla_print, sla_material. - std::vector col_sla_print, col_sla_material; - std::map cm; - std::map fi; - ar(out_print_presets, out_filament_presets, out_printer_presets, col_sla_print, col_sla_material); - ar(cm, fi); - return true; + + out_bundle.vendors = std::move(cache.vendors); + for (auto& [vendor_id, grp] : cache.preset_groups) { + auto it = out_bundle.vendors.find(vendor_id); + if (it == out_bundle.vendors.end()) continue; + apply_vendor_preset_group(grp, &it->second, + out_bundle.prints, + out_bundle.filaments, + out_bundle.printers, + out_bundle.sla_prints, + out_bundle.sla_materials); + } + return !out_bundle.vendors.empty(); } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "VendorCache: guide load failed (" << cache_path << "): " << e.what(); + BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: guide load failed (" << cache_path << "): " << e.what(); return false; } } diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 5488c4264f..2a035c3548 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -152,38 +152,31 @@ struct PresetBundleMetadata class PresetBundle { public: - // ---- Per-vendor binary preset cache ------------------------------------ - // One file per vendor: - // Bundled (CI-generated): resources/profiles/.cache - // User (runtime): data_dir/system/.cache + // ---- System presets single-bundle cache -------------------------------- + // All vendor profiles + all system presets in one file. + // Bundled (CI): resources/profiles/system_presets.cache + // User runtime: data_dir/system/system_presets.cache - struct VendorCacheStats { + struct SaveCacheResult { bool ok = false; size_t print_presets = 0; size_t filament_presets = 0; size_t printer_presets = 0; }; - static std::string vendor_cache_user_path(const std::string& vendor_id); - static std::string vendor_cache_bundled_path(const std::string& vendor_id); + static std::string bundled_system_presets_cache_path(); + static std::string user_system_presets_cache_path(); - // Capture the given vendor from the loaded bundle, write to output_path, - // and verify the result. Used by generate_system_cache and tests. - // json_path is the vendor's .json profile file; its version/mtime is computed internally. - VendorCacheStats save_bundled_vendor_cache(const std::string& vendor_id, - const std::string& json_path, - bool capture_filament_maps, - const std::string& output_path) const; + // Capture the currently-loaded PresetBundle and write a single-bundle cache + // to output_path. profiles_dir contains the vendor JSON files (for the key). + // Used by generate_system_cache and tests. + SaveCacheResult save_system_presets_cache(const std::string& profiles_dir, + const std::string& output_path) const; - // Load a vendor cache file and extract the data needed by the guide wizard. - // json_ver must match the cache's stored version key (use get_vendor_cache_key()). - // Returns false if the file is missing, stale, or corrupt. - static bool load_vendor_cache_for_guide(const std::string& cache_path, - const std::string& json_ver, - VendorProfile& out_profile, - std::vector& out_printer_presets, - std::vector& out_filament_presets, - std::vector& out_print_presets); + // Load a single-bundle cache into out_bundle for the guide wizard. + // Returns false if the cache is missing, stale, or corrupt. + static bool load_system_presets_cache_for_guide(const std::string& cache_path, + PresetBundle& out_bundle); static DynamicPrintConfig construct_full_config(Preset &in_printer_preset, Preset &in_print_preset, @@ -527,42 +520,14 @@ public: bool check_preset_references() const; private: - // ---- VendorCache — header-only metadata struct -------------------------- - // Preset collections are NOT stored here; they are serialized directly from - // PresetBundle's own prints/filaments/printers/… on save, and applied directly - // into those collections on load. This avoids duplicating the preset vectors. - struct VendorCacheHeader { - static constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ" - static constexpr uint32_t CACHE_VERSION = 8; - - uint32_t cache_version = CACHE_VERSION; - uint32_t config_options_count = 0; - std::string vendor_json_version; - VendorProfile profile; - - template - void serialize(Archive& ar) - { - ar(cache_version, config_options_count, vendor_json_version, profile); - } - - bool is_valid(const std::string& current_vendor_json_version) const - { - return cache_version == CACHE_VERSION - && config_options_count == static_cast(print_config_def.options.size()) - && vendor_json_version == current_vendor_json_version; - } - }; - - // Returns true and applies the cache if a valid file exists for vendor_id. - bool try_load_and_apply_vendor_cache(const std::string& vendor_id, const std::string& json_ver); - // Read a cache file, verify it, and apply its presets directly into PresetBundle's collections. - bool load_and_apply_vendor_cache(const std::string& path, const std::string& json_ver); - // Write PresetBundle's presets for vendor_id directly to a cache file — no intermediate copy. - void write_vendor_cache(const std::string& path, const std::string& vendor_id, - const std::string& json_ver, bool capture_filament_maps) const; - // Read raw blob bytes (file I/O + magic/CRC check) shared by all cache readers. + // Compute combined invalidation key from all vendor JSON files in system_dir. + static std::string compute_system_presets_cache_key(const std::string& system_dir); + // Try user cache then bundled cache; apply all presets on hit. + bool try_load_system_presets_from_cache(const std::string& expected_key); + // Read raw cache blob: verify magic, size, CRC. static bool read_cache_blob(const std::string& path, std::string& out_blob); + // Write a cache blob with the standard 20-byte file header. + static void write_cache_blob(const std::string& path, const std::string& blob); // Orca: validation only - flag any printer with two or more compatible // filament presets sharing one filament_id (ambiguous AMS subtype match). diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp index 389938aa28..7aa52a8b1c 100644 --- a/src/slic3r/GUI/WebGuideDialog.cpp +++ b/src/slic3r/GUI/WebGuideDialog.cpp @@ -1397,42 +1397,21 @@ bool GuideFrame::BuildProfileDataFromPresetBundle() } // Builds guide profile JSON from the per-vendor bundled caches -// (resources/profiles/.cache, generated by CI). +// (resources/profiles/system_presets.cache, generated by CI). // This avoids the 90-second LoadProfileFamily fallback on first launch. bool GuideFrame::BuildProfileDataFromBundledCache() { - // Enumerate per-vendor .cache files in resources/profiles/. - std::vector cache_files; - try { - for (const auto& e : boost::filesystem::directory_iterator(rsrc_vendor_dir)) { - if (e.path().extension() == ".cache") - cache_files.push_back(e.path()); - } - } catch (const std::exception& ex) { - BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromBundledCache: cannot scan " << rsrc_vendor_dir << ": " << ex.what(); - return false; - } - if (cache_files.empty()) + const std::string cache_path = PresetBundle::bundled_system_presets_cache_path(); + if (!boost::filesystem::exists(cache_path)) return false; try { - for (const auto& cache_path : cache_files) { - const std::string vendor_id = cache_path.stem().string(); - // Validate against the version in the corresponding vendor JSON. - const boost::filesystem::path json_path = rsrc_vendor_dir / (vendor_id + ".json"); - const std::string ver_str = get_vendor_cache_key(json_path.string()); + PresetBundle bundle; + if (!PresetBundle::load_system_presets_cache_for_guide(cache_path, bundle)) + return false; - VendorProfile vc_profile; - std::vector vc_printer_presets; - std::vector vc_filament_presets; - std::vector vc_print_presets; - if (!PresetBundle::load_vendor_cache_for_guide(cache_path.string(), ver_str, - vc_profile, vc_printer_presets, - vc_filament_presets, vc_print_presets)) - continue; - - // Models from this vendor's cached profile - for (const auto& cm : vc_profile.models) { + for (const auto& [vendor_id, vp] : bundle.vendors) { + for (const auto& cm : vp.models) { std::string nozzle_str; for (const auto& v : cm.variants) { if (!nozzle_str.empty()) nozzle_str += ";"; @@ -1444,7 +1423,7 @@ bool GuideFrame::BuildProfileDataFromBundledCache() materials_str += m; } boost::filesystem::path cover_path = - (boost::filesystem::path(resources_dir()) / "profiles" / vc_profile.id / (cm.id + "_cover.png")) + (boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (cm.id + "_cover.png")) .make_preferred(); if (!boost::filesystem::exists(cover_path)) cover_path = @@ -1454,7 +1433,7 @@ bool GuideFrame::BuildProfileDataFromBundledCache() json entry; entry["model"] = cm.id; entry["name"] = cm.name; - entry["vendor"] = vc_profile.id; + entry["vendor"] = vp.id; entry["nozzle_diameter"] = nozzle_str; entry["materials"] = materials_str; entry["cover"] = cover_path.string(); @@ -1462,60 +1441,59 @@ bool GuideFrame::BuildProfileDataFromBundledCache() entry["sub_path"] = ""; m_ProfileJson["model"].push_back(entry); } - - // Machines from cached printer presets - for (const auto& cp : vc_printer_presets) { - const auto* pm = cp.config.option("printer_model"); - const auto* pv = cp.config.option("printer_variant"); - if (!pm || pm->value.empty() || !pv) continue; - - json mach; - mach["model"] = pm->value; - mach["nozzle"] = pv->value; - m_ProfileJson["machine"][cp.name] = mach; - } - - // Filaments from cached filament presets - for (const auto& cp : vc_filament_presets) { - const auto* fv = cp.config.option("filament_vendor"); - const auto* ft = cp.config.option("filament_type"); - const auto* compat = cp.config.option("compatible_printers"); - - std::string vendor = (fv && !fv->values.empty()) ? fv->values[0] : ""; - std::string type = (ft && !ft->values.empty()) ? ft->values[0] : ""; - - std::string model_list; - if (compat) { - for (const std::string& pname : compat->values) { - if (m_ProfileJson["machine"].contains(pname)) { - std::string m = m_ProfileJson["machine"][pname]["model"]; - std::string n = m_ProfileJson["machine"][pname]["nozzle"]; - model_list += "[" + m + "++" + n + "]"; - } - } - } - - json ff; - ff["name"] = cp.name; - ff["sub_path"] = cp.file; - ff["vendor"] = vendor; - ff["type"] = type; - ff["models"] = model_list; - ff["selected"] = 0; - m_ProfileJson["filament"][cp.name] = ff; - } - - // Process from cached print presets - for (const auto& cp : vc_print_presets) { - if (!cp.is_visible) continue; - json entry; - entry["name"] = cp.name; - entry["sub_path"] = cp.file; - m_ProfileJson["process"].push_back(entry); - } } - BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from bundled per-vendor caches (" + for (const Preset& cp : bundle.printers()) { + if (!cp.is_system || !cp.vendor) continue; + const auto* pm = cp.config.option("printer_model"); + const auto* pv = cp.config.option("printer_variant"); + if (!pm || pm->value.empty() || !pv) continue; + + json mach; + mach["model"] = pm->value; + mach["nozzle"] = pv->value; + m_ProfileJson["machine"][cp.name] = mach; + } + + for (const Preset& cp : bundle.filaments()) { + if (!cp.is_system || !cp.vendor) continue; + const auto* fv = cp.config.option("filament_vendor"); + const auto* ft = cp.config.option("filament_type"); + const auto* compat = cp.config.option("compatible_printers"); + + std::string vendor = (fv && !fv->values.empty()) ? fv->values[0] : ""; + std::string type = (ft && !ft->values.empty()) ? ft->values[0] : ""; + + std::string model_list; + if (compat) { + for (const std::string& pname : compat->values) { + if (m_ProfileJson["machine"].contains(pname)) { + std::string m = m_ProfileJson["machine"][pname]["model"]; + std::string n = m_ProfileJson["machine"][pname]["nozzle"]; + model_list += "[" + m + "++" + n + "]"; + } + } + } + + json ff; + ff["name"] = cp.name; + ff["sub_path"] = cp.file; + ff["vendor"] = vendor; + ff["type"] = type; + ff["models"] = model_list; + ff["selected"] = 0; + m_ProfileJson["filament"][cp.name] = ff; + } + + for (const Preset& cp : bundle.prints()) { + if (!cp.is_system || !cp.vendor || !cp.is_visible) continue; + json entry; + entry["name"] = cp.name; + entry["sub_path"] = cp.file; + m_ProfileJson["process"].push_back(entry); + } + + BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from single-bundle cache (" << m_ProfileJson["model"].size() << " models, " << m_ProfileJson["machine"].size() << " machines, " << m_ProfileJson["filament"].size() << " filaments)"; diff --git a/tests/libslic3r/test_vendor_cache.cpp b/tests/libslic3r/test_vendor_cache.cpp index 2235f546e8..ff93fe1b3c 100644 --- a/tests/libslic3r/test_vendor_cache.cpp +++ b/tests/libslic3r/test_vendor_cache.cpp @@ -23,8 +23,6 @@ struct TempDir { ~TempDir() { boost::system::error_code ec; fs::remove_all(path, ec); } }; -// Write a minimal vendor JSON with a version field so get_vendor_cache_key() returns -// a deterministic Semver string rather than an mtime-based key. std::string write_vendor_json(const fs::path& dir, const std::string& vendor_id, const std::string& version = "1.0.0") { @@ -34,8 +32,6 @@ std::string write_vendor_json(const fs::path& dir, const std::string& vendor_id, return p.string(); } -// Write a vendor JSON without a "version" field — get_vendor_cache_key() will return -// "mtime:" instead of a Semver string. std::string write_versionless_vendor_json(const fs::path& dir, const std::string& vendor_id) { const fs::path p = dir / (vendor_id + ".json"); @@ -44,33 +40,39 @@ std::string write_versionless_vendor_json(const fs::path& dir, const std::string return p.string(); } -// Binary-patch the config_options_count field in a cache file (blob offset 4) and -// recompute the CRC so the file passes the magic/size/CRC checks but fails is_valid(). -// File layout: [20-byte CacheFileHeader][blob]; in blob: uint32 cache_version @ 0, uint32 options_count @ 4. -void patch_cache_options_count(const std::string& path, uint32_t wrong_count) +void corrupt_blob_byte(const std::string& path) +{ + std::fstream f(path, std::ios::in | std::ios::out | std::ios::binary); + f.seekp(30); + char b = 0; f.read(&b, 1); + f.seekp(30); + b ^= 0xFF; + f.write(&b, 1); +} + +// Patch cache_version (blob[0..3]) and recompute CRC so the file passes +// the CRC check but fails the cache_version check in load_system_presets_cache_for_guide. +void patch_cache_version(const std::string& path, uint32_t wrong_version) { std::ifstream in(path, std::ios::binary); std::vector data(std::istreambuf_iterator(in), {}); in.close(); - if (data.size() < 28) return; // 20-byte header + 4 (cache_version) + 4 (options_count) - - std::memcpy(&data[24], &wrong_count, 4); // file[24..27] = blob[4..7] = options_count - - // Recompute CRC over the modified blob (everything after the 20-byte file header). + if (data.size() < 24) return; + std::memcpy(&data[20], &wrong_version, 4); boost::crc_32_type crc; crc.process_bytes(&data[20], data.size() - 20); const uint32_t new_crc = crc.checksum(); - std::memcpy(&data[16], &new_crc, 4); // file[16..19] = crc32 field in CacheFileHeader - + std::memcpy(&data[16], &new_crc, 4); std::ofstream out(path, std::ios::binary | std::ios::trunc); out.write(data.data(), static_cast(data.size())); } -void add_vendor(PresetBundle& bundle, const std::string& vendor_id) +void add_vendor(PresetBundle& bundle, const std::string& vendor_id, + const std::string& name = "", Semver ver = Semver(1, 0, 0)) { VendorProfile vp(vendor_id); - vp.name = vendor_id + " Corp"; - vp.config_version = Semver(1, 0, 0); + vp.name = name.empty() ? vendor_id + " Corp" : name; + vp.config_version = ver; bundle.vendors.emplace(vendor_id, vp); } @@ -83,14 +85,31 @@ Preset& add_system_preset(PresetCollection& coll, const std::string& name, return p; } +PresetBundle::SaveCacheResult save_one_vendor(PresetBundle& src, + const fs::path& profiles_dir, + const fs::path& cache_path) +{ + return src.save_system_presets_cache(profiles_dir.string(), cache_path.string()); +} + +// Helper: filter a collection by vendor_id. +std::vector presets_for(const PresetCollection& coll, const std::string& vendor_id) +{ + std::vector out; + for (const Preset& p : coll()) + if (p.is_system && p.vendor && p.vendor->id == vendor_id) + out.push_back(&p); + return out; +} + } // namespace -TEST_CASE("VendorCache: save and load via guide API", "[VendorCache]") +TEST_CASE("SystemPresetsCache: save and load via guide API", "[VendorCache]") { TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_vendor_json(tmp.path, vid); - const fs::path cache = tmp.path / (vid + ".cache"); + const std::string vid = "Acme"; + write_vendor_json(tmp.path, vid); + const fs::path cache = tmp.path / "system_presets.cache"; PresetBundle src; add_vendor(src, vid); @@ -99,91 +118,63 @@ TEST_CASE("VendorCache: save and load via guide API", "[VendorCache]") Preset& fp = add_system_preset(src.filaments, vid + " PLA @0.4", vp); fp.alias = "Acme PLA"; fp.filament_id = "GFL_acme_pla"; - add_system_preset(src.printers, vid + " Printer 0.4", vp); - const auto stats = src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); + const auto stats = save_one_vendor(src, tmp.path, cache); REQUIRE(stats.ok); CHECK(stats.filament_presets == 1); CHECK(stats.printer_presets == 1); CHECK(stats.print_presets == 0); - VendorProfile out_profile; - std::vector out_printers, out_filaments, out_prints; - REQUIRE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), get_vendor_cache_key(json_path), - out_profile, out_printers, out_filaments, out_prints)); + PresetBundle out; + REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); + REQUIRE(out.vendors.count(vid) == 1); - CHECK(out_profile.id == vid); - REQUIRE(out_filaments.size() == 1); - CHECK(out_filaments[0].name == vid + " PLA @0.4"); - CHECK(out_filaments[0].alias == "Acme PLA"); - CHECK(out_filaments[0].filament_id == "GFL_acme_pla"); - REQUIRE(out_printers.size() == 1); - CHECK(out_printers[0].name == vid + " Printer 0.4"); + auto fi = presets_for(out.filaments, vid); + auto pr = presets_for(out.printers, vid); + REQUIRE(fi.size() == 1); + CHECK(fi[0]->name == vid + " PLA @0.4"); + CHECK(fi[0]->alias == "Acme PLA"); + CHECK(fi[0]->filament_id == "GFL_acme_pla"); + REQUIRE(pr.size() == 1); + CHECK(pr[0]->name == vid + " Printer 0.4"); } -TEST_CASE("VendorCache: stale json_ver is rejected", "[VendorCache]") -{ - TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_vendor_json(tmp.path, vid, "1.0.0"); - const fs::path cache = tmp.path / (vid + ".cache"); - - PresetBundle src; - add_vendor(src, vid); - add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid)); - src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); - - VendorProfile p; std::vector pr, fi, pp; - CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(cache.string(), "2.0.0", p, pr, fi, pp)); -} - -TEST_CASE("VendorCache: missing file returns false", "[VendorCache]") +TEST_CASE("SystemPresetsCache: missing file returns false", "[VendorCache]") { TempDir tmp; - VendorProfile p; std::vector pr, fi, pp; - CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide( - (tmp.path / "nonexistent.cache").string(), "1.0.0", p, pr, fi, pp)); + PresetBundle out; + CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide( + (tmp.path / "nonexistent.cache").string(), out)); } -TEST_CASE("VendorCache: corrupt data is rejected by CRC check", "[VendorCache]") +TEST_CASE("SystemPresetsCache: corrupt data rejected by CRC check", "[VendorCache]") { TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_vendor_json(tmp.path, vid); - const fs::path cache = tmp.path / (vid + ".cache"); + const std::string vid = "Acme"; + write_vendor_json(tmp.path, vid); + const fs::path cache = tmp.path / "system_presets.cache"; PresetBundle src; add_vendor(src, vid); add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid)); - src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); + save_one_vendor(src, tmp.path, cache); + corrupt_blob_byte(cache.string()); - // Flip a byte in the data region (after the 20-byte CacheFileHeader). - { - std::fstream f(cache.string(), std::ios::in | std::ios::out | std::ios::binary); - f.seekp(30); - char b; f.read(&b, 1); - f.seekp(30); - b ^= 0xFF; - f.write(&b, 1); - } - - VendorProfile p; std::vector pr, fi, pp; - CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp)); + PresetBundle out; + CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); } -TEST_CASE("VendorCache: multiple vendors do not bleed into each other", "[VendorCache]") +TEST_CASE("SystemPresetsCache: multiple vendors in one bundle are isolated", "[VendorCache]") { TempDir tmp; const std::string vid1 = "VendorA"; const std::string vid2 = "VendorB"; + write_vendor_json(tmp.path, vid1); + write_vendor_json(tmp.path, vid2); + const fs::path cache = tmp.path / "system_presets.cache"; PresetBundle src; - const std::string json1 = write_vendor_json(tmp.path, vid1); - const std::string json2 = write_vendor_json(tmp.path, vid2); - for (const auto& vid : {vid1, vid2}) { add_vendor(src, vid); const VendorProfile* vp = &src.vendors.at(vid); @@ -191,27 +182,29 @@ TEST_CASE("VendorCache: multiple vendors do not bleed into each other", "[Vendor add_system_preset(src.printers, vid + " Printer", vp); } - src.save_bundled_vendor_cache(vid1, json1, false, (tmp.path / (vid1 + ".cache")).string()); - src.save_bundled_vendor_cache(vid2, json2, false, (tmp.path / (vid2 + ".cache")).string()); + REQUIRE(save_one_vendor(src, tmp.path, cache).ok); - for (const auto& [vid, jpath] : std::vector>{{vid1, json1}, {vid2, json2}}) { - VendorProfile p; std::vector pr, fi, pp; - REQUIRE(PresetBundle::load_vendor_cache_for_guide( - (tmp.path / (vid + ".cache")).string(), get_vendor_cache_key(jpath), - p, pr, fi, pp)); + PresetBundle out; + REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); + REQUIRE(out.vendors.count(vid1) == 1); + REQUIRE(out.vendors.count(vid2) == 1); + + for (const auto& vid : {vid1, vid2}) { + auto fi = presets_for(out.filaments, vid); + auto pr = presets_for(out.printers, vid); REQUIRE(fi.size() == 1); - CHECK(fi[0].name == vid + " PLA"); + CHECK(fi[0]->name == vid + " PLA"); REQUIRE(pr.size() == 1); - CHECK(pr[0].name == vid + " Printer"); + CHECK(pr[0]->name == vid + " Printer"); } } -TEST_CASE("VendorCache: vendor profile fields are preserved", "[VendorCache]") +TEST_CASE("SystemPresetsCache: vendor profile fields are preserved", "[VendorCache]") { TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_vendor_json(tmp.path, vid, "2.5.1"); - const fs::path cache = tmp.path / (vid + ".cache"); + const std::string vid = "Acme"; + write_vendor_json(tmp.path, vid, "2.5.1"); + const fs::path cache = tmp.path / "system_presets.cache"; PresetBundle src; VendorProfile vp(vid); @@ -224,55 +217,51 @@ TEST_CASE("VendorCache: vendor profile fields are preserved", "[VendorCache]") model.variants.push_back(v0_4); vp.models.push_back(model); src.vendors.emplace(vid, vp); + save_one_vendor(src, tmp.path, cache); - src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); - - VendorProfile out_p; std::vector pr, fi, pp; - REQUIRE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), get_vendor_cache_key(json_path), out_p, pr, fi, pp)); - - CHECK(out_p.id == vid); - CHECK(out_p.name == "Acme Corporation"); - REQUIRE(out_p.models.size() == 1); - CHECK(out_p.models[0].id == "AcmePro"); - CHECK(out_p.models[0].name == "Acme Pro"); - REQUIRE(out_p.models[0].variants.size() == 1); - CHECK(out_p.models[0].variants[0].name == "0.4"); + PresetBundle out; + REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); + REQUIRE(out.vendors.count(vid) == 1); + const VendorProfile& gvp = out.vendors.at(vid); + CHECK(gvp.id == vid); + CHECK(gvp.name == "Acme Corporation"); + REQUIRE(gvp.models.size() == 1); + CHECK(gvp.models[0].id == "AcmePro"); + CHECK(gvp.models[0].name == "Acme Pro"); + REQUIRE(gvp.models[0].variants.size() == 1); + CHECK(gvp.models[0].variants[0].name == "0.4"); } -TEST_CASE("VendorCache: config option values are preserved", "[VendorCache]") +TEST_CASE("SystemPresetsCache: config option values are preserved", "[VendorCache]") { TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_vendor_json(tmp.path, vid); - const fs::path cache = tmp.path / (vid + ".cache"); + const std::string vid = "Acme"; + write_vendor_json(tmp.path, vid); + const fs::path cache = tmp.path / "system_presets.cache"; PresetBundle src; add_vendor(src, vid); - const VendorProfile* vp = &src.vendors.at(vid); - - Preset& fp = add_system_preset(src.filaments, vid + " PETG @0.4", vp); + Preset& fp = add_system_preset(src.filaments, vid + " PETG @0.4", &src.vendors.at(vid)); fp.config.set_key_value("filament_type", new ConfigOptionStrings({"PETG"})); + save_one_vendor(src, tmp.path, cache); - src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); - - VendorProfile p; std::vector pr, fi, pp; - REQUIRE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp)); + PresetBundle out; + REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); + auto fi = presets_for(out.filaments, vid); REQUIRE(fi.size() == 1); - const auto* ft = fi[0].config.option("filament_type"); + const auto* ft = fi[0]->config.option("filament_type"); REQUIRE(ft != nullptr); REQUIRE(ft->values.size() >= 1); CHECK(ft->values[0] == "PETG"); } -TEST_CASE("VendorCache: multiple presets per collection round-trip", "[VendorCache]") +TEST_CASE("SystemPresetsCache: multiple presets per collection round-trip", "[VendorCache]") { TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_vendor_json(tmp.path, vid); - const fs::path cache = tmp.path / (vid + ".cache"); + const std::string vid = "Acme"; + write_vendor_json(tmp.path, vid); + const fs::path cache = tmp.path / "system_presets.cache"; PresetBundle src; add_vendor(src, vid); @@ -280,75 +269,70 @@ TEST_CASE("VendorCache: multiple presets per collection round-trip", "[VendorCac const std::vector fi_names = {vid + " PLA", vid + " PETG", vid + " ABS"}; const std::vector pr_names = {vid + " Printer 0.4", vid + " Printer 0.6"}; - for (const auto& n : fi_names) add_system_preset(src.filaments, n, vp); for (const auto& n : pr_names) add_system_preset(src.printers, n, vp); - const auto stats = src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); + const auto stats = save_one_vendor(src, tmp.path, cache); REQUIRE(stats.ok); CHECK(stats.filament_presets == 3); CHECK(stats.printer_presets == 2); - VendorProfile p; std::vector pr, fi, pp; - REQUIRE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp)); + PresetBundle out; + REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); + auto fi = presets_for(out.filaments, vid); + auto pr = presets_for(out.printers, vid); REQUIRE(fi.size() == 3); REQUIRE(pr.size() == 2); + std::set fi_got, pr_got; - for (const auto& x : fi) fi_got.insert(x.name); - for (const auto& x : pr) pr_got.insert(x.name); + for (const auto* p : fi) fi_got.insert(p->name); + for (const auto* p : pr) pr_got.insert(p->name); for (const auto& n : fi_names) CHECK(fi_got.count(n) == 1); for (const auto& n : pr_names) CHECK(pr_got.count(n) == 1); } -TEST_CASE("VendorCache: truncated file is rejected", "[VendorCache]") +TEST_CASE("SystemPresetsCache: truncated file is rejected", "[VendorCache]") { TempDir tmp; const fs::path cache = tmp.path / "truncated.cache"; - - // 3 bytes — shorter than the 20-byte CacheFileHeader { std::ofstream f(cache.string(), std::ios::binary); const char data[] = {0x4F, 0x52, 0x43}; f.write(data, sizeof(data)); } - - VendorProfile p; std::vector pr, fi, pp; - CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), "1.0.0", p, pr, fi, pp)); + PresetBundle out; + CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); } -TEST_CASE("VendorCache: wrong magic is rejected", "[VendorCache]") +TEST_CASE("SystemPresetsCache: wrong magic is rejected", "[VendorCache]") { TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_vendor_json(tmp.path, vid); - const fs::path cache = tmp.path / (vid + ".cache"); + const std::string vid = "Acme"; + write_vendor_json(tmp.path, vid); + const fs::path cache = tmp.path / "system_presets.cache"; PresetBundle src; add_vendor(src, vid); add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid)); - src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); + save_one_vendor(src, tmp.path, cache); - // Overwrite first 4 bytes (magic field) with a wrong value; rest of file stays valid. { std::fstream f(cache.string(), std::ios::in | std::ios::out | std::ios::binary); const uint32_t bad = 0xDEADBEEFu; f.write(reinterpret_cast(&bad), sizeof(bad)); } - VendorProfile p; std::vector pr, fi, pp; - CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp)); + PresetBundle out; + CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); } -TEST_CASE("VendorCache: vendor with no presets saves and loads cleanly", "[VendorCache]") +TEST_CASE("SystemPresetsCache: vendor with no presets saves and loads cleanly", "[VendorCache]") { TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_vendor_json(tmp.path, vid); - const fs::path cache = tmp.path / (vid + ".cache"); + const std::string vid = "Acme"; + write_vendor_json(tmp.path, vid); + const fs::path cache = tmp.path / "system_presets.cache"; PresetBundle src; VendorProfile vp(vid); @@ -356,144 +340,91 @@ TEST_CASE("VendorCache: vendor with no presets saves and loads cleanly", "[Vendo vp.config_version = Semver(1, 0, 0); src.vendors.emplace(vid, vp); - const auto stats = src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); + const auto stats = save_one_vendor(src, tmp.path, cache); REQUIRE(stats.ok); CHECK(stats.print_presets == 0); CHECK(stats.filament_presets == 0); CHECK(stats.printer_presets == 0); - VendorProfile p; std::vector pr, fi, pp; - REQUIRE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp)); - CHECK(p.id == vid); - CHECK(p.name == "Acme Corporation"); - CHECK(fi.empty()); - CHECK(pr.empty()); - CHECK(pp.empty()); + PresetBundle out; + REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); + REQUIRE(out.vendors.count(vid) == 1); + CHECK(out.vendors.at(vid).id == vid); + CHECK(out.vendors.at(vid).name == "Acme Corporation"); + CHECK(presets_for(out.filaments, vid).empty()); + CHECK(presets_for(out.printers, vid).empty()); + CHECK(presets_for(out.prints, vid).empty()); } -TEST_CASE("VendorCache: all Preset metadata fields are preserved", "[VendorCache]") +TEST_CASE("SystemPresetsCache: all Preset metadata fields are preserved", "[VendorCache]") { TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_vendor_json(tmp.path, vid); - const fs::path cache = tmp.path / (vid + ".cache"); + const std::string vid = "Acme"; + write_vendor_json(tmp.path, vid); + const fs::path cache = tmp.path / "system_presets.cache"; PresetBundle src; add_vendor(src, vid); - const VendorProfile* vp = &src.vendors.at(vid); + Preset& fp = add_system_preset(src.filaments, vid + " PLA @0.4", &src.vendors.at(vid)); + fp.setting_id = "sid-test-001"; + fp.description = "A test filament preset"; + fp.bundle_id = "bundle-xyz"; + fp.user_id = "user-abc"; + fp.base_id = "base-123"; + fp.sync_info = "update"; + fp.updated_time = 1700000000LL; + fp.key_values = {{"color", "red"}, {"diameter", "1.75"}}; + fp.ini_str = "[filament]\nnozzle_temperature = 230\n"; + save_one_vendor(src, tmp.path, cache); - Preset& fp = add_system_preset(src.filaments, vid + " PLA @0.4", vp); - fp.setting_id = "sid-test-001"; - fp.description = "A test filament preset"; - fp.bundle_id = "bundle-xyz"; - fp.user_id = "user-abc"; - fp.base_id = "base-123"; - fp.sync_info = "update"; - fp.updated_time = 1700000000LL; - fp.key_values = {{"color", "red"}, {"diameter", "1.75"}}; - fp.ini_str = "[filament]\nnozzle_temperature = 230\n"; - - src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); - - VendorProfile p; std::vector pr, fi, pp; - REQUIRE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp)); + PresetBundle out; + REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); + auto fi = presets_for(out.filaments, vid); REQUIRE(fi.size() == 1); - const Preset& out = fi[0]; - CHECK(out.setting_id == "sid-test-001"); - CHECK(out.description == "A test filament preset"); - CHECK(out.bundle_id == "bundle-xyz"); - CHECK(out.user_id == "user-abc"); - CHECK(out.base_id == "base-123"); - CHECK(out.sync_info == "update"); - CHECK(out.updated_time == 1700000000LL); - REQUIRE(out.key_values.count("color") == 1); - CHECK(out.key_values.at("color") == "red"); - REQUIRE(out.key_values.count("diameter") == 1); - CHECK(out.key_values.at("diameter") == "1.75"); - CHECK(out.ini_str == "[filament]\nnozzle_temperature = 230\n"); + CHECK(fi[0]->setting_id == "sid-test-001"); + CHECK(fi[0]->description == "A test filament preset"); + CHECK(fi[0]->bundle_id == "bundle-xyz"); + CHECK(fi[0]->user_id == "user-abc"); + CHECK(fi[0]->base_id == "base-123"); + CHECK(fi[0]->sync_info == "update"); + CHECK(fi[0]->updated_time == 1700000000LL); + REQUIRE(fi[0]->key_values.count("color") == 1); + CHECK(fi[0]->key_values.at("color") == "red"); + REQUIRE(fi[0]->key_values.count("diameter") == 1); + CHECK(fi[0]->key_values.at("diameter") == "1.75"); + CHECK(fi[0]->ini_str == "[filament]\nnozzle_temperature = 230\n"); } -// TC09 — versionless JSON uses mtime as the cache key; same mtime → cache valid. -// TC10 — after the JSON file is touched (mtime changes), the old key no longer matches. -TEST_CASE("VendorCache: versionless vendor uses mtime key", "[VendorCache]") +TEST_CASE("SystemPresetsCache: wrong cache_version is rejected", "[VendorCache]") { TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_versionless_vendor_json(tmp.path, vid); - const fs::path cache = tmp.path / (vid + ".cache"); + const std::string vid = "Acme"; + write_vendor_json(tmp.path, vid); + const fs::path cache = tmp.path / "system_presets.cache"; PresetBundle src; add_vendor(src, vid); add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid)); + save_one_vendor(src, tmp.path, cache); + patch_cache_version(cache.string(), 0xFFFFFFFFu); - const std::string key_before = get_vendor_cache_key(json_path); - REQUIRE_FALSE(key_before.empty()); - // Must be mtime-based, not a Semver. - REQUIRE(key_before.substr(0, 6) == "mtime:"); - - src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); - - SECTION("same mtime → cache is valid") { - VendorProfile p; std::vector pr, fi, pp; - CHECK(PresetBundle::load_vendor_cache_for_guide( - cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp)); - CHECK(fi.size() == 1); - } - - SECTION("touched file changes mtime → cache is invalid") { - // Advance mtime by 2 seconds so the key definitely differs. - const std::time_t old_mtime = fs::last_write_time(json_path); - fs::last_write_time(json_path, old_mtime + 2); - - const std::string key_after = get_vendor_cache_key(json_path); - REQUIRE(key_after != key_before); - - VendorProfile p; std::vector pr, fi, pp; - CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), key_after, p, pr, fi, pp)); - } + PresetBundle out; + CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); } -// TC07 — config_options_count in the cache header must equal the live print_config_def -// option count. A patched value (1) is rejected by is_valid() even if the CRC is correct. -TEST_CASE("VendorCache: config_options_count mismatch is rejected", "[VendorCache]") +TEST_CASE("SystemPresetsCache: mid-blob truncation is rejected", "[VendorCache]") { TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_vendor_json(tmp.path, vid); - const fs::path cache = tmp.path / (vid + ".cache"); + const std::string vid = "Acme"; + write_vendor_json(tmp.path, vid); + const fs::path cache = tmp.path / "system_presets.cache"; PresetBundle src; add_vendor(src, vid); add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid)); - src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); + save_one_vendor(src, tmp.path, cache); - // Patch config_options_count to 1 and fix CRC — file is structurally valid but semantically stale. - patch_cache_options_count(cache.string(), 1u); - - VendorProfile p; std::vector pr, fi, pp; - CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp)); -} - -// TC12 variant — header is intact but the blob is cut short; the blob read fails. -TEST_CASE("VendorCache: mid-blob truncation is rejected", "[VendorCache]") -{ - TempDir tmp; - const std::string vid = "Acme"; - const std::string json_path = write_vendor_json(tmp.path, vid); - const fs::path cache = tmp.path / (vid + ".cache"); - - PresetBundle src; - add_vendor(src, vid); - add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid)); - src.save_bundled_vendor_cache(vid, json_path, false, cache.string()); - - // Keep the full 20-byte CacheFileHeader intact; truncate the blob to 10 bytes. - // data_size in the header still says the full blob length, so the read will fail. { std::ifstream in(cache.string(), std::ios::binary); std::vector buf(30); // 20-byte header + 10 bytes of blob @@ -503,7 +434,25 @@ TEST_CASE("VendorCache: mid-blob truncation is rejected", "[VendorCache]") out.write(buf.data(), 30); } - VendorProfile p; std::vector pr, fi, pp; - CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide( - cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp)); + PresetBundle out; + CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); +} + +TEST_CASE("SystemPresetsCache: versionless vendor uses mtime key in bundle", "[VendorCache]") +{ + TempDir tmp; + const std::string vid = "Acme"; + write_versionless_vendor_json(tmp.path, vid); + const fs::path cache = tmp.path / "system_presets.cache"; + + PresetBundle src; + add_vendor(src, vid); + add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid)); + REQUIRE(save_one_vendor(src, tmp.path, cache).ok); + + PresetBundle out; + REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out)); + auto fi = presets_for(out.filaments, vid); + REQUIRE(fi.size() == 1); + CHECK(fi[0]->name == vid + " PLA"); }