Ctrl K
ComunidadGuías…

server/src/app_catalog_policy.cpp

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * @file src/app_catalog_policy.cpp
 * @brief Pure application artwork and stable-ID catalog policy.
 */

#include "app_catalog_policy.h"

#include <algorithm>
#include <array>
#include <boost/crc.hpp>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <sstream>

namespace proc::catalog {
  namespace {
    std::string numeric_id(const std::vector<std::string> &parts, std::optional<int> index = std::nullopt) {
      boost::crc_32_type crc;
      for (const auto &part : parts) {
        crc.process_bytes(part.data(), part.size());
      }
      if (index) {
        const auto suffix = std::to_string(*index);
        crc.process_bytes(suffix.data(), suffix.size());
      }
      return std::to_string(std::abs(static_cast<std::int32_t>(crc.checksum())));
    }

    void remember_alias(alias_state_t &state, const std::string &alias) {
      if (!alias.empty() && alias != state.current_id) {
        state.aliases.insert(alias);
      }
    }

    std::string lowercase_extension(const std::string &path) {
      auto extension = std::filesystem::path(path).extension().string();
      std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char ch) {
        return static_cast<char>(std::tolower(ch));
      });
      return extension;
    }

    bool is_strict_descendant(
      const std::filesystem::path &candidate,
      const std::filesystem::path &root) {
      const auto normalized_candidate = candidate.lexically_normal();
      const auto normalized_root = root.lexically_normal();
      if (!normalized_candidate.is_absolute() || !normalized_root.is_absolute()) {
        return false;
      }

      auto candidate_component = normalized_candidate.begin();
      for (auto root_component = normalized_root.begin();
           root_component != normalized_root.end();
           ++root_component, ++candidate_component) {
        if (candidate_component == normalized_candidate.end() ||
            *candidate_component != *root_component) {
          return false;
        }
      }
      return candidate_component != normalized_candidate.end();
    }
  }  // namespace

  bool has_png_signature(std::span<const std::uint8_t> bytes) {
    static constexpr std::array<std::uint8_t, 8> signature {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
    return bytes.size() >= signature.size() && std::equal(signature.begin(), signature.end(), bytes.begin());
  }

  std::string asset_path(const std::string &assets_root, const std::string &relative_path) {
    return (std::filesystem::path(assets_root) / relative_path).string();
  }

  bool machine_image_path_is_confined(
    const std::string &image_path,
    const std::string &assets_root,
    const std::string &covers_root) {
    if (image_path.empty()) {
      return false;
    }

    const std::filesystem::path requested {image_path};
    if (requested.is_absolute()) {
      return is_strict_descendant(requested, assets_root) ||
             is_strict_descendant(requested, covers_root);
    }

    // Legacy Steam catalogs used this client-facing spelling even though the
    // installed file lives directly in the immutable assets root.
    if (requested == std::filesystem::path {"./assets/steam.png"}) {
      return true;
    }
    return is_strict_descendant(std::filesystem::path {assets_root} / requested,
                                assets_root);
  }

  std::string validate_image_path(
    std::string image_path,
    const std::string &assets_root,
    const std::string &default_image,
    const image_reader_t &read_image) {
    if (image_path.empty() || lowercase_extension(image_path) != ".png") {
      return default_image;
    }

    const auto asset_candidate = asset_path(assets_root, image_path);
    if (const auto bytes = read_image(asset_candidate)) {
      return has_png_signature(*bytes) ? asset_candidate : default_image;
    }
    if (image_path == "./assets/steam.png") {
      return asset_path(assets_root, "steam.png");
    }
    const auto bytes = read_image(image_path);
    return bytes && has_png_signature(*bytes) ? image_path : default_image;
  }

  std::tuple<std::string, std::string> calculate_ids(
    const std::string &app_name,
    const std::string &app_uuid,
    const std::string &legacy_image_identity,
    int index) {
    std::vector<std::string> parts;
    if (!app_uuid.empty()) {
      parts.push_back(app_uuid);
    } else {
      parts.push_back(app_name);
      if (!legacy_image_identity.empty()) {
        parts.push_back(legacy_image_identity);
      }
    }
    return {numeric_id(parts), numeric_id(parts, index)};
  }

  std::tuple<std::string, std::string> calculate_versioned_ids(
    const std::string &app_uuid,
    const std::string &cover_fingerprint,
    int index) {
    const std::vector<std::string> parts {app_uuid, "\n", cover_fingerprint};
    return {numeric_id(parts), numeric_id(parts, index)};
  }

  void assign_compatible_id(
    app_identity_t &app,
    int index,
    std::set<std::string> &occupied_ids,
    std::map<std::string, alias_state_t> &persisted,
    std::set<std::string> &active_uuids,
    bool &state_changed) {
    const auto legacy_ids = calculate_ids(app.name, app.uuid, app.legacy_image_identity, index);
    if (app.uuid.empty()) {
      app.id = occupied_ids.contains(std::get<0>(legacy_ids)) ? std::get<1>(legacy_ids) : std::get<0>(legacy_ids);
      occupied_ids.insert(app.id);
      return;
    }

    active_uuids.insert(app.uuid);
    auto [iter, inserted] = persisted.try_emplace(
      app.uuid,
      alias_state_t {std::get<0>(legacy_ids), app.art_version, {}});
    auto &state = iter->second;
    state_changed = state_changed || inserted;
    if (state.current_id.empty()) {
      state.current_id = std::get<0>(legacy_ids);
      state_changed = true;
    }
    if (state.cover_fingerprint.empty()) {
      state.cover_fingerprint = app.art_version;
      state_changed = true;
    }
    // A UUID-only ID may already be cached with placeholder artwork on a
    // client, including after removal pruned our previous fingerprint.
    // Version first-seen and existing UUID-only entries too, so re-adding
    // an app cannot resurrect that stale client cache.
    if (state.cover_fingerprint != app.art_version ||
        (!app.art_version.empty() && state.current_id == std::get<0>(legacy_ids))) {
      const auto previous = state.current_id;
      const auto versioned = calculate_versioned_ids(app.uuid, app.art_version, index);
      state.current_id = occupied_ids.contains(std::get<0>(versioned)) ? std::get<1>(versioned) : std::get<0>(versioned);
      state.cover_fingerprint = app.art_version;
      remember_alias(state, previous);
      remember_alias(state, std::get<0>(legacy_ids));
      state_changed = true;
    }
    if (occupied_ids.contains(state.current_id)) {
      remember_alias(state, state.current_id);
      const auto versioned = calculate_versioned_ids(app.uuid, app.art_version, index);
      state.current_id = occupied_ids.contains(std::get<1>(versioned)) ? std::get<1>(legacy_ids) : std::get<1>(versioned);
      state_changed = true;
    }
    app.id = state.current_id;
    app.aliases.assign(state.aliases.begin(), state.aliases.end());
    occupied_ids.insert(app.id);
  }

  void prune_and_filter_aliases(
    std::vector<app_identity_t> &apps,
    std::map<std::string, alias_state_t> &persisted,
    const std::set<std::string> &active_uuids,
    bool &state_changed) {
    for (auto it = persisted.begin(); it != persisted.end();) {
      if (!active_uuids.contains(it->first)) {
        it = persisted.erase(it);
        state_changed = true;
      } else {
        ++it;
      }
    }

    std::set<std::string> current_ids;
    std::map<std::string, int> alias_counts;
    for (const auto &app : apps) {
      current_ids.insert(app.id);
      for (const auto &alias : app.aliases) {
        ++alias_counts[alias];
      }
    }
    for (auto &app : apps) {
      if (app.uuid.empty()) {
        continue;
      }
      std::vector<std::string> filtered;
      std::set<std::string> seen;
      for (const auto &alias : app.aliases) {
        if (alias.empty() || alias == app.id || !seen.insert(alias).second || current_ids.contains(alias) || alias_counts[alias] > 1) {
          state_changed = true;
          continue;
        }
        filtered.push_back(alias);
      }
      app.aliases = std::move(filtered);
      auto &state = persisted[app.uuid];
      state.current_id = app.id;
      state.cover_fingerprint = app.art_version;
      state.aliases = {app.aliases.begin(), app.aliases.end()};
    }
  }

  std::optional<std::size_t> resolve_app(
    const std::vector<app_identity_t> &apps,
    std::string app_id,
    const std::string &app_uuid) {
    if (!app_uuid.empty()) {
      for (std::size_t i = 0; i < apps.size(); ++i) {
        if (apps[i].uuid == app_uuid) {
          return i;
        }
      }
    }
    app_id.erase(app_id.begin(), std::find_if(app_id.begin(), app_id.end(), [](unsigned char ch) { return !std::isspace(ch); }));
    app_id.erase(std::find_if(app_id.rbegin(), app_id.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), app_id.end());
    if (app_id.empty() || app_id == "0") {
      return std::nullopt;
    }
    for (std::size_t i = 0; i < apps.size(); ++i) {
      if (apps[i].id == app_id) {
        return i;
      }
    }
    std::optional<std::size_t> alias_match;
    for (std::size_t i = 0; i < apps.size(); ++i) {
      if (std::find(apps[i].aliases.begin(), apps[i].aliases.end(), app_id) == apps[i].aliases.end()) {
        continue;
      }
      if (alias_match) {
        return std::nullopt;
      }
      alias_match = i;
    }
    return alias_match;
  }
}  // namespace proc::catalog