1use std::path::{Path, PathBuf};
17
18use qframe::diagnostics::{Diagnostic, Location};
19use qframe::document::{Document, Shape, ValueKind};
20use qframe::storage::data_dir;
21
22use crate::store::Loaded;
23
24use super::{Key, Lineup, Measured, Model, Price, ProviderEntry, ProviderKind, Tag, Wire, record::trim_base};
25
26const APP: &str = "quvyta/code";
28
29const FILE_NAME: &str = "providers.toml";
31
32#[cfg(unix)]
34const FOLDER_MODE: u32 = 0o700;
35
36#[cfg(unix)]
38const FILE_MODE: u32 = 0o600;
39
40const PROVIDER: &str = "provider";
42const MODEL: &str = "model";
45const LINEUP: &str = "lineup";
48const TAG: &str = "tag";
50const KIND: &str = "kind";
52const BASE: &str = "base";
54const WIRE: &str = "wire";
56const KEY: &str = "key";
58const ID: &str = "id";
60const CLAIMED: &str = "claimed-context";
62const MEASURED: &str = "measured-context";
64const MEASURED_KIND: &str = "measured";
66const PRICE: &str = "price";
68const NAME: &str = "name";
70const STEP: &str = "model";
72
73#[derive(Debug, Clone, PartialEq, Default)]
75pub struct Providers {
76 path: Option<PathBuf>,
77 entries: Vec<ProviderEntry>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum AddError {
83 Taken(String),
86}
87
88impl Providers {
89 #[must_use]
92 pub fn in_memory() -> Self {
93 Self::default()
94 }
95
96 #[must_use]
99 pub fn file() -> Option<PathBuf> {
100 data_dir(APP).map(|dir| dir.join(FILE_NAME))
101 }
102
103 #[must_use]
106 pub fn load() -> Loaded<Self> {
107 match Self::file() {
108 Some(path) => Self::open(&path),
109 None => Loaded { value: Self::in_memory(), diagnostics: Vec::new() },
110 }
111 }
112
113 #[must_use]
116 pub fn open(path: &Path) -> Loaded<Self> {
117 let text = match std::fs::read_to_string(path) {
118 Ok(text) => text,
119 Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
120 Err(error) => {
121 let diagnostics = vec![Diagnostic::error(None, format!("{}: {error}", path.display()))];
122 return Loaded { value: Self { path: Some(path.to_path_buf()), entries: Vec::new() }, diagnostics };
123 }
124 };
125 let name = path.file_name().map_or_else(|| FILE_NAME.to_owned(), |name| name.to_string_lossy().into_owned());
126 let mut loaded = Self::parse(&name, &text);
127 loaded.value.path = Some(path.to_path_buf());
128 loaded
129 }
130
131 #[must_use]
133 pub fn parse(file: &str, text: &str) -> Loaded<Self> {
134 let document = Document::parse(file, text, &shape());
135 let mut diagnostics = document.diagnostics().to_vec();
136 let root = document.root();
137 let mut entries: Vec<ProviderEntry> = Vec::new();
138
139 for table in root.entries(PROVIDER) {
140 let at = |key: &str| table.value_location(key).cloned();
141 let Some(written) = table.text(TAG) else { continue };
142 let tag = match Tag::parse(written) {
143 Ok(tag) => tag,
144 Err(problem) => {
145 let message = format!("`{TAG}` is `{written}`, which is not a tag: {problem:?}");
146 diagnostics.push(Diagnostic::error(at(TAG), message));
147 continue;
148 }
149 };
150 if entries.iter().any(|entry| entry.tag == tag) {
151 let message = format!("`{TAG}` is `{tag}`, and a provider of that tag was already read");
152 diagnostics.push(Diagnostic::error(at(TAG), message));
153 continue;
154 }
155 let (Some(kind), Some(base)) = (table.text(KIND).and_then(ProviderKind::parse), table.text(BASE)) else {
158 continue;
159 };
160 let wire = match table.text(WIRE) {
161 Some(written) => Wire::parse(written).unwrap_or_else(|| kind.wire()),
162 None => kind.wire(),
163 };
164 let key = table.text(KEY).and_then(Key::new);
165 if table.text(KEY).is_some() && key.is_none() {
166 let message = format!("`{KEY}` of `{tag}` is not a key that fits a request header; it is not used");
167 diagnostics.push(Diagnostic::warning(at(KEY), message));
168 }
169 entries.push(ProviderEntry {
170 tag,
171 kind,
172 base: trim_base(base),
173 wire,
174 models: Vec::new(),
175 key,
176 lineups: Vec::new(),
177 });
178 }
179
180 for table in root.entries(MODEL) {
181 let at = |key: &str| table.value_location(key).cloned();
182 let (Some(tag), Some(id)) = (table.text(TAG), table.text(ID)) else { continue };
183 let Some(entry) = entries.iter_mut().find(|entry| entry.tag.as_str() == tag) else {
184 let message = format!("`{TAG}` is `{tag}`, and no provider of that tag was read; the model is dropped");
185 diagnostics.push(Diagnostic::warning(at(TAG), message));
186 continue;
187 };
188 let claimed = positive(table.integer(CLAIMED), CLAIMED, at(CLAIMED), &mut diagnostics);
189 let tokens = positive(table.integer(MEASURED), MEASURED, at(MEASURED), &mut diagnostics);
190 let measured = match (tokens, table.text(MEASURED_KIND)) {
193 (Some(tokens), Some(written)) => match Measured::parse(written, tokens) {
194 Some(measured) => Some(measured),
195 None => {
196 let message = format!("`{MEASURED_KIND}` is `{written}`; the measurement is dropped");
197 diagnostics.push(Diagnostic::warning(at(MEASURED_KIND), message));
198 None
199 }
200 },
201 (Some(_), None) => {
202 let message =
203 format!("`{MEASURED}` is there without `{MEASURED_KIND}`; the measurement is dropped");
204 diagnostics.push(Diagnostic::warning(at(MEASURED), message));
205 None
206 }
207 (None, _) => None,
208 };
209 let price = table.text(PRICE).and_then(Price::parse);
212 entry.models.push(Model { id: id.to_owned(), claimed, measured, price });
213 }
214
215 for table in root.entries(LINEUP) {
219 let at = |key: &str| table.value_location(key).cloned();
220 let (Some(tag), Some(written), Some(step)) = (table.text(TAG), table.text(NAME), table.text(STEP)) else {
221 continue;
222 };
223 let Some(entry) = entries.iter_mut().find(|entry| entry.tag.as_str() == tag) else {
224 let message = format!("`{TAG}` is `{tag}`, and no provider of that tag was read; the step is dropped");
225 diagnostics.push(Diagnostic::warning(at(TAG), message));
226 continue;
227 };
228 let name = match Tag::parse(written) {
229 Ok(name) => name,
230 Err(problem) => {
231 let message =
232 format!("`{NAME}` is `{written}`, which is not a tag: {problem:?}; the step is dropped");
233 diagnostics.push(Diagnostic::warning(at(NAME), message));
234 continue;
235 }
236 };
237 let step = step.to_owned();
238 match entry.lineups.iter_mut().find(|lineup| lineup.name == name) {
241 Some(lineup) if lineup.models.contains(&step) => {
242 let message = format!("`{STEP}` is `{step}`, and it is already in `{name}`; the step is dropped");
243 diagnostics.push(Diagnostic::warning(at(STEP), message));
244 }
245 Some(lineup) => lineup.models.push(step),
246 None => entry.lineups.push(Lineup { name, models: vec![step] }),
247 }
248 }
249
250 Loaded { value: Self { path: None, entries }, diagnostics }
251 }
252
253 #[must_use]
255 pub fn entries(&self) -> &[ProviderEntry] {
256 &self.entries
257 }
258
259 #[must_use]
261 pub fn get(&self, tag: &str) -> Option<&ProviderEntry> {
262 self.entries.iter().find(|entry| entry.tag.as_str() == tag)
263 }
264
265 pub fn add(&mut self, entry: ProviderEntry) -> Result<(), AddError> {
271 if self.entries.iter().any(|already| already.tag == entry.tag) {
272 return Err(AddError::Taken(entry.tag.as_str().to_owned()));
273 }
274 self.entries.push(entry);
275 Ok(())
276 }
277
278 pub fn replace(&mut self, tag: &str, entry: ProviderEntry) {
280 if let Some(slot) = self.entries.iter_mut().find(|already| already.tag.as_str() == tag) {
281 *slot = entry;
282 }
283 }
284
285 pub fn remove(&mut self, tag: &str) -> bool {
288 let before = self.entries.len();
289 self.entries.retain(|entry| entry.tag.as_str() != tag);
290 before != self.entries.len()
291 }
292
293 pub fn forget_key(&mut self, tag: &str) -> bool {
297 match self.entries.iter_mut().find(|entry| entry.tag.as_str() == tag) {
298 Some(entry) => entry.key.take().is_some(),
299 None => false,
300 }
301 }
302
303 pub fn set_lineup(&mut self, tag: &str, lineup: Lineup) {
307 let Some(entry) = self.entries.iter_mut().find(|entry| entry.tag.as_str() == tag) else { return };
308 match entry.lineups.iter_mut().find(|already| already.name == lineup.name) {
309 Some(slot) => *slot = lineup,
310 None => entry.lineups.push(lineup),
311 }
312 }
313
314 pub fn remove_lineup(&mut self, tag: &str, name: &str) -> bool {
317 match self.entries.iter_mut().find(|entry| entry.tag.as_str() == tag) {
318 Some(entry) => {
319 let before = entry.lineups.len();
320 entry.lineups.retain(|lineup| lineup.name.as_str() != name);
321 before != entry.lineups.len()
322 }
323 None => false,
324 }
325 }
326
327 #[must_use]
329 pub fn to_toml(&self) -> String {
330 let mut text = String::new();
331 for entry in &self.entries {
332 text.push_str(&format!("[[{PROVIDER}]]\n"));
333 text.push_str(&format!("{TAG} = {}\n", quote(entry.tag.as_str())));
334 text.push_str(&format!("{KIND} = {}\n", quote(entry.kind.id())));
335 text.push_str(&format!("{BASE} = {}\n", quote(&entry.base)));
336 text.push_str(&format!("{WIRE} = {}\n", quote(entry.wire.id())));
337 if let Some(key) = &entry.key {
338 text.push_str(&format!("{KEY} = {}\n", quote(key.expose())));
339 }
340 text.push('\n');
341 }
342 for entry in &self.entries {
343 for model in &entry.models {
344 text.push_str(&format!("[[{MODEL}]]\n"));
345 text.push_str(&format!("{TAG} = {}\n", quote(entry.tag.as_str())));
346 text.push_str(&format!("{ID} = {}\n", quote(&model.id)));
347 if let Some(claimed) = model.claimed {
348 text.push_str(&format!("{CLAIMED} = {claimed}\n"));
349 }
350 if let Some(measured) = model.measured {
351 text.push_str(&format!("{MEASURED} = {}\n", measured.tokens()));
352 text.push_str(&format!("{MEASURED_KIND} = {}\n", quote(measured.id())));
353 }
354 if let Some(price) = model.price {
355 text.push_str(&format!("{PRICE} = {}\n", quote(price.id())));
356 }
357 text.push('\n');
358 }
359 }
360 for entry in &self.entries {
363 for lineup in &entry.lineups {
364 for step in &lineup.models {
365 text.push_str(&format!("[[{LINEUP}]]\n"));
366 text.push_str(&format!("{TAG} = {}\n", quote(entry.tag.as_str())));
367 text.push_str(&format!("{NAME} = {}\n", quote(lineup.name.as_str())));
368 text.push_str(&format!("{STEP} = {}\n", quote(step)));
369 text.push('\n');
370 }
371 }
372 }
373 text
374 }
375
376 pub fn save(&self) -> Result<(), String> {
389 let Some(path) = &self.path else { return Ok(()) };
390 let folder = path.parent().ok_or_else(|| format!("{}: it has no folder", path.display()))?;
391 create_folder(folder).map_err(|error| format!("{}: {error}", folder.display()))?;
392 let temporary = path.with_extension("toml.writing");
393 write_narrow(&temporary, &self.to_toml()).map_err(|error| format!("{}: {error}", temporary.display()))?;
394 std::fs::rename(&temporary, path).map_err(|error| {
395 let _ = std::fs::remove_file(&temporary);
396 format!("{}: {error}", path.display())
397 })
398 }
399
400 #[must_use]
402 pub fn path(&self) -> Option<&Path> {
403 self.path.as_deref()
404 }
405
406 #[must_use]
408 pub fn at(mut self, path: impl Into<PathBuf>) -> Self {
409 self.path = Some(path.into());
410 self
411 }
412}
413
414fn positive(value: Option<i64>, key: &str, at: Option<Location>, diagnostics: &mut Vec<Diagnostic>) -> Option<u64> {
416 match value {
417 None => None,
418 Some(number) if number > 0 => u64::try_from(number).ok(),
419 Some(number) => {
420 diagnostics.push(Diagnostic::warning(at, format!("`{key}` is {number}; a window is a count of tokens")));
421 None
422 }
423 }
424}
425
426fn shape() -> Shape {
428 let provider = Shape::new()
429 .required(TAG, ValueKind::text())
430 .required(KIND, ValueKind::choice(ProviderKind::ALL.map(ProviderKind::id)))
431 .required(BASE, ValueKind::text())
432 .optional(WIRE, ValueKind::choice(Wire::ALL.map(Wire::id)))
433 .optional(KEY, ValueKind::text());
434 let model = Shape::new()
435 .required(TAG, ValueKind::text())
436 .required(ID, ValueKind::text())
437 .optional(CLAIMED, ValueKind::integer())
438 .optional(MEASURED, ValueKind::integer())
439 .optional(MEASURED_KIND, ValueKind::choice(["about", "at-least"]))
440 .optional(PRICE, ValueKind::choice(Price::ALL.map(Price::id)));
441 let lineup = Shape::new()
442 .required(TAG, ValueKind::text())
443 .required(NAME, ValueKind::text())
444 .required(STEP, ValueKind::text());
445 Shape::new().entries(PROVIDER, provider).entries(MODEL, model).entries(LINEUP, lineup)
446}
447
448fn quote(text: &str) -> String {
450 let mut quoted = String::with_capacity(text.len() + 2);
451 quoted.push('"');
452 for character in text.chars() {
453 match character {
454 '"' => quoted.push_str("\\\""),
455 '\\' => quoted.push_str("\\\\"),
456 '\n' => quoted.push_str("\\n"),
457 '\r' => quoted.push_str("\\r"),
458 '\t' => quoted.push_str("\\t"),
459 other if (other as u32) < 0x20 => quoted.push_str(&format!("\\u{:04X}", other as u32)),
460 other => quoted.push(other),
461 }
462 }
463 quoted.push('"');
464 quoted
465}
466
467#[derive(Debug, Clone, PartialEq, Eq)]
469pub struct PermissionProblem {
470 pub place: PathBuf,
472 pub mode: u32,
474 pub wanted: u32,
476}
477
478#[must_use]
486pub fn permission_problems(path: &Path) -> Vec<PermissionProblem> {
487 #[cfg(unix)]
488 {
489 use std::os::unix::fs::PermissionsExt as _;
490
491 if !path.exists() {
492 return Vec::new();
493 }
494 let mut problems = Vec::new();
495 let mut check = |place: &Path, wanted: u32| {
496 let Ok(metadata) = std::fs::metadata(place) else { return };
497 let mode = metadata.permissions().mode() & 0o777;
498 if mode & !wanted != 0 {
499 problems.push(PermissionProblem { place: place.to_path_buf(), mode, wanted });
500 }
501 };
502 if let Some(folder) = path.parent() {
503 check(folder, FOLDER_MODE);
504 }
505 check(path, FILE_MODE);
506 problems
507 }
508 #[cfg(not(unix))]
509 {
510 let _ = path;
511 Vec::new()
512 }
513}
514
515fn create_folder(folder: &Path) -> std::io::Result<()> {
517 #[cfg(unix)]
518 {
519 use std::os::unix::fs::DirBuilderExt as _;
520
521 if let Some(above) = folder.parent() {
522 std::fs::create_dir_all(above)?;
523 }
524 match std::fs::DirBuilder::new().mode(FOLDER_MODE).create(folder) {
525 Ok(()) => Ok(()),
526 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
531 use std::os::unix::fs::PermissionsExt as _;
532 let mode = std::fs::metadata(folder)?.permissions().mode() & 0o777;
533 if mode & !FOLDER_MODE != 0 {
534 let _ = std::fs::set_permissions(folder, std::fs::Permissions::from_mode(mode & FOLDER_MODE));
535 }
536 Ok(())
537 }
538 Err(error) => Err(error),
539 }
540 }
541 #[cfg(not(unix))]
542 std::fs::create_dir_all(folder)
543}
544
545fn write_narrow(path: &Path, text: &str) -> std::io::Result<()> {
547 use std::io::Write as _;
548
549 let mut options = std::fs::OpenOptions::new();
550 options.write(true).create(true).truncate(true);
551 #[cfg(unix)]
552 {
553 use std::os::unix::fs::OpenOptionsExt as _;
554 options.mode(FILE_MODE);
555 }
556 let mut file = options.open(path)?;
557 file.write_all(text.as_bytes())?;
558 file.sync_all()
559}