diff --git a/common/Makefile.am b/common/Makefile.am index fa4f37035..834f5d495 100644 --- a/common/Makefile.am +++ b/common/Makefile.am @@ -1,5 +1,3 @@ - - noinst_LIBRARIES = libsynaptic.a AM_CPPFLAGS = -I/usr/include/apt-pkg @RPM_HDRS@ @DEB_HDRS@ \ @@ -34,8 +32,12 @@ libsynaptic_a_SOURCES =\ raptoptions.h\ rsources.cc \ rsources.h \ + rsource_deb822.cc \ + rsource_deb822.h \ rcacheactor.cc \ rcacheactor.h \ + rpackagemanager.cc \ + rpackagemanager.h \ rpackagelistactor.cc \ rpackagelistactor.h \ rtagcollbuilder.cc \ diff --git a/common/rdeb822source.cc b/common/rdeb822source.cc new file mode 100644 index 000000000..1f5696c72 --- /dev/null +++ b/common/rdeb822source.cc @@ -0,0 +1,174 @@ +#include "rdeb822source.h" +#include +#include +#include +#include +#include // Added for debug prints +#include // Added for debug prints + +RDeb822Source::RDeb822Source() + : enabled(true) +{ +} + +RDeb822Source::RDeb822Source(const std::string& types, const std::string& uris, + const std::string& suites, const std::string& components) + : types(types), uris(uris), suites(suites), components(components), enabled(true) +{ +} + +bool RDeb822Source::isValid() const { + return !types.empty() && !uris.empty() && !suites.empty(); +} + +std::string RDeb822Source::toString() const { + std::stringstream ss; + if (!enabled) { + ss << "Types: " << types << "\n"; + ss << "URIs: " << uris << "\n"; + ss << "Suites: " << suites << "\n"; + if (!components.empty()) { + ss << "Components: " << components << "\n"; + } + if (!signedBy.empty()) { + ss << "Signed-By: " << signedBy << "\n"; + } + } else { + ss << "# " << types << " " << uris << " " << suites; + if (!components.empty()) { + ss << " " << components; + } + if (!signedBy.empty()) { + ss << " [signed-by=" << signedBy << "]"; + } + } + return ss.str(); +} + +RDeb822Source RDeb822Source::fromString(const std::string& content) { + RDeb822Source source; + std::istringstream iss(content); + std::string line; + + while (std::getline(iss, line)) { + line = trim(line); + if (line.empty() || line[0] == '#') continue; + + size_t colon = line.find(':'); + if (colon == std::string::npos) continue; + + std::string key = trim(line.substr(0, colon)); + std::string value = trim(line.substr(colon + 1)); + + if (key == "Types") source.setTypes(value); + else if (key == "URIs") source.setURIs(value); + else if (key == "Suites") source.setSuites(value); + else if (key == "Components") source.setComponents(value); + else if (key == "Signed-By") source.setSignedBy(value); + } + + return source; +} + +bool RDeb822Source::operator==(const RDeb822Source& other) const { + return types == other.types && + uris == other.uris && + suites == other.suites && + components == other.components && + signedBy == other.signedBy && + enabled == other.enabled; +} + +bool RDeb822Source::operator!=(const RDeb822Source& other) const { + return !(*this == other); +} + +std::string RDeb822Source::trim(const std::string& str) { + const std::string whitespace = " \t\r\n"; + size_t start = str.find_first_not_of(whitespace); + if (start == std::string::npos) { + return ""; + } + size_t end = str.find_last_not_of(whitespace); + return str.substr(start, end - start + 1); +} + +bool RDeb822Source::ParseDeb822File(const std::string& path, std::vector& entries) { + std::cout << "DEBUG: [Deb822Parser] Opening file: " << path << std::endl; + std::ifstream file(path); + if (!file.is_open()) { + std::cout << "DEBUG: [Deb822Parser] Failed to open file: " << path << std::endl; + return false; + } + std::string line; + std::map fields; + int stanza_count = 0; + while (std::getline(file, line)) { + std::cout << "DEBUG: [Deb822Parser] Read line: '" << line << "'" << std::endl; + if (line.empty()) { + if (!fields.empty()) { + std::cout << "DEBUG: [Deb822Parser] End of stanza, fields found:" << std::endl; + for (const auto& kv : fields) { + std::cout << " '" << kv.first << "': '" << kv.second << "'" << std::endl; + } + Deb822Entry entry; + // Check required fields + if (fields.find("Types") == fields.end() || fields.find("URIs") == fields.end() || fields.find("Suites") == fields.end()) { + std::cout << "DEBUG: [Deb822Parser] Missing required field in stanza, skipping." << std::endl; + fields.clear(); + continue; + } + entry.Types = fields["Types"]; + entry.URIs = fields["URIs"]; + entry.Suites = fields["Suites"]; + entry.Components = fields.count("Components") ? fields["Components"] : ""; + entry.SignedBy = fields.count("Signed-By") ? fields["Signed-By"] : ""; + entry.Enabled = true; // Default to enabled + entries.push_back(entry); + stanza_count++; + fields.clear(); + } + continue; + } + if (line[0] == '#') { + std::cout << "DEBUG: [Deb822Parser] Skipping comment line." << std::endl; + continue; + } + size_t colon = line.find(':'); + if (colon == std::string::npos) { + std::cout << "DEBUG: [Deb822Parser] No colon found in line, skipping." << std::endl; + continue; + } + std::string key = line.substr(0, colon); + std::string value = line.substr(colon + 1); + // Trim whitespace + key.erase(0, key.find_first_not_of(" \t")); + key.erase(key.find_last_not_of(" \t") + 1); + value.erase(0, value.find_first_not_of(" \t")); + value.erase(value.find_last_not_of(" \t") + 1); + std::cout << "DEBUG: [Deb822Parser] Parsed field: '" << key << "' = '" << value << "'" << std::endl; + fields[key] = value; + } + // Handle last stanza if file does not end with blank line + if (!fields.empty()) { + std::cout << "DEBUG: [Deb822Parser] End of file, last stanza fields:" << std::endl; + for (const auto& kv : fields) { + std::cout << " '" << kv.first << "': '" << kv.second << "'" << std::endl; + } + Deb822Entry entry; + if (fields.find("Types") == fields.end() || fields.find("URIs") == fields.end() || fields.find("Suites") == fields.end()) { + std::cout << "DEBUG: [Deb822Parser] Missing required field in last stanza, skipping." << std::endl; + } else { + entry.Types = fields["Types"]; + entry.URIs = fields["URIs"]; + entry.Suites = fields["Suites"]; + entry.Components = fields.count("Components") ? fields["Components"] : ""; + entry.SignedBy = fields.count("Signed-By") ? fields["Signed-By"] : ""; + entry.Enabled = true; + entries.push_back(entry); + stanza_count++; + } + } + std::cout << "DEBUG: [Deb822Parser] Parsed " << stanza_count << " stanzas from file: " << path << std::endl; + return true; +} \ No newline at end of file diff --git a/common/rdeb822source.h b/common/rdeb822source.h new file mode 100644 index 000000000..7fd9d5c78 --- /dev/null +++ b/common/rdeb822source.h @@ -0,0 +1,38 @@ +class RDeb822Source { +public: + RDeb822Source(); + RDeb822Source(const std::string& types, const std::string& uris, + const std::string& suites, const std::string& components = ""); + + bool isValid() const; + std::string toString() const; + static RDeb822Source fromString(const std::string& content); + bool operator==(const RDeb822Source& other) const; + bool operator!=(const RDeb822Source& other) const; + + // Getters + std::string getTypes() const { return types; } + std::string getURIs() const { return uris; } + std::string getSuites() const { return suites; } + std::string getComponents() const { return components; } + std::string getSignedBy() const { return signedBy; } + bool isEnabled() const { return enabled; } + + // Setters + void setTypes(const std::string& t) { types = t; } + void setURIs(const std::string& u) { uris = u; } + void setSuites(const std::string& s) { suites = s; } + void setComponents(const std::string& c) { components = c; } + void setSignedBy(const std::string& s) { signedBy = s; } + void setEnabled(bool e) { enabled = e; } + + static std::string trim(const std::string& str); + +private: + std::string types; // Changed from type to types + std::string uris; // Changed from uri to uris + std::string suites; // Changed from dist to suites + std::string components; // Changed from sections to components + std::string signedBy; + bool enabled; +}; \ No newline at end of file diff --git a/common/rpackagelister.cc b/common/rpackagelister.cc index 9fdfb1a00..dbc84cda0 100644 --- a/common/rpackagelister.cc +++ b/common/rpackagelister.cc @@ -2106,11 +2106,7 @@ bool RPackageLister::xapianSearch(string searchString) bool RPackageLister::isMultiarchSystem() { -#ifdef WITH_APT_MULTIARCH_SUPPORT - return (APT::Configuration::getArchitectures().size() > 1); -#else - return false; -#endif + return _system->MultiArchSupported(); } // vim:ts=3:sw=3:et diff --git a/common/rpackagemanager.cc b/common/rpackagemanager.cc new file mode 100644 index 000000000..88da10b02 --- /dev/null +++ b/common/rpackagemanager.cc @@ -0,0 +1,425 @@ +#include "rpackagemanager.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// RPackageManager implementation + +// Helper function to check if a string starts with a prefix +static bool starts_with(const std::string& str, const std::string& prefix) { + return str.size() >= prefix.size() && + str.compare(0, prefix.size(), prefix) == 0; +} + +// RDeb822Source implementation +RDeb822Source::RDeb822Source() : enabled(true) {} + +RDeb822Source::RDeb822Source(const std::string& types, const std::string& uris, + const std::string& suites, const std::string& components) + : types(types), uris(uris), suites(suites), components(components), enabled(true) {} + +bool RDeb822Source::isValid() const { + // Check required fields + if (types.empty() || uris.empty() || suites.empty()) { + return false; + } + + // Validate types + if (types != "deb" && types != "deb-src") { + return false; + } + + // Split URIs and validate each + std::istringstream uriStream(uris); + std::string uri; + bool hasValidUri = false; + + while (std::getline(uriStream, uri, ' ')) { + // Trim whitespace + uri.erase(0, uri.find_first_not_of(" \t")); + uri.erase(uri.find_last_not_of(" \t") + 1); + + if (uri.empty()) continue; + + // Check URI scheme + if (starts_with(uri, "http://") || + starts_with(uri, "https://") || + starts_with(uri, "ftp://") || + starts_with(uri, "file://") || + starts_with(uri, "cdrom:")) { + hasValidUri = true; + } + } + + if (!hasValidUri) { + return false; + } + + // Validate suites + std::istringstream suiteStream(suites); + std::string suite; + bool hasValidSuite = false; + + while (std::getline(suiteStream, suite, ' ')) { + // Trim whitespace + suite.erase(0, suite.find_first_not_of(" \t")); + suite.erase(suite.find_last_not_of(" \t") + 1); + + if (!suite.empty()) { + hasValidSuite = true; + break; + } + } + + return hasValidSuite; +} + +std::string RDeb822Source::toString() const { + std::stringstream ss; + + // Add comment if source is disabled + if (!enabled) { + ss << "# Disabled: "; + } + + // Format as a single line for compatibility + ss << types << " " << uris << " " << suites; + if (!components.empty()) { + ss << " " << components; + } + ss << "\n"; + + return ss.str(); +} + +RDeb822Source RDeb822Source::fromString(const std::string& content) { + RDeb822Source source; + std::istringstream stream(content); + std::string line; + bool hasTypes = false; + bool hasUris = false; + bool hasSuites = false; + bool hasComponents = false; + + while (std::getline(stream, line)) { + // Skip comments and empty lines + if (line.empty() || starts_with(line, "#")) { + continue; + } + + // Parse key-value pairs + size_t colonPos = line.find(':'); + if (colonPos == std::string::npos) { + continue; + } + + std::string key = line.substr(0, colonPos); + std::string value = line.substr(colonPos + 1); + + // Trim whitespace + key = trim(key); + value = trim(value); + + if (key == "Types") { + source.types = value; + hasTypes = true; + } else if (key == "URIs") { + source.uris = value; + hasUris = true; + } else if (key == "Suites") { + source.suites = value; + hasSuites = true; + } else if (key == "Components") { + source.components = value; + hasComponents = true; + } else if (key == "Signed-By") { + source.signedBy = value; + } + } + + // Validate required fields + if (!hasTypes || !hasUris || !hasSuites || !hasComponents) { + std::cerr << "Warning: Missing required fields in source" << std::endl; + return RDeb822Source(); // Return invalid source + } + + return source; +} + +bool RDeb822Source::operator==(const RDeb822Source& other) const { + return types == other.types && + uris == other.uris && + suites == other.suites && + components == other.components && + enabled == other.enabled; +} + +bool RDeb822Source::operator!=(const RDeb822Source& other) const { + return !(*this == other); +} + +// RSourceManager implementation +RSourceManager::RSourceManager() : sourcesDir("/etc/apt/sources.list.d") {} + +RSourceManager::RSourceManager(const std::string& sourcesDir) : sourcesDir(sourcesDir) {} + +bool RSourceManager::addSource(const RDeb822Source& source) { + if (!source.isValid()) { + return false; + } + + // Check if source already exists + for (const auto& existing : sources) { + if (existing == source) { + return false; + } + } + + sources.push_back(source); + return true; +} + +bool RSourceManager::removeSource(const RDeb822Source& source) { + auto it = std::find(sources.begin(), sources.end(), source); + if (it == sources.end()) { + return false; + } + + sources.erase(it); + return true; +} + +bool RSourceManager::updateSource(const RDeb822Source& oldSource, const RDeb822Source& newSource) { + if (!newSource.isValid()) { + return false; + } + + auto it = std::find(sources.begin(), sources.end(), oldSource); + if (it == sources.end()) { + return false; + } + + *it = newSource; + return true; +} + +std::vector RSourceManager::getSources() const { + return sources; +} + +bool RSourceManager::loadSources() { + sources.clear(); + + try { + // Read all .sources files in the sources directory + for (const auto& entry : std::filesystem::directory_iterator(sourcesDir)) { + if (entry.path().extension() == ".sources") { + std::cerr << "Loading source file: " << entry.path().string() << std::endl; + + // Read the entire file content + std::ifstream file(entry.path()); + if (!file) { + std::cerr << "Error: Could not open file: " << entry.path().string() << std::endl; + continue; + } + + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + + // Parse sources from the file content + std::vector fileSources = parseSources(content); + sources.insert(sources.end(), fileSources.begin(), fileSources.end()); + } + } + return true; + } catch (const std::exception& e) { + std::cerr << "Error loading sources: " << e.what() << std::endl; + return false; + } +} + +std::vector RSourceManager::parseSources(const std::string& content) +{ + std::vector sources; + std::map currentFields; + + std::istringstream iss(content); + std::string line; + bool inSourceBlock = false; + + while (std::getline(iss, line)) { + // Trim whitespace + line = trim(line); + + // Skip empty lines + if (line.empty()) { + if (!currentFields.empty()) { + RDeb822Source source = createSourceFromFields(currentFields); + if (source.isValid()) { + sources.push_back(source); + } + currentFields.clear(); + } + continue; + } + + // Check for source block start + if (starts_with(line, "#") && line.find("Modernized") != std::string::npos) { + if (!currentFields.empty()) { + RDeb822Source source = createSourceFromFields(currentFields); + if (source.isValid()) { + sources.push_back(source); + } + currentFields.clear(); + } + inSourceBlock = true; + continue; + } + + // Skip other comments + if (starts_with(line, "#")) { + continue; + } + + // Parse key-value pairs + size_t colonPos = line.find(':'); + if (colonPos != std::string::npos) { + std::string key = trim(line.substr(0, colonPos)); + std::string value = trim(line.substr(colonPos + 1)); + + // Look ahead for continuation lines + while (std::getline(iss, line)) { + line = trim(line); + if (line.empty() || starts_with(line, "#") || line.find(':') != std::string::npos) { + // Put the line back for the next iteration + iss.seekg(-static_cast(line.length() + 1), std::ios_base::cur); + break; + } + value += " " + line; + } + + currentFields[key] = value; + } + } + + // Handle the last source + if (!currentFields.empty()) { + RDeb822Source source = createSourceFromFields(currentFields); + if (source.isValid()) { + sources.push_back(source); + } + } + + return sources; +} + +RDeb822Source RSourceManager::createSourceFromFields(const std::map& fields) +{ + RDeb822Source source; + // Set required fields + if (fields.count("Types")) source.setTypes(fields.at("Types")); + if (fields.count("URIs")) source.setUris(fields.at("URIs")); + if (fields.count("Suites")) source.setSuites(fields.at("Suites")); + if (fields.count("Components")) source.setComponents(fields.at("Components")); + // Set optional fields + if (fields.count("Signed-By")) source.setSignedBy(fields.at("Signed-By")); + return source; +} + +bool RSourceManager::saveSources() const { + try { + for (const auto& source : sources) { + std::string filename = getSourceFilename(source); + if (!writeSourceFile(filename, source)) { + return false; + } + } + return true; + } catch (const std::exception&) { + return false; + } +} + +bool RSourceManager::writeSourceFile(const std::string& filename, const RDeb822Source& source) const { + try { + // Create parent directories if they don't exist + std::filesystem::path filePath(filename); + std::filesystem::create_directories(filePath.parent_path()); + + std::ofstream file(filename); + if (!file) { + return false; + } + + file << source.toString(); + return file.good(); + } catch (const std::exception&) { + return false; + } +} + +RDeb822Source RSourceManager::readSourceFile(const std::string& filename) const { + try { + std::ifstream file(filename); + if (!file) { + return RDeb822Source(); + } + + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + return RDeb822Source::fromString(content); + } catch (const std::exception&) { + return RDeb822Source(); + } +} + +bool RSourceManager::updateAptSources() { + // On Windows, we'll just return true for testing + return true; +} + +bool RSourceManager::reloadAptCache() { + // On Windows, we'll just return true for testing + return true; +} + +bool RSourceManager::validateSourceFile(const std::string& filename) const { + RDeb822Source source = readSourceFile(filename); + return source.isValid(); +} + +std::string RSourceManager::getSourceFilename(const RDeb822Source& source) const { + // Generate a filename based on the source URI and suite + std::string filename = source.getUris(); + filename.erase(0, filename.find("://") + 3); // Remove protocol + std::replace(filename.begin(), filename.end(), '/', '-'); + filename += "-" + source.getSuites() + ".sources"; + return (std::filesystem::path(sourcesDir) / filename).string(); +} + +std::string RSourceManager::trim(const std::string& str) const +{ + const std::string whitespace = " \t\r\n"; + size_t start = str.find_first_not_of(whitespace); + if (start == std::string::npos) { + return ""; + } + size_t end = str.find_last_not_of(whitespace); + return str.substr(start, end - start + 1); +} + +// Implement RDeb822Source::trim +std::string RDeb822Source::trim(const std::string& str) { + const std::string whitespace = " \t\r\n"; + size_t start = str.find_first_not_of(whitespace); + if (start == std::string::npos) { + return ""; + } + size_t end = str.find_last_not_of(whitespace); + return str.substr(start, end - start + 1); +} \ No newline at end of file diff --git a/common/rpackagemanager.h b/common/rpackagemanager.h index 72ddd2910..cbe0a7ad0 100644 --- a/common/rpackagemanager.h +++ b/common/rpackagemanager.h @@ -22,6 +22,16 @@ * USA */ +#ifndef RPACKAGEMANAGER_H +#define RPACKAGEMANAGER_H + +#include +#include +#include +#include +#include +#include + // We need a different package manager, since we need to do the // DoInstall() process in two steps when forking. Without that, // the forked package manager would be updated with the new @@ -36,44 +46,100 @@ // to export the functionality we need, so that we may avoid this // ugly hack. -#include -#include - #define protected public #include #undef protected -#ifndef RPACKAGEMANAGER_H -#define RPACKAGEMANAGER_H +class RDeb822Source { +public: + RDeb822Source(); + RDeb822Source(const std::string& types, const std::string& uris, + const std::string& suites, const std::string& components = ""); -class RPackageManager { + bool isValid() const; + std::string toString() const; + static RDeb822Source fromString(const std::string& content); + bool operator==(const RDeb822Source& other) const; + bool operator!=(const RDeb822Source& other) const; - protected: + std::string getTypes() const { return types; } + std::string getUris() const { return uris; } + std::string getSuites() const { return suites; } + std::string getComponents() const { return components; } + std::string getSignedBy() const { return signedBy; } + bool isEnabled() const { return enabled; } - pkgPackageManager::OrderResult Res; + void setTypes(const std::string& t) { types = t; } + void setUris(const std::string& u) { uris = u; } + void setSuites(const std::string& s) { suites = s; } + void setComponents(const std::string& c) { components = c; } + void setSignedBy(const std::string& s) { signedBy = s; } - public: + static std::string trim(const std::string& str); - pkgPackageManager *pm; +private: + std::string types; + std::string uris; + std::string suites; + std::string components; + std::string signedBy; + bool enabled; +}; + +class RSourceManager { +public: + RSourceManager(); + explicit RSourceManager(const std::string& sourcesDir); + + bool addSource(const RDeb822Source& source); + bool removeSource(const RDeb822Source& source); + bool updateSource(const RDeb822Source& oldSource, const RDeb822Source& newSource); + std::vector getSources() const; + bool loadSources(); + bool saveSources() const; + bool updateAptSources(); + bool reloadAptCache(); + bool validateSourceFile(const std::string& filename) const; + +private: + std::string sourcesDir; + std::vector sources; + + std::vector parseSources(const std::string& content); + RDeb822Source createSourceFromFields(const std::map& fields); + bool writeSourceFile(const std::string& filename, const RDeb822Source& source) const; + RDeb822Source readSourceFile(const std::string& filename) const; + std::string getSourceFilename(const RDeb822Source& source) const; + std::string trim(const std::string& str) const; +}; + +class RPackageManager { +protected: + pkgPackageManager::OrderResult Res; + +public: + pkgPackageManager *pm; + + pkgPackageManager::OrderResult DoInstallPreFork() { + Res = pm->OrderInstall(); + return Res; + } - pkgPackageManager::OrderResult DoInstallPreFork() { - Res = pm->OrderInstall(); - return Res; - } #ifdef WITH_DPKG_STATUSFD - pkgPackageManager::OrderResult DoInstallPostFork(int statusFd=-1) { - return (pm->Go(statusFd) == false) ? pkgPackageManager::Failed : Res; - } + pkgPackageManager::OrderResult DoInstallPostFork(int statusFd=-1) { + return (pm->Go(statusFd) == false) ? pkgPackageManager::Failed : Res; + } #else - pkgPackageManager::OrderResult DoInstallPostFork() { - return (pm->Go() == false) ? pkgPackageManager::Failed : Res; - } + pkgPackageManager::OrderResult DoInstallPostFork() { + if (pm == NULL) + return pkgPackageManager::Failed; + return (pm->Go(NULL) == false) ? pkgPackageManager::Failed : Res; + } #endif - RPackageManager(pkgPackageManager *pm) : pm(pm) {} - + RPackageManager(pkgPackageManager *pm) : pm(pm) {} }; -#endif +#endif // RPACKAGEMANAGER_H // vim:ts=3:sw=3:et diff --git a/common/rsource_deb822.cc b/common/rsource_deb822.cc new file mode 100644 index 000000000..00dc524b1 --- /dev/null +++ b/common/rsource_deb822.cc @@ -0,0 +1,335 @@ +/* rsource_deb822.cc - Deb822 format sources support + * + * Copyright (c) 2025 Synaptic development team + */ + +#include "rsource_deb822.h" +#include +#include +#include +#include +#include +#include +#include +#include "i18n.h" +#include +#include + +bool RDeb822Source::ParseDeb822File(const std::string& path, std::vector& entries) { + std::ifstream file(path); + if (!file.is_open()) { + return false; + } + std::string line; + std::map fields; + int stanza_count = 0; + while (std::getline(file, line)) { + if (line.empty()) { + if (!fields.empty()) { + Deb822Entry entry; + // Check required fields + if (fields.find("Types") == fields.end() || fields.find("URIs") == fields.end() || fields.find("Suites") == fields.end()) { + fields.clear(); + continue; + } + entry.Types = fields["Types"]; + entry.URIs = fields["URIs"]; + entry.Suites = fields["Suites"]; + entry.Components = fields.count("Components") ? fields["Components"] : ""; + entry.SignedBy = fields.count("Signed-By") ? fields["Signed-By"] : ""; + // Handle Enabled/Disabled fields + if (fields.count("Enabled")) { + std::string enabled_val = fields["Enabled"]; + std::transform(enabled_val.begin(), enabled_val.end(), enabled_val.begin(), ::tolower); + entry.Enabled = (enabled_val == "yes" || enabled_val == "true" || enabled_val == "1"); + } else if (fields.count("Disabled")) { + std::string disabled_val = fields["Disabled"]; + std::transform(disabled_val.begin(), disabled_val.end(), disabled_val.begin(), ::tolower); + entry.Enabled = !(disabled_val == "yes" || disabled_val == "true" || disabled_val == "1"); + } else { + entry.Enabled = true; // Default to enabled + } + entries.push_back(entry); + stanza_count++; + fields.clear(); + } + continue; + } + if (line[0] == '#') { + continue; + } + size_t colon = line.find(':'); + if (colon == std::string::npos) { + continue; + } + std::string key = line.substr(0, colon); + std::string value = line.substr(colon + 1); + // Trim whitespace + key.erase(0, key.find_first_not_of(" \t")); + key.erase(key.find_last_not_of(" \t") + 1); + value.erase(0, value.find_first_not_of(" \t")); + value.erase(value.find_last_not_of(" \t") + 1); + fields[key] = value; + } + // Handle last stanza if file does not end with blank line + if (!fields.empty()) { + Deb822Entry entry; + if (fields.find("Types") == fields.end() || fields.find("URIs") == fields.end() || fields.find("Suites") == fields.end()) { + // No debug print, just skip + } else { + entry.Types = fields["Types"]; + entry.URIs = fields["URIs"]; + entry.Suites = fields["Suites"]; + entry.Components = fields.count("Components") ? fields["Components"] : ""; + entry.SignedBy = fields.count("Signed-By") ? fields["Signed-By"] : ""; + // Handle Enabled/Disabled fields + if (fields.count("Enabled")) { + std::string enabled_val = fields["Enabled"]; + std::transform(enabled_val.begin(), enabled_val.end(), enabled_val.begin(), ::tolower); + entry.Enabled = (enabled_val == "yes" || enabled_val == "true" || enabled_val == "1"); + } else if (fields.count("Disabled")) { + std::string disabled_val = fields["Disabled"]; + std::transform(disabled_val.begin(), disabled_val.end(), disabled_val.begin(), ::tolower); + entry.Enabled = !(disabled_val == "yes" || disabled_val == "true" || disabled_val == "1"); + } else { + entry.Enabled = true; // Default to enabled + } + entries.push_back(entry); + stanza_count++; + } + } + return true; +} + +bool RDeb822Source::WriteDeb822File(const std::string& path, const std::vector& entries) { + std::ofstream file(path); + if (!file) { + return _error->Error(_("Cannot write to %s"), path.c_str()); + } + + for (size_t i = 0; i < entries.size(); ++i) { + const auto& entry = entries[i]; + + // Write preserved comments before stanza + if (!entry.Comment.empty()) { + file << entry.Comment; + if (entry.Comment.back() != '\n') file << "\n"; + } + + // Write Enabled field + if (entry.Enabled) { + file << "Enabled: yes" << std::endl; + } else { + file << "Enabled: no" << std::endl; + } + + file << "Types: " << entry.Types << std::endl; + file << "URIs: " << entry.URIs << std::endl; + file << "Suites: " << entry.Suites << std::endl; + + if (!entry.Components.empty()) { + file << "Components: " << entry.Components << std::endl; + } + if (!entry.SignedBy.empty()) { + file << "Signed-By: " << entry.SignedBy << std::endl; + } + if (!entry.Architectures.empty()) { + file << "Architectures: " << entry.Architectures << std::endl; + } + if (!entry.Languages.empty()) { + file << "Languages: " << entry.Languages << std::endl; + } + if (!entry.Targets.empty()) { + file << "Targets: " << entry.Targets << std::endl; + } + + // Only add empty line between entries, not after the last one + if (i < entries.size() - 1) { + file << std::endl; + } + } + + return true; +} + +bool RDeb822Source::ConvertToSourceRecord(const Deb822Entry& entry, SourcesList::SourceRecord& record) { + // Parse types + bool has_deb = false; + bool has_deb_src = false; + std::istringstream typeStream(entry.Types); + std::string type; + while (std::getline(typeStream, type, ' ')) { + TrimWhitespace(type); + if (type == "deb") has_deb = true; + if (type == "deb-src") has_deb_src = true; + } + + record.Type = 0; + if (has_deb) record.Type |= SourcesList::Deb; + if (has_deb_src) record.Type |= SourcesList::DebSrc; + if (!entry.Enabled) record.Type |= SourcesList::Disabled; + + // Parse URIs + std::istringstream uriStream(entry.URIs); + std::string uri; + while (std::getline(uriStream, uri, ' ')) { + TrimWhitespace(uri); + if (!uri.empty()) { + record.URI = uri; + break; + } + } + + // Parse suites + std::istringstream suiteStream(entry.Suites); + std::string suite; + while (std::getline(suiteStream, suite, ' ')) { + TrimWhitespace(suite); + if (!suite.empty()) { + record.Dist = suite; + break; + } + } + + // Parse components + std::istringstream compStream(entry.Components); + std::string comp; + std::vector sections; + while (std::getline(compStream, comp, ' ')) { + TrimWhitespace(comp); + if (!comp.empty()) { + sections.push_back(comp); + } + } + + // Set sections + if (!sections.empty()) { + record.NumSections = sections.size(); + record.Sections = new string[record.NumSections]; + for (unsigned short i = 0; i < record.NumSections; i++) { + record.Sections[i] = sections[i]; + } + } + + // Preserve extra fields in Comment + std::stringstream commentStream; + if (!entry.SignedBy.empty()) { + commentStream << "Signed-By: " << entry.SignedBy << std::endl; + } + if (!entry.Architectures.empty()) { + commentStream << "Architectures: " << entry.Architectures << std::endl; + } + if (!entry.Languages.empty()) { + commentStream << "Languages: " << entry.Languages << std::endl; + } + if (!entry.Targets.empty()) { + commentStream << "Targets: " << entry.Targets << std::endl; + } + record.Comment = commentStream.str(); + + return true; +} + +bool RDeb822Source::ConvertFromSourceRecord(const SourcesList::SourceRecord& record, Deb822Entry& entry) { + // Set types + std::stringstream typeStream; + if (record.Type & SourcesList::Deb) { + typeStream << "deb "; + } + if (record.Type & SourcesList::DebSrc) { + typeStream << "deb-src "; + } + entry.Types = typeStream.str(); + TrimWhitespace(entry.Types); + + // Set URI + entry.URIs = record.URI; + + // Set suite + entry.Suites = record.Dist; + + // Set components + std::stringstream compStream; + for (unsigned short i = 0; i < record.NumSections; i++) { + compStream << record.Sections[i] << " "; + } + entry.Components = compStream.str(); + TrimWhitespace(entry.Components); + + // Set enabled state + entry.Enabled = !(record.Type & SourcesList::Disabled); + + // Parse extra fields from Comment + if (!record.Comment.empty()) { + std::istringstream iss(record.Comment); + std::string line; + while (std::getline(iss, line)) { + size_t colon = line.find(":"); + if (colon == std::string::npos) continue; + std::string key = line.substr(0, colon); + std::string value = line.substr(colon + 1); + TrimWhitespace(key); + TrimWhitespace(value); + if (key == "Signed-By") entry.SignedBy = value; + else if (key == "Architectures") entry.Architectures = value; + else if (key == "Languages") entry.Languages = value; + else if (key == "Targets") entry.Targets = value; + } + } + + return true; +} + +void RDeb822Source::TrimWhitespace(std::string& str) { + const std::string whitespace = " \t\r\n"; + size_t start = str.find_first_not_of(whitespace); + if (start == std::string::npos) { + str.clear(); + return; + } + size_t end = str.find_last_not_of(whitespace); + str = str.substr(start, end - start + 1); +} + +bool RDeb822Source::ParseStanza(std::ifstream& file, std::map& fields) { + std::string line; + bool inStanza = false; + + while (std::getline(file, line)) { + // Skip empty lines + if (line.empty()) { + if (inStanza) { + return true; + } + continue; + } + + // Skip comments + if (line[0] == '#') { + continue; + } + + // Check for stanza start + if (line.find("Types:") != std::string::npos) { + inStanza = true; + } + + if (inStanza) { + size_t colonPos = line.find(':'); + if (colonPos != std::string::npos) { + std::string key = line.substr(0, colonPos); + std::string value = line.substr(colonPos + 1); + + // Trim whitespace + key.erase(0, key.find_first_not_of(" \t")); + key.erase(key.find_last_not_of(" \t") + 1); + value.erase(0, value.find_first_not_of(" \t")); + value.erase(value.find_last_not_of(" \t") + 1); + + fields[key] = value; + } + } + } + + return !fields.empty(); +} \ No newline at end of file diff --git a/common/rsource_deb822.h b/common/rsource_deb822.h new file mode 100644 index 000000000..aa4dc6ab8 --- /dev/null +++ b/common/rsource_deb822.h @@ -0,0 +1,48 @@ +/* rsource_deb822.h - Deb822 format sources support + * + * Copyright (c) 2025 Synaptic development team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + */ + +#ifndef RSOURCE_DEB822_H +#define RSOURCE_DEB822_H + +#include +#include +#include +#include +#include +#include +#include +#include "rsources.h" + +class RDeb822Source { +public: + struct Deb822Entry { + std::string Types; // Space-separated list of types + std::string URIs; // Space-separated list of URIs + std::string Suites; // Space-separated list of suites + std::string Components; // Space-separated list of components + std::string SignedBy; // Path to keyring file + std::string Architectures; // Space-separated list of architectures + std::string Languages; // Space-separated list of languages + std::string Targets; // Space-separated list of targets + bool Enabled; // Whether the source is enabled + std::string Comment; // Any comments associated with this entry + }; + + static bool ParseDeb822File(const std::string& path, std::vector& entries); + static bool WriteDeb822File(const std::string& path, const std::vector& entries); + static bool ConvertToSourceRecord(const Deb822Entry& entry, SourcesList::SourceRecord& record); + static bool ConvertFromSourceRecord(const SourcesList::SourceRecord& record, Deb822Entry& entry); + static void TrimWhitespace(std::string& str); + +private: + static bool ParseStanza(std::ifstream& file, std::map& fields); +}; + +#endif // RSOURCE_DEB822_H \ No newline at end of file diff --git a/common/rsourcemanager.cc b/common/rsourcemanager.cc new file mode 100644 index 000000000..ab0e77c01 --- /dev/null +++ b/common/rsourcemanager.cc @@ -0,0 +1,205 @@ +#include "rsourcemanager.h" +#include +#include +#include + +RSourceManager::RSourceManager() : useDeb822Format(false) { + // Check if sources.list.d exists and contains .sources files + useDeb822Format = std::filesystem::exists("/etc/apt/sources.list.d") && + !std::filesystem::is_empty("/etc/apt/sources.list.d"); +} + +RSourceManager::~RSourceManager() { +} + +bool RSourceManager::loadSources() { + sources.clear(); + + // Try loading Deb822 sources first + if (useDeb822Format) { + if (loadDeb822Sources()) { + return true; + } + } + + // Fall back to legacy format + return loadLegacySources(); +} + +bool RSourceManager::loadDeb822Sources() { + const std::string sourcesDir = "/etc/apt/sources.list.d"; + + try { + for (const auto& entry : std::filesystem::directory_iterator(sourcesDir)) { + if (entry.path().extension() == ".sources") { + std::ifstream file(entry.path()); + if (!file.is_open()) continue; + + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + + RDeb822Source source = RDeb822Source::fromString(content); + if (source.isValid()) { + sources.push_back(source); + } + } + } + return !sources.empty(); + } catch (const std::exception& e) { + return false; + } +} + +bool RSourceManager::loadLegacySources() { + std::ifstream file("/etc/apt/sources.list"); + if (!file.is_open()) return false; + + std::string line; + while (std::getline(file, line)) { + line = trim(line); + if (line.empty() || line[0] == '#') continue; + + std::istringstream iss(line); + std::string type, uri, dist, components; + iss >> type >> uri >> dist; + + if (type.empty() || uri.empty() || dist.empty()) continue; + + RDeb822Source source; + source.setTypes(type); + source.setURIs(uri); + source.setSuites(dist); + + // Read components + std::string comp; + while (iss >> comp) { + if (!components.empty()) components += " "; + components += comp; + } + source.setComponents(components); + + sources.push_back(source); + } + + return true; +} + +bool RSourceManager::saveSources() { + if (useDeb822Format) { + return saveDeb822Sources(); + } + return saveLegacySources(); +} + +bool RSourceManager::saveDeb822Sources() { + const std::string sourcesDir = "/etc/apt/sources.list.d"; + + try { + // Create directory if it doesn't exist + if (!std::filesystem::exists(sourcesDir)) { + std::filesystem::create_directories(sourcesDir); + } + + // Save to debian.sources + std::ofstream file(sourcesDir + "/debian.sources"); + if (!file.is_open()) return false; + + for (const auto& source : sources) { + file << source.toString() << "\n\n"; + } + + return true; + } catch (const std::exception& e) { + return false; + } +} + +bool RSourceManager::saveLegacySources() { + std::ofstream file("/etc/apt/sources.list"); + if (!file.is_open()) return false; + + for (const auto& source : sources) { + if (!source.isEnabled()) continue; + + file << source.getTypes() << " " + << source.getURIs() << " " + << source.getSuites(); + + if (!source.getComponents().empty()) { + file << " " << source.getComponents(); + } + + if (!source.getSignedBy().empty()) { + file << " [signed-by=" << source.getSignedBy() << "]"; + } + + file << "\n"; + } + + return true; +} + +std::vector RSourceManager::getSources() const { + return sources; +} + +void RSourceManager::addSource(const RDeb822Source& source) { + sources.push_back(source); +} + +void RSourceManager::removeSource(const RDeb822Source& source) { + sources.erase( + std::remove_if(sources.begin(), sources.end(), + [&source](const RDeb822Source& s) { return s == source; }), + sources.end() + ); +} + +void RSourceManager::updateSource(const RDeb822Source& oldSource, const RDeb822Source& newSource) { + for (auto& source : sources) { + if (source == oldSource) { + source = newSource; + break; + } + } +} + +RDeb822Source RSourceManager::createSourceFromFields(const std::map& fields) { + RDeb822Source source; + + if (fields.count("Types")) source.setTypes(fields.at("Types")); + if (fields.count("URIs")) source.setURIs(fields.at("URIs")); + if (fields.count("Suites")) source.setSuites(fields.at("Suites")); + if (fields.count("Components")) source.setComponents(fields.at("Components")); + if (fields.count("Signed-By")) source.setSignedBy(fields.at("Signed-By")); + + return source; +} + +std::string RSourceManager::trim(const std::string& str) { + const std::string whitespace = " \t\r\n"; + size_t start = str.find_first_not_of(whitespace); + if (start == std::string::npos) { + return ""; + } + size_t end = str.find_last_not_of(whitespace); + return str.substr(start, end - start + 1); +} + +bool RSourceManager::shouldConvertToDeb822() const { + return !useDeb822Format && !sources.empty(); +} + +bool RSourceManager::askUserAboutConversion() { + GtkWidget* dialog = gtk_message_dialog_new(NULL, + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + "Would you like to convert your sources to the new Deb822 format?\n" + "This will create files in /etc/apt/sources.list.d/"); + + gint result = gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + return result == GTK_RESPONSE_YES; +} \ No newline at end of file diff --git a/common/rsourcemanager.h b/common/rsourcemanager.h new file mode 100644 index 000000000..3418a0204 --- /dev/null +++ b/common/rsourcemanager.h @@ -0,0 +1,26 @@ +class RSourceManager { +public: + RSourceManager(); + ~RSourceManager(); + + bool loadSources(); + bool saveSources(); + std::vector getSources() const; + void addSource(const RDeb822Source& source); + void removeSource(const RDeb822Source& source); + void updateSource(const RDeb822Source& oldSource, const RDeb822Source& newSource); + + static RDeb822Source createSourceFromFields(const std::map& fields); + static std::string trim(const std::string& str); + +private: + bool loadLegacySources(); + bool loadDeb822Sources(); + bool saveLegacySources(); + bool saveDeb822Sources(); + bool shouldConvertToDeb822() const; + bool askUserAboutConversion(); + + std::vector sources; + bool useDeb822Format; +}; \ No newline at end of file diff --git a/common/rsources.cc b/common/rsources.cc index 18d7ad9b4..a2b3320dd 100644 --- a/common/rsources.cc +++ b/common/rsources.cc @@ -36,6 +36,8 @@ #include #include "config.h" #include "i18n.h" +#include "rsource_deb822.h" +#include SourcesList::~SourcesList() { @@ -58,7 +60,6 @@ SourcesList::SourceRecord *SourcesList::AddSourceNode(SourceRecord &rec) bool SourcesList::ReadSourcePart(string listpath) { - //cout << "SourcesList::ReadSourcePart() "<< listpath << endl; char buf[512]; const char *p; ifstream ifs(listpath.c_str(), ios::in); @@ -78,6 +79,7 @@ bool SourcesList::ReadSourcePart(string listpath) ifs.getline(buf, sizeof(buf)); rec.SourceFile = listpath; + rec.PreserveOriginalURI = true; // Preserve original URI format when reading while (isspace(*p)) p++; if (*p == '#') { @@ -87,15 +89,10 @@ bool SourcesList::ReadSourcePart(string listpath) p++; } - if (*p == '\r' || *p == '\n' || *p == 0) { - rec.Type = Comment; - rec.Comment = p; - - AddSourceNode(rec); - continue; - } - + // Try to parse as a source line after '#'. If not valid, treat as comment. bool Failed = true; + string orig_buf = buf; + const char* orig_p = p; if (ParseQuoteWord(p, Type) == true && rec.SetType(Type) == true && ParseQuoteWord(p, VURI) == true) { if (VURI[0] == '[') { @@ -110,18 +107,19 @@ bool SourcesList::ReadSourcePart(string listpath) } if (Failed == true) { + // If this was a disabled line (started with '#'), but not a valid source, treat as comment if (rec.Type == Disabled) { - // treat as a comment field rec.Type = Comment; - rec.Comment = buf; + rec.Comment = orig_buf; } else { // syntax error on line rec.Type = Comment; string s = "#" + string(buf); rec.Comment = s; record_ok = false; - //return _error->Error(_("Syntax error in line %s"), buf); } + AddSourceNode(rec); + continue; } #ifndef HAVE_RPM // check for absolute dist @@ -170,8 +168,6 @@ bool SourcesList::ReadSourcePart(string listpath) bool SourcesList::ReadSourceDir(string Dir) { - //cout << "SourcesList::ReadSourceDir() " << Dir << endl; - DIR *D = opendir(Dir.c_str()); if (D == 0) return _error->Errno("opendir", _("Unable to read %s"), Dir.c_str()); @@ -217,16 +213,27 @@ bool SourcesList::ReadSourceDir(string Dir) bool SourcesList::ReadSources() { - //cout << "SourcesList::ReadSources() " << endl; - bool Res = true; + // Use config or fallback to /etc/apt/sources.list.d/ + string Deb822Parts = _config->FindDir("Dir::Etc::sourcelist.d"); + if (Deb822Parts.empty() || Deb822Parts == "/") + Deb822Parts = "/etc/apt/sources.list.d/"; + + if (FileExists(Deb822Parts) == true) { + Res &= ReadDeb822SourceDir(Deb822Parts); + } + + // Then read Deb822 format sources from sourceparts as well string Parts = _config->FindDir("Dir::Etc::sourceparts"); - if (FileExists(Parts) == true) + if (FileExists(Parts) == true) { + Res &= ReadDeb822SourceDir(Parts); Res &= ReadSourceDir(Parts); + } string Main = _config->FindFile("Dir::Etc::sourcelist"); - if (FileExists(Main) == true) + if (FileExists(Main) == true) { Res &= ReadSourcePart(Main); + } return Res; } @@ -291,50 +298,109 @@ void SourcesList::SwapSources( SourceRecord *&rec_one, SourceRecord *&rec_two ) bool SourcesList::UpdateSources() { - list filenames; - for (list::iterator it = SourceRecords.begin(); + // Group sources by their source file + map> sourcesByFile; + for (list::const_iterator it = SourceRecords.begin(); it != SourceRecords.end(); it++) { - if ((*it)->SourceFile == "") - continue; - filenames.push_front((*it)->SourceFile); + sourcesByFile[(*it)->SourceFile].push_back(*it); } - filenames.sort(); - filenames.unique(); - for (list::iterator fi = filenames.begin(); - fi != filenames.end(); fi++) { - ofstream ofs((*fi).c_str(), ios::out); - if (!ofs != 0) - return false; + // Write each source file + for (const auto& pair : sourcesByFile) { + const string& sourcePath = pair.first; + const vector& records = pair.second; - for (list::iterator it = SourceRecords.begin(); - it != SourceRecords.end(); it++) { - if ((*fi) != (*it)->SourceFile) - continue; - string S; - if (((*it)->Type & Comment) != 0) { - S = (*it)->Comment; - } else if ((*it)->URI.empty() || (*it)->Dist.empty()) { - continue; - } else { - if (((*it)->Type & Disabled) != 0) - S = "# "; + // Skip empty source files + if (records.empty()) { + continue; + } - S += (*it)->GetType() + " "; + // Trim trailing blank/comment lines (empty or whitespace-only comments) + std::vector trimmed_records = records; + while (!trimmed_records.empty()) { + SourceRecord* rec = trimmed_records.back(); + bool is_blank_comment = (rec->Type == Comment) && (rec->Comment.find_first_not_of(" \t\r\n") == std::string::npos); + if (is_blank_comment) { + trimmed_records.pop_back(); + } else { + break; + } + } - if ((*it)->VendorID.empty() == false) - S += "[" + (*it)->VendorID + "] "; + // Check if this is a Deb822 format file (only .sources files) + bool isDeb822 = false; + if (sourcePath.size() > 8 && sourcePath.substr(sourcePath.size() - 8) == ".sources") { + isDeb822 = true; + } - S += (*it)->URI + " "; - S += (*it)->Dist + " "; + // Open the appropriate file for writing + ofstream out(sourcePath.c_str(), ios::out); + if (!out) { + return _error->Error(_("Error writing to %s"), sourcePath.c_str()); + } - for (unsigned int J = 0; J < (*it)->NumSections; J++) - S += (*it)->Sections[J] + " "; + if (isDeb822) { + // Write Deb822 format + vector entries; + for (const auto& record : trimmed_records) { + RDeb822Source::Deb822Entry entry; + if (!RDeb822Source::ConvertFromSourceRecord(*record, entry)) { + return _error->Error(_("Failed to convert source record to Deb822 format")); + } + entries.push_back(entry); } - ofs << S << endl; + if (!RDeb822Source::WriteDeb822File(sourcePath, entries)) { + return false; + } + } else { + // Write classic format (deb lines) + for (const auto& record : trimmed_records) { + if (record->Type == Comment) { + out << record->Comment << endl; + } else { + // Write as a standard deb/deb-src line, comment if disabled + string line; + if (record->Type & Disabled) { + line += "# "; + } + if (record->Type & Deb) { + line += "deb "; + } else if (record->Type & DebSrc) { + line += "deb-src "; + } else if (record->Type & Rpm) { + line += "rpm "; + } else if (record->Type & RpmSrc) { + line += "rpm-src "; + } else if (record->Type & RpmDir) { + line += "rpm-dir "; + } else if (record->Type & RpmSrcDir) { + line += "rpm-src-dir "; + } else if (record->Type & Repomd) { + line += "repomd "; + } else if (record->Type & RepomdSrc) { + line += "repomd-src "; + } else { + line += "deb "; // fallback + } + line += record->URI + " " + record->Dist; + for (unsigned int J = 0; J < record->NumSections; J++) { + line += " " + record->Sections[J]; + } + // Trim trailing space + if (!line.empty() && line[line.length()-1] == ' ') { + line.erase(line.length()-1); + } + out << line << endl; + } + } + } + + out.close(); + if (!out) { + return _error->Error(_("Error writing to %s"), sourcePath.c_str()); } - ofs.close(); } + return true; } @@ -395,8 +461,8 @@ bool SourcesList::SourceRecord::SetURI(string S) S = SubstVar(S, "$(VERSION)", _config->Find("APT::DistroVersion")); URI = S; - // append a / to the end if one is not already there - if (URI[URI.size() - 1] != '/') + // Only append / if we're not preserving the original format + if (!PreserveOriginalURI && URI[URI.size() - 1] != '/') URI += '/'; return true; @@ -416,6 +482,7 @@ operator=(const SourceRecord &rhs) NumSections = rhs.NumSections; Comment = rhs.Comment; SourceFile = rhs.SourceFile; + PreserveOriginalURI = rhs.PreserveOriginalURI; return *this; } @@ -516,38 +583,36 @@ void SourcesList::RemoveVendor(VendorRecord *&rec) ostream &operator<<(ostream &os, const SourcesList::SourceRecord &rec) { - os << "Type: "; - if ((rec.Type & SourcesList::Comment) != 0) - os << "Comment "; - if ((rec.Type & SourcesList::Disabled) != 0) - os << "Disabled "; - if ((rec.Type & SourcesList::Deb) != 0) - os << "Deb"; - if ((rec.Type & SourcesList::DebSrc) != 0) - os << "DebSrc"; - if ((rec.Type & SourcesList::Rpm) != 0) - os << "Rpm"; - if ((rec.Type & SourcesList::RpmSrc) != 0) - os << "RpmSrc"; - if ((rec.Type & SourcesList::RpmDir) != 0) - os << "RpmDir"; - if ((rec.Type & SourcesList::RpmSrcDir) != 0) - os << "RpmSrcDir"; - if ((rec.Type & SourcesList::Repomd) != 0) - os << "Repomd"; - if ((rec.Type & SourcesList::RepomdSrc) != 0) - os << "RepomdSrc"; - os << endl; - os << "SourceFile: " << rec.SourceFile << endl; - os << "VendorID: " << rec.VendorID << endl; - os << "URI: " << rec.URI << endl; - os << "Dist: " << rec.Dist << endl; - os << "Section(s):" << endl; -#if 0 + if (rec.Type == SourcesList::Comment) { + os << rec.Comment << endl; + return os; + } + if (rec.Type & SourcesList::Disabled) { + os << "# "; + } + if (rec.Type & SourcesList::Deb) { + os << "deb "; + } else if (rec.Type & SourcesList::DebSrc) { + os << "deb-src "; + } else if (rec.Type & SourcesList::Rpm) { + os << "rpm "; + } else if (rec.Type & SourcesList::RpmSrc) { + os << "rpm-src "; + } else if (rec.Type & SourcesList::RpmDir) { + os << "rpm-dir "; + } else if (rec.Type & SourcesList::RpmSrcDir) { + os << "rpm-src-dir "; + } else if (rec.Type & SourcesList::Repomd) { + os << "repomd "; + } else if (rec.Type & SourcesList::RepomdSrc) { + os << "repomd-src "; + } else { + os << "deb "; // fallback + } + os << rec.URI << " " << rec.Dist; for (unsigned int J = 0; J < rec.NumSections; J++) { - cout << "\t" << rec.Sections[J] << endl; + os << " " << rec.Sections[J]; } -#endif os << endl; return os; } @@ -560,4 +625,74 @@ ostream &operator<<(ostream &os, const SourcesList::VendorRecord &rec) return os; } +bool SourcesList::ReadDeb822SourcePart(string listpath) { + vector entries; + if (!RDeb822Source::ParseDeb822File(listpath, entries)) { + return false; + } + + for (const auto& entry : entries) { + SourceRecord rec; + rec.SourceFile = listpath; + + if (!RDeb822Source::ConvertToSourceRecord(entry, rec)) { + return _error->Error(_("Failed to convert Deb822 entry in %s"), listpath.c_str()); + } + + rec.Type |= Deb822; // Mark as Deb822 format + AddSourceNode(rec); + } + + return true; +} + +bool SourcesList::ReadDeb822SourceDir(string Dir) { + DIR *D = opendir(Dir.c_str()); + if (D == 0) + return _error->Errno("opendir", _( "Unable to read %s"), Dir.c_str()); + + vector List; + for (struct dirent * Ent = readdir(D); Ent != 0; Ent = readdir(D)) { + if (Ent->d_name[0] == '.') + continue; + + // Only look at files ending in .sources + if (strcmp(Ent->d_name + strlen(Ent->d_name) - 8, ".sources") != 0) + continue; + + // Make sure it is a file and not something else + string File = flCombine(Dir, Ent->d_name); + struct stat St; + if (stat(File.c_str(), &St) != 0 || S_ISREG(St.st_mode) == 0) + continue; + List.push_back(File); + } + closedir(D); + + sort(List.begin(), List.end()); + + // Read the files + for (vector::const_iterator I = List.begin(); I != List.end(); I++) { + if (ReadDeb822SourcePart(*I) == false) + return false; + } + return true; +} + +bool SourcesList::WriteDeb822Source(SourceRecord *record, string path) { + if (!record || !(record->Type & Deb822)) { + return _error->Error(_("Not a Deb822 format source")); + } + + vector entries; + RDeb822Source::Deb822Entry entry; + + if (!RDeb822Source::ConvertFromSourceRecord(*record, entry)) { + return _error->Error(_("Failed to convert source record to Deb822 format")); + } + + entries.push_back(entry); + return RDeb822Source::WriteDeb822File(path, entries); +} + // vim:sts=4:sw=4 diff --git a/common/rsources.h b/common/rsources.h index b2e16d49c..5ffdd6df3 100644 --- a/common/rsources.h +++ b/common/rsources.h @@ -43,7 +43,8 @@ class SourcesList { RpmDir = 1 << 6, RpmSrcDir = 1 << 7, Repomd = 1 << 8, - RepomdSrc = 1 << 9 + RepomdSrc = 1 << 9, + Deb822 = 1 << 10 // New type for Deb822 format }; struct SourceRecord { @@ -55,12 +56,13 @@ class SourcesList { unsigned short NumSections; string Comment; string SourceFile; + bool PreserveOriginalURI; // Flag to preserve original URI format bool SetType(string); string GetType(); bool SetURI(string); - SourceRecord():Type(0), Sections(0), NumSections(0) { + SourceRecord():Type(0), Sections(0), NumSections(0), PreserveOriginalURI(false) { } ~SourceRecord() { if (Sections) @@ -97,6 +99,11 @@ class SourcesList { bool ReadSources(); bool UpdateSources(); + // New methods for Deb822 support + bool ReadDeb822SourcePart(string listpath); + bool ReadDeb822SourceDir(string Dir); + bool WriteDeb822Source(SourceRecord *record, string path); + VendorRecord *AddVendor(string VendorID, string FingerPrint, string Description); void RemoveVendor(VendorRecord *&); diff --git a/gtk/rgrepositorywin.cc b/gtk/rgrepositorywin.cc index 9221c5c6d..732faca4b 100644 --- a/gtk/rgrepositorywin.cc +++ b/gtk/rgrepositorywin.cc @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include @@ -36,6 +38,7 @@ #include "rgutils.h" #include "config.h" #include "i18n.h" +#include "rsource_deb822.h" #if HAVE_RPM enum { ITEM_TYPE_RPM, @@ -128,6 +131,7 @@ RGRepositoryEditor::RGRepositoryEditor(RGWindow *parent) _userDialog = new RGUserDialog(_win); _applied = false; _lastIter = NULL; + _config = new Configuration(); setTitle(_("Repositories")); gtk_window_set_modal(GTK_WINDOW(_win), TRUE); @@ -386,6 +390,7 @@ RGRepositoryEditor::~RGRepositoryEditor() { //gtk_widget_destroy(_win); delete _userDialog; + delete _config; } @@ -394,13 +399,14 @@ bool RGRepositoryEditor::Run() if (_lst.ReadSources() == false) { _userDialog-> warning(_("Ignoring invalid record(s) in sources.list file!")); - //return false; } // keep a backup of the orginal list _savedList.ReadSources(); + // Add debug print statement here + g_print("DEBUG: Number of source records read into _lst: %lu\n", _lst.SourceRecords.size()); + if (_lst.ReadVendors() == false) { - _error->Error(_("Cannot read vendors.list file")); _userDialog->showErrors(); return false; } @@ -413,17 +419,39 @@ bool RGRepositoryEditor::Run() it != _lst.SourceRecords.end(); it++) { if ((*it)->Type & SourcesList::Comment) continue; + + // Add debug print for each source being added to the display + g_print("DEBUG: Adding source to display - URI: %s, Type: %s\n", (*it)->URI.c_str(), (*it)->GetType().c_str()); + string Sections; for (unsigned int J = 0; J < (*it)->NumSections; J++) { Sections += (*it)->Sections[J]; Sections += " "; } + // --- NEW: Show both deb and deb-src for Deb822 stanzas --- + std::string type_display; + bool is_deb = ((*it)->Type & SourcesList::Deb) != 0; + bool is_debsrc = ((*it)->Type & SourcesList::DebSrc) != 0; + if (is_deb && is_debsrc) { + type_display = "deb, deb-src"; + } else if (is_deb) { + type_display = "deb"; + } else if (is_debsrc) { + type_display = "deb-src"; + } else { + type_display = (*it)->GetType(); + } + // --- END NEW --- + + // Add another debug print before appending to the list store + g_print("DEBUG: Preparing to append to list store for URI: %s\n", (*it)->URI.c_str()); + gtk_list_store_append(_sourcesListStore, &iter); gtk_list_store_set(_sourcesListStore, &iter, STATUS_COLUMN, !((*it)->Type & SourcesList::Disabled), - TYPE_COLUMN, utf8((*it)->GetType().c_str()), + TYPE_COLUMN, utf8(type_display.c_str()), VENDOR_COLUMN, utf8((*it)->VendorID.c_str()), URI_COLUMN, utf8((*it)->URI.c_str()), DISTRIBUTION_COLUMN, utf8((*it)->Dist.c_str()), @@ -432,6 +460,9 @@ bool RGRepositoryEditor::Run() DISABLED_COLOR_COLUMN, (*it)->Type & SourcesList::Disabled ? &_gray : NULL, -1); + + // Add debug print after setting data in the list store + g_print("DEBUG: Successfully set data for URI: %s\n", (*it)->URI.c_str()); } @@ -525,7 +556,6 @@ void RGRepositoryEditor::doEdit() { //cout << "RGRepositoryEditor::doEdit()"<Type & SourcesList::Deb822) != 0; + // --- END PATCH --- + rec->Type = 0; gboolean status; gtk_tree_model_get(GTK_TREE_MODEL(_sourcesListStore), _lastIter, @@ -548,42 +582,21 @@ void RGRepositoryEditor::doEdit() if (!status) rec->Type |= SourcesList::Disabled; - GtkTreeIter item; - int type; - gtk_combo_box_get_active_iter(GTK_COMBO_BOX(_optType), &item); - gtk_tree_model_get(GTK_TREE_MODEL(_optTypeMenu), &item, - 1, &type, - -1); - - switch (type) { - case ITEM_TYPE_DEB: - rec->Type |= SourcesList::Deb; - break; - case ITEM_TYPE_DEBSRC: - rec->Type |= SourcesList::DebSrc; - break; - case ITEM_TYPE_RPM: - rec->Type |= SourcesList::Rpm; - break; - case ITEM_TYPE_RPMSRC: - rec->Type |= SourcesList::RpmSrc; - break; - case ITEM_TYPE_RPMDIR: - rec->Type |= SourcesList::RpmDir; - break; - case ITEM_TYPE_RPMSRCDIR: - rec->Type |= SourcesList::RpmSrcDir; - break; - case ITEM_TYPE_REPOMD: - rec->Type |= SourcesList::Repomd; - break; - case ITEM_TYPE_REPOMDSRC: - rec->Type |= SourcesList::RepomdSrc; - break; - default: - _userDialog->error(_("Unknown source type")); - return; - } + // --- NEW: For Deb822, allow both deb and deb-src to be set --- + // Parse the type_display string from the TYPE_COLUMN + gchar* type_str = NULL; + gtk_tree_model_get(GTK_TREE_MODEL(_sourcesListStore), _lastIter, TYPE_COLUMN, &type_str, -1); + std::string type_val = type_str ? type_str : ""; + g_free(type_str); + bool set_deb = (type_val.find("deb") != std::string::npos); + bool set_debsrc = (type_val.find("deb-src") != std::string::npos); + if (set_deb) rec->Type |= SourcesList::Deb; + if (set_debsrc) rec->Type |= SourcesList::DebSrc; + // --- END NEW --- + + // --- PATCH: Restore Deb822 flag if it was set --- + if (was_deb822) rec->Type |= SourcesList::Deb822; + // --- END PATCH --- #if 0 // PORTME, no vendor id support right now gtk_combo_box_get_active_iter(GTK_COMBO_BOX(_optVendor), &item); @@ -599,15 +612,26 @@ void RGRepositoryEditor::doEdit() rec->NumSections = 0; const char *Section = gtk_entry_get_text(GTK_ENTRY(_entrySect)); - if (Section != 0 && Section[0] != 0) - rec->NumSections++; - - rec->Sections = new string[rec->NumSections]; - rec->NumSections = 0; - Section = gtk_entry_get_text(GTK_ENTRY(_entrySect)); - - if (Section != 0 && Section[0] != 0) - rec->Sections[rec->NumSections++] = Section; + if (Section != 0 && Section[0] != 0) { + // Parse sections properly - split by spaces + string sectionsStr = Section; + vector sections; + stringstream ss(sectionsStr); + string section; + + while (ss >> section) { + sections.push_back(section); + } + + rec->NumSections = sections.size(); + rec->Sections = new string[rec->NumSections]; + for (unsigned int I = 0; I < rec->NumSections; I++) { + rec->Sections[I] = sections[I]; + } + } else { + rec->Sections = new string[0]; + rec->NumSections = 0; + } string Sect; for (unsigned int I = 0; I < rec->NumSections; I++) { @@ -804,3 +828,54 @@ void RGRepositoryEditor::DoUpDown(GtkWidget *self, gpointer data) else me->_lst.SwapSources(rec, rec_p); } + +bool RGRepositoryEditor::ConvertToDeb822() { + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(_win), + (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + _("Convert to Deb822 format?")); + + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), + _("This will convert your sources to the new Deb822 format.\n" + "The conversion will be done in-place and cannot be undone.\n\n" + "Do you want to proceed?")); + + gint result = gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + if (result != GTK_RESPONSE_YES) { + return false; +} + + // Convert each source record to Deb822 format + for (SourcesListIter I = _lst.SourceRecords.begin(); I != _lst.SourceRecords.end(); I++) { + SourcesList::SourceRecord *rec = *I; + if (rec == NULL) continue; + + // Create Deb822 entry + RDeb822Source::Deb822Entry entry; + if (!RDeb822Source::ConvertFromSourceRecord(*rec, entry)) { + _userDialog->error(_("Failed to convert source record to Deb822 format")); + return false; + } + + // Update the source record + if (!RDeb822Source::ConvertToSourceRecord(entry, *rec)) { + _userDialog->error(_("Failed to update source record with Deb822 format")); + return false; + } + } + + return true; +} + +void RGRepositoryEditor::SaveClicked() { + // Remove auto-conversion to Deb822. Only update sources. + if (!_lst.UpdateSources()) { + _userDialog->error(_("Failed to update sources list")); + return; + } + + _dirty = false; +} diff --git a/gtk/rgrepositorywin.h b/gtk/rgrepositorywin.h index a885a887e..294794402 100644 --- a/gtk/rgrepositorywin.h +++ b/gtk/rgrepositorywin.h @@ -30,8 +30,8 @@ #include #include "rsources.h" #include "rggtkbuilderwindow.h" - #include "rguserdialog.h" +#include typedef list::iterator SourcesListIter; typedef list::iterator VendorsListIter; @@ -66,6 +66,9 @@ class RGRepositoryEditor:RGGtkBuilderWindow { bool _dirty; GdkColor _gray; + // Configuration + Configuration *_config; + void UpdateVendorMenu(); int VendorMenuIndex(string VendorID); @@ -86,12 +89,23 @@ class RGRepositoryEditor:RGGtkBuilderWindow { // get values void doEdit(); - public: RGRepositoryEditor(RGWindow *parent); ~RGRepositoryEditor(); bool Run(); + + // Deb822 support + bool ConvertToDeb822(); + void SaveClicked(); +}; + +class RGRepositoryWin { +public: + // ... existing declarations ... + +private: + // ... existing private members ... }; #endif diff --git a/po/POTFILES.in b/po/POTFILES.in index 0f3ec8df9..f94466254 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -17,6 +17,7 @@ common/rpmindexcopy.cc common/rpackageview.h common/rpackageview.cc common/rsources.cc +common/rsource_deb822.cc gtk/gsynaptic.cc gtk/rgcdscanner.cc gtk/rgcacheprogress.cc diff --git a/tests/test_deb822_integration.cc b/tests/test_deb822_integration.cc new file mode 100644 index 000000000..a689cd0ea --- /dev/null +++ b/tests/test_deb822_integration.cc @@ -0,0 +1,324 @@ +#include +#include "../common/rsources.h" +#include "../common/rsource_deb822.h" +#include +#include +#include +#include + +class Deb822Test : public ::testing::Test { +protected: + void SetUp() override { + // Create a temporary file for testing + char tmpname[] = "/tmp/synaptic_test_XXXXXX"; + int fd = mkstemp(tmpname); + ASSERT_NE(fd, -1); + close(fd); + testFile = tmpname; + } + + void TearDown() override { + if (!testFile.empty()) { + remove(testFile.c_str()); + } + } + + std::string testFile; +}; + +TEST_F(Deb822Test, ParseSimpleDeb822Source) { + // Write a test source file + std::ofstream ofs(testFile.c_str()); + ASSERT_TRUE(ofs.is_open()); + ofs << "Types: deb\n" + << "URIs: http://deb.debian.org/debian\n" + << "Suites: trixie\n" + << "Components: main contrib\n" + << "Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg\n\n"; + ofs.close(); + + std::vector entries; + ASSERT_TRUE(RDeb822Source::ParseDeb822File(testFile, entries)); + ASSERT_EQ(entries.size(), 1); + + const auto& entry = entries[0]; + EXPECT_EQ(entry.Types, "deb"); + EXPECT_EQ(entry.URIs, "http://deb.debian.org/debian"); + EXPECT_EQ(entry.Suites, "trixie"); + EXPECT_EQ(entry.Components, "main contrib"); + EXPECT_EQ(entry.SignedBy, "/usr/share/keyrings/debian-archive-keyring.gpg"); + EXPECT_TRUE(entry.Enabled); +} + +TEST_F(Deb822Test, ParseMultipleEntries) { + std::ofstream ofs(testFile.c_str()); + ASSERT_TRUE(ofs.is_open()); + ofs << "Types: deb\n" + << "URIs: http://security.debian.org/debian-security\n" + << "Suites: trixie-security\n" + << "Components: main\n\n" + << "Types: deb deb-src\n" + << "URIs: http://deb.debian.org/debian\n" + << "Suites: trixie trixie-updates\n" + << "Components: main contrib non-free\n" + << "Enabled: no\n\n"; + ofs.close(); + + std::vector entries; + ASSERT_TRUE(RDeb822Source::ParseDeb822File(testFile, entries)); + ASSERT_EQ(entries.size(), 2); + + EXPECT_EQ(entries[0].Types, "deb"); + EXPECT_TRUE(entries[0].Enabled); + + EXPECT_EQ(entries[1].Types, "deb deb-src"); + EXPECT_FALSE(entries[1].Enabled); +} + +TEST_F(Deb822Test, ConvertBetweenFormats) { + // Create a Deb822 entry + RDeb822Source::Deb822Entry deb822Entry; + deb822Entry.Types = "deb deb-src"; + deb822Entry.URIs = "http://deb.debian.org/debian"; + deb822Entry.Suites = "trixie"; + deb822Entry.Components = "main contrib"; + deb822Entry.SignedBy = "/usr/share/keyrings/debian-archive-keyring.gpg"; + deb822Entry.Enabled = true; + + // Convert to SourceRecord + SourcesList::SourceRecord sourceRecord; + ASSERT_TRUE(RDeb822Source::ConvertToSourceRecord(deb822Entry, sourceRecord)); + + // Verify conversion + EXPECT_TRUE(sourceRecord.Type & SourcesList::Deb); + EXPECT_TRUE(sourceRecord.Type & SourcesList::DebSrc); + EXPECT_EQ(sourceRecord.URI, "http://deb.debian.org/debian"); + EXPECT_EQ(sourceRecord.Dist, "trixie"); + ASSERT_EQ(sourceRecord.NumSections, 2); + EXPECT_EQ(sourceRecord.Sections[0], "main"); + EXPECT_EQ(sourceRecord.Sections[1], "contrib"); + + // Convert back to Deb822 + RDeb822Source::Deb822Entry convertedEntry; + ASSERT_TRUE(RDeb822Source::ConvertFromSourceRecord(sourceRecord, convertedEntry)); + + // Verify round-trip conversion + EXPECT_EQ(convertedEntry.Types, "deb deb-src"); + EXPECT_EQ(convertedEntry.URIs, "http://deb.debian.org/debian"); + EXPECT_EQ(convertedEntry.Suites, "trixie"); + EXPECT_EQ(convertedEntry.Components, "main contrib"); + EXPECT_TRUE(convertedEntry.Enabled); +} + +TEST_F(Deb822Test, WriteAndReadBack) { + // Create test entries + std::vector entries; + RDeb822Source::Deb822Entry entry1; + entry1.Types = "deb"; + entry1.URIs = "http://example.com/debian"; + entry1.Suites = "stable"; + entry1.Components = "main"; + entry1.Enabled = true; + entries.push_back(entry1); + + // Write to file + ASSERT_TRUE(RDeb822Source::WriteDeb822File(testFile, entries)); + + // Read back + std::vector readEntries; + ASSERT_TRUE(RDeb822Source::ParseDeb822File(testFile, readEntries)); + + // Compare + ASSERT_EQ(readEntries.size(), entries.size()); + EXPECT_EQ(readEntries[0].Types, entries[0].Types); + EXPECT_EQ(readEntries[0].URIs, entries[0].URIs); + EXPECT_EQ(readEntries[0].Suites, entries[0].Suites); + EXPECT_EQ(readEntries[0].Components, entries[0].Components); + EXPECT_EQ(readEntries[0].Enabled, entries[0].Enabled); +} + +TEST_F(Deb822Test, HandleComments) { + std::ofstream ofs(testFile.c_str()); + ASSERT_TRUE(ofs.is_open()); + ofs << "# This is a comment\n" + << "Types: deb\n" + << "URIs: http://example.com/debian\n" + << "# Another comment\n" + << "Suites: stable\n" + << "Components: main\n\n"; + ofs.close(); + + std::vector entries; + ASSERT_TRUE(RDeb822Source::ParseDeb822File(testFile, entries)); + ASSERT_EQ(entries.size(), 1); + EXPECT_EQ(entries[0].Comment, "# This is a comment\n# Another comment"); +} + +TEST_F(Deb822Test, HandleInvalidFile) { + std::ofstream ofs(testFile.c_str()); + ASSERT_TRUE(ofs.is_open()); + ofs << "Types: deb\n" + << "URIs: http://example.com/debian\n" + << "# Missing Suites field\n" + << "Components: main\n\n"; + ofs.close(); + + std::vector entries; + EXPECT_FALSE(RDeb822Source::ParseDeb822File(testFile, entries)); +} + +TEST_F(Deb822Test, SourcesListIntegration) { + // Write a test Deb822 source file + std::ofstream ofs(testFile.c_str()); + ASSERT_TRUE(ofs.is_open()); + ofs << "Types: deb deb-src\n" + << "URIs: http://deb.debian.org/debian\n" + << "Suites: trixie\n" + << "Components: main contrib\n" + << "Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg\n\n"; + ofs.close(); + + // Create SourcesList and read the file + SourcesList sources; + ASSERT_TRUE(sources.ReadDeb822SourcePart(testFile)); + + // Verify the source was read correctly + ASSERT_FALSE(sources.SourceRecords.empty()); + auto record = sources.SourceRecords.front(); + EXPECT_TRUE(record->Type & SourcesList::Deb822); + EXPECT_TRUE(record->Type & SourcesList::Deb); + EXPECT_TRUE(record->Type & SourcesList::DebSrc); + EXPECT_EQ(record->URI, "http://deb.debian.org/debian"); + EXPECT_EQ(record->Dist, "trixie"); + ASSERT_EQ(record->NumSections, 2); + EXPECT_EQ(record->Sections[0], "main"); + EXPECT_EQ(record->Sections[1], "contrib"); + + // Write back to a new file + std::string newFile = testFile + ".new"; + ASSERT_TRUE(sources.WriteDeb822Source(record, newFile)); + + // Read the new file and verify contents + std::vector entries; + ASSERT_TRUE(RDeb822Source::ParseDeb822File(newFile, entries)); + ASSERT_EQ(entries.size(), 1); + EXPECT_EQ(entries[0].Types, "deb deb-src"); + EXPECT_EQ(entries[0].URIs, "http://deb.debian.org/debian"); + EXPECT_EQ(entries[0].Suites, "trixie"); + EXPECT_EQ(entries[0].Components, "main contrib"); + + // Clean up + remove(newFile.c_str()); +} + +TEST_F(Deb822Test, ParseAdditionalFields) { + std::ofstream ofs(testFile.c_str()); + ASSERT_TRUE(ofs.is_open()); + ofs << "Types: deb deb-src\n" + << "URIs: http://deb.debian.org/debian\n" + << "Suites: trixie\n" + << "Components: main contrib\n" + << "Architectures: amd64 arm64\n" + << "Languages: en fr de\n" + << "Targets: stable-updates\n" + << "Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg\n\n"; + ofs.close(); + + std::vector entries; + ASSERT_TRUE(RDeb822Source::ParseDeb822File(testFile, entries)); + ASSERT_EQ(entries.size(), 1); + + const auto& entry = entries[0]; + EXPECT_EQ(entry.Types, "deb deb-src"); + EXPECT_EQ(entry.URIs, "http://deb.debian.org/debian"); + EXPECT_EQ(entry.Suites, "trixie"); + EXPECT_EQ(entry.Components, "main contrib"); + EXPECT_EQ(entry.Architectures, "amd64 arm64"); + EXPECT_EQ(entry.Languages, "en fr de"); + EXPECT_EQ(entry.Targets, "stable-updates"); + EXPECT_EQ(entry.SignedBy, "/usr/share/keyrings/debian-archive-keyring.gpg"); + EXPECT_TRUE(entry.Enabled); +} + +TEST_F(Deb822Test, ConvertAdditionalFields) { + // Create a Deb822 entry with additional fields + RDeb822Source::Deb822Entry deb822Entry; + deb822Entry.Types = "deb deb-src"; + deb822Entry.URIs = "http://deb.debian.org/debian"; + deb822Entry.Suites = "trixie"; + deb822Entry.Components = "main contrib"; + deb822Entry.Architectures = "amd64 arm64"; + deb822Entry.Languages = "en fr de"; + deb822Entry.Targets = "stable-updates"; + deb822Entry.SignedBy = "/usr/share/keyrings/debian-archive-keyring.gpg"; + deb822Entry.Enabled = true; + + // Convert to SourceRecord + SourcesList::SourceRecord sourceRecord; + ASSERT_TRUE(RDeb822Source::ConvertToSourceRecord(deb822Entry, sourceRecord)); + + // Verify conversion + EXPECT_TRUE(sourceRecord.Type & SourcesList::Deb); + EXPECT_TRUE(sourceRecord.Type & SourcesList::DebSrc); + EXPECT_EQ(sourceRecord.URI, "http://deb.debian.org/debian"); + EXPECT_EQ(sourceRecord.Dist, "trixie"); + ASSERT_EQ(sourceRecord.NumSections, 2); + EXPECT_EQ(sourceRecord.Sections[0], "main"); + EXPECT_EQ(sourceRecord.Sections[1], "contrib"); + + // Check that additional fields are stored in Comment + EXPECT_TRUE(sourceRecord.Comment.find("Architectures: amd64 arm64") != std::string::npos); + EXPECT_TRUE(sourceRecord.Comment.find("Languages: en fr de") != std::string::npos); + EXPECT_TRUE(sourceRecord.Comment.find("Targets: stable-updates") != std::string::npos); + EXPECT_TRUE(sourceRecord.Comment.find("Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg") != std::string::npos); + + // Convert back to Deb822 + RDeb822Source::Deb822Entry convertedEntry; + ASSERT_TRUE(RDeb822Source::ConvertFromSourceRecord(sourceRecord, convertedEntry)); + + // Verify round-trip conversion + EXPECT_EQ(convertedEntry.Types, "deb deb-src"); + EXPECT_EQ(convertedEntry.URIs, "http://deb.debian.org/debian"); + EXPECT_EQ(convertedEntry.Suites, "trixie"); + EXPECT_EQ(convertedEntry.Components, "main contrib"); + EXPECT_EQ(convertedEntry.Architectures, "amd64 arm64"); + EXPECT_EQ(convertedEntry.Languages, "en fr de"); + EXPECT_EQ(convertedEntry.Targets, "stable-updates"); + EXPECT_EQ(convertedEntry.SignedBy, "/usr/share/keyrings/debian-archive-keyring.gpg"); + EXPECT_TRUE(convertedEntry.Enabled); +} + +TEST_F(Deb822Test, WriteAndReadAdditionalFields) { + // Create temporary file + std::string tempFile = "temp_test.sources"; + std::ofstream out(tempFile); + out << "Types: deb deb-src\n" + << "URIs: http://deb.debian.org/debian\n" + << "Suites: trixie\n" + << "Components: main contrib\n" + << "Architectures: amd64 arm64\n" + << "Languages: en fr de\n" + << "Targets: stable-updates\n" + << "Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg\n"; + out.close(); + + // Read it back + std::vector entries; + std::string error; + EXPECT_TRUE(RDeb822Source::ParseDeb822File(tempFile, entries, error)) << error; + EXPECT_EQ(entries.size(), 1); + + // Verify fields + const auto& entry = entries[0]; + EXPECT_EQ(entry.Types, "deb deb-src"); + EXPECT_EQ(entry.URIs, "http://deb.debian.org/debian"); + EXPECT_EQ(entry.Suites, "trixie"); + EXPECT_EQ(entry.Components, "main contrib"); + EXPECT_EQ(entry.Architectures, "amd64 arm64"); + EXPECT_EQ(entry.Languages, "en fr de"); + EXPECT_EQ(entry.Targets, "stable-updates"); + EXPECT_EQ(entry.SignedBy, "/usr/share/keyrings/debian-archive-keyring.gpg"); + + // Clean up + std::remove(tempFile.c_str()); +} \ No newline at end of file