winbrew_database\catalog/
installers.rs1use anyhow::{Context, Result};
2use rusqlite::{Connection, params};
3
4use super::row::conversion_err;
5use crate::models::catalog::installer_type::CatalogInstallerType;
6use crate::models::catalog::package::CatalogInstaller;
7use crate::models::catalog::raw::RawCatalogInstaller;
8use crate::models::shared::HashAlgorithm;
9
10pub(crate) fn get_installers(conn: &Connection, package_id: &str) -> Result<Vec<CatalogInstaller>> {
19 let mut stmt = conn.prepare(
20 "SELECT package_id, url, hash, hash_algorithm, installer_type, installer_switches, platform, commands, protocols, file_extensions, capabilities, scope, arch, kind, nested_kind
21 FROM catalog_installers
22 WHERE package_id = ?1
23 ORDER BY url ASC, hash ASC, hash_algorithm ASC, installer_type ASC, installer_switches ASC, scope ASC, arch ASC, kind ASC, nested_kind ASC",
24 )?;
25
26 stmt.query_map(params![package_id], row_to_installer)?
27 .collect::<std::result::Result<Vec<_>, _>>()
28 .context("failed to read catalog installer")
29}
30
31fn row_to_installer(row: &rusqlite::Row) -> rusqlite::Result<CatalogInstaller> {
32 let hash = row.get::<_, Option<String>>("hash")?.unwrap_or_default();
34
35 let raw = RawCatalogInstaller {
36 package_id: row.get::<_, String>("package_id")?,
37 url: row.get("url")?,
38 hash,
39 hash_algorithm: parse_text::<HashAlgorithm>(row.get::<_, String>("hash_algorithm")?)?,
40 installer_type: parse_text::<CatalogInstallerType>(
41 row.get::<_, String>("installer_type")?,
42 )?,
43 installer_switches: row.get("installer_switches")?,
44 platform: row.get("platform")?,
45 commands: row.get("commands")?,
46 protocols: row.get("protocols")?,
47 file_extensions: row.get("file_extensions")?,
48 capabilities: row.get("capabilities")?,
49 scope: row.get("scope")?,
50 arch: row.get("arch")?,
51 kind: row.get("kind")?,
52 nested_kind: row.get("nested_kind")?,
53 };
54
55 CatalogInstaller::try_from(raw).map_err(conversion_err)
56}
57
58fn parse_text<T>(value: String) -> rusqlite::Result<T>
59where
60 T: std::str::FromStr,
61 T::Err: std::error::Error + Send + Sync + 'static,
62{
63 value.parse::<T>().map_err(conversion_err)
64}
65
66#[cfg(test)]
67mod tests {
68 use super::get_installers;
69 use crate::models::catalog::CatalogInstallerType;
70 use crate::models::install::installer::InstallerType;
71 use crate::models::shared::HashAlgorithm;
72 use rusqlite::{Connection, params};
73
74 const CATALOG_SCHEMA: &str = include_str!("../../../../infra/parser/schema/catalog.sql");
75
76 fn insert_catalog_package(conn: &Connection) {
77 conn.execute(
78 "INSERT INTO catalog_packages (id, name, version, source, source_id) VALUES (?1, ?2, ?3, ?4, ?5)",
79 params![
80 "winget/Contoso.App",
81 "Contoso App",
82 "1.2.3",
83 "winget",
84 "Contoso.App",
85 ],
86 )
87 .expect("seed catalog package");
88 }
89
90 fn open_test_db() -> Connection {
91 let conn = Connection::open_in_memory().expect("open in-memory database");
92 conn.execute_batch(CATALOG_SCHEMA)
93 .expect("load catalog schema");
94 insert_catalog_package(&conn);
95 conn
96 }
97
98 #[test]
99 fn get_installers_reads_nested_kind_when_present() {
100 let conn = open_test_db();
101
102 conn.execute(
103 r#"
104 INSERT INTO catalog_installers (package_id, url, hash, hash_algorithm, installer_type, installer_switches, arch, kind, nested_kind)
105 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
106 "#,
107 params![
108 "winget/Contoso.App",
109 "https://example.test/app-one.zip",
110 "sha256:deadbeef",
111 "sha256",
112 "zip",
113 None::<String>,
114 "x64",
115 "zip",
116 "portable",
117 ],
118 )
119 .expect("insert portable installer");
120
121 conn.execute(
122 r#"
123 INSERT INTO catalog_installers (package_id, url, hash, hash_algorithm, installer_type, installer_switches, arch, kind, nested_kind)
124 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
125 "#,
126 params![
127 "winget/Contoso.App",
128 "https://example.test/app-two.zip",
129 "sha256:deadbeef",
130 "sha256",
131 "zip",
132 None::<String>,
133 "x64",
134 "zip",
135 "msi",
136 ],
137 )
138 .expect("insert msi installer");
139
140 let installers = get_installers(&conn, "winget/Contoso.App")
141 .expect("catalog installers should load with nested kind");
142
143 assert_eq!(installers.len(), 2);
144 assert_eq!(installers[0].nested_kind, Some(InstallerType::Portable));
145 assert_eq!(installers[1].nested_kind, Some(InstallerType::Msi));
146 assert_eq!(installers[0].hash_algorithm, HashAlgorithm::Sha256);
147 assert_eq!(installers[1].hash_algorithm, HashAlgorithm::Sha256);
148 assert_eq!(installers[0].installer_type, CatalogInstallerType::Zip);
149 assert_eq!(installers[1].installer_type, CatalogInstallerType::Zip);
150 }
151
152 #[test]
153 fn get_installers_reads_null_hash_as_empty_string() {
154 let conn = open_test_db();
155
156 conn.execute(
157 r#"
158 INSERT INTO catalog_installers (package_id, url, hash, hash_algorithm, installer_type, installer_switches, arch, kind, nested_kind)
159 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
160 "#,
161 params![
162 "winget/Contoso.App",
163 "https://example.test/app.exe",
164 None::<String>,
165 "sha256",
166 "exe",
167 None::<String>,
168 "x64",
169 "exe",
170 None::<String>,
171 ],
172 )
173 .expect("insert checksumless installer");
174
175 let installers = get_installers(&conn, "winget/Contoso.App")
176 .expect("catalog installers should load with null hash");
177
178 assert_eq!(installers.len(), 1);
179 assert!(installers[0].hash.is_empty());
180 assert_eq!(installers[0].kind, InstallerType::Exe);
181 }
182}