Skip to repository content1141 lines · 37.7 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:58:18.265Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
util.rs
1pub mod archive;
2pub mod command;
3pub mod disambiguate;
4pub mod fs;
5pub mod markdown;
6pub mod path_list;
7pub mod paths;
8pub mod process;
9pub mod redact;
10pub mod schemars;
11pub mod serde;
12pub mod shell;
13pub mod shell_builder;
14pub mod shell_env;
15pub mod size;
16#[cfg(any(test, feature = "test-support"))]
17pub mod test;
18pub mod time;
19
20use anyhow::Result;
21use itertools::Either;
22use regex::Regex;
23use std::path::PathBuf;
24use std::sync::LazyLock;
25use std::{
26 borrow::Cow,
27 cmp::{self, Ordering},
28 ops::{Range, RangeInclusive},
29};
30use unicase::UniCase;
31
32pub use gpui_util::*;
33pub use path::PathExt;
34pub use path::normalize_path;
35pub use path::rel_path;
36
37pub use take_until::*;
38#[cfg(any(test, feature = "test-support"))]
39pub use util_macros::{line_endings, path, uri};
40
41pub use self::shell::{
42 get_default_system_shell, get_default_system_shell_preferring_bash, get_system_shell,
43};
44
45#[inline]
46pub const fn is_utf8_char_boundary(u8: u8) -> bool {
47 // This is bit magic equivalent to: b < 128 || b >= 192
48 (u8 as i8) >= -0x40
49}
50
51pub fn truncate(s: &str, max_chars: usize) -> &str {
52 match s.char_indices().nth(max_chars) {
53 None => s,
54 Some((idx, _)) => &s[..idx],
55 }
56}
57
58/// Parses the contents of an `os-release` file (as found at `/etc/os-release`
59/// and described by the systemd spec) into a human-readable string such as
60/// `"ubuntu 24.04"`, combining the `ID` and `VERSION_ID` fields.
61///
62/// Returns `None` if no `ID` field is present. When `VERSION_ID` is absent
63/// (e.g. on rolling releases), only the `ID` is returned.
64pub fn parse_os_release(content: &str) -> Option<String> {
65 let mut id = None;
66 let mut version_id = None;
67 for line in content.lines() {
68 match line.split_once('=') {
69 Some(("ID", value)) => id = Some(value.trim_matches('"')),
70 Some(("VERSION_ID", value)) => version_id = Some(value.trim_matches('"')),
71 _ => {}
72 }
73 }
74 let id = id?;
75 Some(match version_id {
76 Some(version) => format!("{id} {version}"),
77 None => id.to_string(),
78 })
79}
80
81/// Removes characters from the end of the string if its length is greater than `max_chars` and
82/// appends "..." to the string. Returns string unchanged if its length is smaller than max_chars.
83pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {
84 debug_assert!(max_chars >= 5);
85
86 // If the string's byte length is <= max_chars, walking the string can be skipped since the
87 // number of chars is <= the number of bytes.
88 if s.len() <= max_chars {
89 return s.to_string();
90 }
91 let truncation_ix = s.char_indices().map(|(i, _)| i).nth(max_chars);
92 match truncation_ix {
93 Some(index) => s[..index].to_string() + "…",
94 _ => s.to_string(),
95 }
96}
97
98/// Removes characters from the front of the string if its length is greater than `max_chars` and
99/// prepends the string with "...". Returns string unchanged if its length is smaller than max_chars.
100pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String {
101 debug_assert!(max_chars >= 5);
102
103 // If the string's byte length is <= max_chars, walking the string can be skipped since the
104 // number of chars is <= the number of bytes.
105 if s.len() <= max_chars {
106 return s.to_string();
107 }
108 let suffix_char_length = max_chars.saturating_sub(1);
109 let truncation_ix = s
110 .char_indices()
111 .map(|(i, _)| i)
112 .nth_back(suffix_char_length);
113 match truncation_ix {
114 Some(index) if index > 0 => "…".to_string() + &s[index..],
115 _ => s.to_string(),
116 }
117}
118
119/// Takes only `max_lines` from the string and, if there were more than `max_lines-1`, appends a
120/// a newline and "..." to the string, so that `max_lines` are returned.
121/// Returns string unchanged if its length is smaller than max_lines.
122pub fn truncate_lines_and_trailoff(s: &str, max_lines: usize) -> String {
123 let mut lines = s.lines().take(max_lines).collect::<Vec<_>>();
124 if lines.len() > max_lines - 1 {
125 lines.pop();
126 lines.join("\n") + "\n…"
127 } else {
128 lines.join("\n")
129 }
130}
131
132/// Truncates the string at a character boundary, such that the result is less than `max_bytes` in
133/// length.
134pub fn truncate_to_byte_limit(s: &str, max_bytes: usize) -> &str {
135 if s.len() < max_bytes {
136 return s;
137 }
138
139 for i in (0..max_bytes).rev() {
140 if s.is_char_boundary(i) {
141 return &s[..i];
142 }
143 }
144
145 ""
146}
147
148/// Takes a prefix of complete lines which fit within the byte limit. If the first line is longer
149/// than the limit, truncates at a character boundary.
150pub fn truncate_lines_to_byte_limit(s: &str, max_bytes: usize) -> &str {
151 if s.len() < max_bytes {
152 return s;
153 }
154
155 for i in (0..max_bytes).rev() {
156 if s.is_char_boundary(i) && s.as_bytes()[i] == b'\n' {
157 // Since the i-th character is \n, valid to slice at i + 1.
158 return &s[..i + 1];
159 }
160 }
161
162 truncate_to_byte_limit(s, max_bytes)
163}
164
165#[test]
166fn test_truncate_lines_to_byte_limit() {
167 let text = "Line 1\nLine 2\nLine 3\nLine 4";
168
169 // Limit that includes all lines
170 assert_eq!(truncate_lines_to_byte_limit(text, 100), text);
171
172 // Exactly the first line
173 assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n");
174
175 // Limit between lines
176 assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n");
177 assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n");
178
179 // Limit before first newline
180 assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line ");
181
182 // Test with non-ASCII characters
183 let text_utf8 = "Line 1\nLíne 2\nLine 3";
184 assert_eq!(
185 truncate_lines_to_byte_limit(text_utf8, 15),
186 "Line 1\nLíne 2\n"
187 );
188}
189
190/// Extend a sorted vector with a sorted sequence of items, maintaining the vector's sort order and
191/// enforcing a maximum length. This also de-duplicates items. Sort the items according to the given callback. Before calling this,
192/// both `vec` and `new_items` should already be sorted according to the `cmp` comparator.
193pub fn extend_sorted<T, I, F>(vec: &mut Vec<T>, new_items: I, limit: usize, mut cmp: F)
194where
195 I: IntoIterator<Item = T>,
196 F: FnMut(&T, &T) -> Ordering,
197{
198 let mut start_index = 0;
199 for new_item in new_items {
200 if let Err(i) = vec[start_index..].binary_search_by(|m| cmp(m, &new_item)) {
201 let index = start_index + i;
202 if vec.len() < limit {
203 vec.insert(index, new_item);
204 } else if index < vec.len() {
205 vec.pop();
206 vec.insert(index, new_item);
207 }
208 start_index = index;
209 }
210 }
211}
212
213pub fn truncate_to_bottom_n_sorted_by<T, F>(items: &mut Vec<T>, limit: usize, compare: &F)
214where
215 F: Fn(&T, &T) -> Ordering,
216{
217 if limit == 0 {
218 items.truncate(0);
219 }
220 if items.len() <= limit {
221 items.sort_by(compare);
222 return;
223 }
224 // When limit is near to items.len() it may be more efficient to sort the whole list and
225 // truncate, rather than always doing selection first as is done below. It's hard to analyze
226 // where the threshold for this should be since the quickselect style algorithm used by
227 // `select_nth_unstable_by` makes the prefix partially sorted, and so its work is not wasted -
228 // the expected number of comparisons needed by `sort_by` is less than it is for some arbitrary
229 // unsorted input.
230 items.select_nth_unstable_by(limit, compare);
231 items.truncate(limit);
232 items.sort_by(compare);
233}
234
235/// Prevents execution of the application with root privileges on Unix systems.
236///
237/// This function checks if the current process is running with root privileges
238/// and terminates the program with an error message unless explicitly allowed via the
239/// `ZED_ALLOW_ROOT` environment variable.
240#[cfg(unix)]
241pub fn prevent_root_execution() {
242 let is_root = nix::unistd::geteuid().is_root();
243 let allow_root = std::env::var("ZED_ALLOW_ROOT").is_ok_and(|val| val == "true");
244
245 if is_root && !allow_root {
246 eprintln!(
247 "\
248Error: Running Omega as root or via sudo is unsupported.
249 Doing so (even once) may subtly break things for all subsequent non-root usage of Omega.
250 It is untested and not recommended, don't complain when things break.
251 If you wish to proceed anyways, set `ZED_ALLOW_ROOT=true` in your environment."
252 );
253 std::process::exit(1);
254 }
255}
256
257#[cfg(unix)]
258fn load_shell_from_passwd() -> Result<()> {
259 let buflen = match unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) } {
260 n if n < 0 => 1024,
261 n => n as usize,
262 };
263 let mut buffer = Vec::with_capacity(buflen);
264
265 let mut pwd: std::mem::MaybeUninit<libc::passwd> = std::mem::MaybeUninit::uninit();
266 let mut result: *mut libc::passwd = std::ptr::null_mut();
267
268 let uid = unsafe { libc::getuid() };
269 let status = unsafe {
270 libc::getpwuid_r(
271 uid,
272 pwd.as_mut_ptr(),
273 buffer.as_mut_ptr() as *mut libc::c_char,
274 buflen,
275 &mut result,
276 )
277 };
278 anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid);
279
280 // SAFETY: If `getpwuid_r` doesn't error, we have the entry here.
281 let entry = unsafe { pwd.assume_init() };
282
283 anyhow::ensure!(
284 status == 0,
285 "call to getpwuid_r failed. uid: {}, status: {}",
286 uid,
287 status
288 );
289 anyhow::ensure!(
290 entry.pw_uid == uid,
291 "passwd entry has different uid ({}) than getuid ({}) returned",
292 entry.pw_uid,
293 uid,
294 );
295
296 let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() };
297 let should_set_shell = std::env::var("SHELL").map_or(true, |shell_env| {
298 shell_env != shell && !std::path::Path::new(&shell_env).exists()
299 });
300
301 if should_set_shell {
302 log::info!(
303 "updating SHELL environment variable to value from passwd entry: {:?}",
304 shell,
305 );
306 unsafe { std::env::set_var("SHELL", shell) };
307 }
308
309 Ok(())
310}
311
312/// Returns a shell escaped path for the current zed executable
313pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result<String> {
314 use anyhow::Context as _;
315 use paths::PathExt;
316 let mut zed_path =
317 std::env::current_exe().context("Failed to determine current Omega executable path.")?;
318 if cfg!(target_os = "linux")
319 && !zed_path.is_file()
320 && let Some(truncated) = zed_path
321 .clone()
322 .file_name()
323 .and_then(|s| s.to_str())
324 .and_then(|n| n.strip_suffix(" (deleted)"))
325 {
326 // Might have been deleted during update; let's use the new binary if there is one.
327 zed_path.set_file_name(truncated);
328 }
329
330 zed_path
331 .try_shell_safe(shell_kind)
332 .context("Failed to shell-escape Omega executable path.")
333}
334
335/// Returns a path for the zed cli executable, this function
336/// should be called from the zed executable, not zed-cli.
337pub fn get_zed_cli_path() -> Result<PathBuf> {
338 use anyhow::Context as _;
339 let zed_path =
340 std::env::current_exe().context("Failed to determine current Omega executable path.")?;
341 let parent = zed_path
342 .parent()
343 .context("Failed to determine parent directory of the Omega executable path.")?;
344
345 let possible_locations: &[&str] = if cfg!(target_os = "macos") {
346 // On macOS, the zed executable and zed-cli are inside the app bundle,
347 // so here ./cli is for both installed and development builds.
348 &["./cli"]
349 } else if cfg!(target_os = "windows") {
350 // bin/zed.exe is for installed builds, ./cli.exe is for development builds.
351 &["bin/zed.exe", "./cli.exe"]
352 } else if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") {
353 // bin is the standard, ./cli is for the target directory in development builds.
354 &["../bin/zed", "./cli"]
355 } else {
356 anyhow::bail!("unsupported platform for determining the Omega CLI path");
357 };
358
359 possible_locations
360 .iter()
361 .find_map(|p| {
362 parent
363 .join(p)
364 .canonicalize()
365 .ok()
366 .filter(|p| p != &zed_path)
367 })
368 .with_context(|| {
369 format!(
370 "could not find the Omega CLI from any of: {}",
371 possible_locations.join(", ")
372 )
373 })
374}
375
376#[cfg(unix)]
377pub async fn load_login_shell_environment() -> Result<()> {
378 use anyhow::Context as _;
379
380 load_shell_from_passwd().log_err();
381
382 // If possible, we want to `cd` in the user's `$HOME` to trigger programs
383 // such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook
384 // into shell's `cd` command (and hooks) to manipulate env.
385 // We do this so that we get the env a user would have when spawning a shell
386 // in home directory.
387 for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir())
388 .await
389 .with_context(|| format!("capturing environment with {:?}", get_system_shell()))?
390 {
391 // Skip SHLVL to prevent it from polluting Zed's process environment.
392 // The login shell used for env capture increments SHLVL, and if we propagate it,
393 // terminals spawned by Zed will inherit it and increment again, causing SHLVL
394 // to start at 2 instead of 1 (and increase by 2 on each reload).
395 if name == "SHLVL" {
396 continue;
397 }
398 unsafe { std::env::set_var(&name, &value) };
399 }
400
401 log::info!(
402 "set environment variables from shell:{}, path:{}",
403 std::env::var("SHELL").unwrap_or_default(),
404 std::env::var("PATH").unwrap_or_default(),
405 );
406
407 Ok(())
408}
409
410/// Configures the process to start a new session, to prevent interactive shells from taking control
411/// of the terminal.
412///
413/// For more details: <https://registerspill.thorstenball.com/p/how-to-lose-control-of-your-shell>
414pub fn set_pre_exec_to_start_new_session(
415 command: &mut std::process::Command,
416) -> &mut std::process::Command {
417 // safety: code in pre_exec should be signal safe.
418 // https://man7.org/linux/man-pages/man7/signal-safety.7.html
419 #[cfg(unix)]
420 unsafe {
421 use std::os::unix::process::CommandExt;
422 command.pre_exec(|| {
423 libc::setsid();
424 Ok(())
425 });
426 };
427 command
428}
429
430pub fn merge_json_lenient_value_into(
431 source: serde_json_lenient::Value,
432 target: &mut serde_json_lenient::Value,
433) {
434 match (source, target) {
435 (serde_json_lenient::Value::Object(source), serde_json_lenient::Value::Object(target)) => {
436 for (key, value) in source {
437 if let Some(target) = target.get_mut(&key) {
438 merge_json_lenient_value_into(value, target);
439 } else {
440 target.insert(key, value);
441 }
442 }
443 }
444
445 (serde_json_lenient::Value::Array(source), serde_json_lenient::Value::Array(target)) => {
446 for value in source {
447 target.push(value);
448 }
449 }
450
451 (source, target) => *target = source,
452 }
453}
454
455/// Merges `source` into `target`: objects are merged recursively, key by key;
456/// any other colliding value in `target` — including arrays — is replaced by
457/// `source`'s value wholesale.
458pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
459 use serde_json::Value;
460
461 match (source, target) {
462 (Value::Object(source), Value::Object(target)) => {
463 for (key, value) in source {
464 if let Some(target) = target.get_mut(&key) {
465 merge_json_value_into(value, target);
466 } else {
467 target.insert(key, value);
468 }
469 }
470 }
471 (source, target) => *target = source,
472 }
473}
474
475pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
476 use serde_json::Value;
477 if let Value::Object(source_object) = source {
478 let target_object = if let Value::Object(target) = target {
479 target
480 } else {
481 *target = Value::Object(Default::default());
482 target.as_object_mut().unwrap()
483 };
484 for (key, value) in source_object {
485 if let Some(target) = target_object.get_mut(&key) {
486 merge_non_null_json_value_into(value, target);
487 } else if !value.is_null() {
488 target_object.insert(key, value);
489 }
490 }
491 } else if !source.is_null() {
492 *target = source
493 }
494}
495
496pub fn expanded_and_wrapped_usize_range(
497 range: Range<usize>,
498 additional_before: usize,
499 additional_after: usize,
500 wrap_length: usize,
501) -> impl Iterator<Item = usize> {
502 let start_wraps = range.start < additional_before;
503 let end_wraps = wrap_length < range.end + additional_after;
504 if start_wraps && end_wraps {
505 Either::Left(0..wrap_length)
506 } else if start_wraps {
507 let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before);
508 if wrapped_start <= range.end {
509 Either::Left(0..wrap_length)
510 } else {
511 Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length))
512 }
513 } else if end_wraps {
514 let wrapped_end = range.end + additional_after - wrap_length;
515 if range.start <= wrapped_end {
516 Either::Left(0..wrap_length)
517 } else {
518 Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length))
519 }
520 } else {
521 Either::Left((range.start - additional_before)..(range.end + additional_after))
522 }
523}
524
525/// Yields `[i, i + 1, i - 1, i + 2, ..]`, each modulo `wrap_length` and bounded by
526/// `additional_before` and `additional_after`. If the wrapping causes overlap, duplicates are not
527/// emitted. If wrap_length is 0, nothing is yielded.
528pub fn wrapped_usize_outward_from(
529 start: usize,
530 additional_before: usize,
531 additional_after: usize,
532 wrap_length: usize,
533) -> impl Iterator<Item = usize> {
534 let mut count = 0;
535 let mut after_offset = 1;
536 let mut before_offset = 1;
537
538 std::iter::from_fn(move || {
539 count += 1;
540 if count > wrap_length {
541 None
542 } else if count == 1 {
543 Some(start % wrap_length)
544 } else if after_offset <= additional_after && after_offset <= before_offset {
545 let value = (start + after_offset) % wrap_length;
546 after_offset += 1;
547 Some(value)
548 } else if before_offset <= additional_before {
549 let value = (start + wrap_length - before_offset) % wrap_length;
550 before_offset += 1;
551 Some(value)
552 } else if after_offset <= additional_after {
553 let value = (start + after_offset) % wrap_length;
554 after_offset += 1;
555 Some(value)
556 } else {
557 None
558 }
559 })
560}
561
562#[cfg(any(test, feature = "test-support"))]
563mod rng {
564 use rand::prelude::*;
565
566 pub struct RandomCharIter<T: Rng> {
567 rng: T,
568 simple_text: bool,
569 }
570
571 impl<T: Rng> RandomCharIter<T> {
572 pub fn new(rng: T) -> Self {
573 Self {
574 rng,
575 simple_text: std::env::var("SIMPLE_TEXT").is_ok_and(|v| !v.is_empty()),
576 }
577 }
578
579 pub fn with_simple_text(mut self) -> Self {
580 self.simple_text = true;
581 self
582 }
583 }
584
585 impl<T: Rng> Iterator for RandomCharIter<T> {
586 type Item = char;
587
588 fn next(&mut self) -> Option<Self::Item> {
589 if self.simple_text {
590 return if self.rng.random_range(0..100) < 5 {
591 Some('\n')
592 } else {
593 Some(self.rng.random_range(b'a'..b'z' + 1).into())
594 };
595 }
596
597 match self.rng.random_range(0..100) {
598 // whitespace
599 0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.rng).copied(),
600 // two-byte greek letters
601 20..=32 => char::from_u32(self.rng.random_range(('α' as u32)..('ω' as u32 + 1))),
602 // // three-byte characters
603 33..=45 => ['✋', '✅', '❌', '❎', '⭐']
604 .choose(&mut self.rng)
605 .copied(),
606 // // four-byte characters
607 46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.rng).copied(),
608 // ascii letters
609 _ => Some(self.rng.random_range(b'a'..b'z' + 1).into()),
610 }
611 }
612 }
613}
614#[cfg(any(test, feature = "test-support"))]
615pub use rng::RandomCharIter;
616
617/// Get an embedded file as a string.
618pub fn asset_str<A: rust_embed::RustEmbed>(path: &str) -> Cow<'static, str> {
619 match A::get(path).expect(path).data {
620 Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()),
621 Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()),
622 }
623}
624
625pub trait RangeExt<T> {
626 fn sorted(&self) -> Self;
627 fn to_inclusive(&self) -> RangeInclusive<T>;
628 fn overlaps(&self, other: &Range<T>) -> bool;
629 fn contains_inclusive(&self, other: &Range<T>) -> bool;
630}
631
632impl<T: Ord + Clone> RangeExt<T> for Range<T> {
633 fn sorted(&self) -> Self {
634 cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone()
635 }
636
637 fn to_inclusive(&self) -> RangeInclusive<T> {
638 self.start.clone()..=self.end.clone()
639 }
640
641 fn overlaps(&self, other: &Range<T>) -> bool {
642 self.start < other.end && other.start < self.end
643 }
644
645 fn contains_inclusive(&self, other: &Range<T>) -> bool {
646 self.start <= other.start && other.end <= self.end
647 }
648}
649
650impl<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
651 fn sorted(&self) -> Self {
652 cmp::min(self.start(), self.end()).clone()..=cmp::max(self.start(), self.end()).clone()
653 }
654
655 fn to_inclusive(&self) -> RangeInclusive<T> {
656 self.clone()
657 }
658
659 fn overlaps(&self, other: &Range<T>) -> bool {
660 self.start() < &other.end && &other.start <= self.end()
661 }
662
663 fn contains_inclusive(&self, other: &Range<T>) -> bool {
664 self.start() <= &other.start && &other.end <= self.end()
665 }
666}
667
668/// A way to sort strings with starting numbers numerically first, falling back to alphanumeric one,
669/// case-insensitive.
670///
671/// This is useful for turning regular alphanumerically sorted sequences as `1-abc, 10, 11-def, .., 2, 21-abc`
672/// into `1-abc, 2, 10, 11-def, .., 21-abc`
673#[derive(Debug, PartialEq, Eq)]
674pub struct NumericPrefixWithSuffix<'a>(Option<u64>, &'a str);
675
676impl<'a> NumericPrefixWithSuffix<'a> {
677 pub fn from_numeric_prefixed_str(str: &'a str) -> Self {
678 let i = str.chars().take_while(|c| c.is_ascii_digit()).count();
679 let (prefix, remainder) = str.split_at(i);
680
681 let prefix = prefix.parse().ok();
682 Self(prefix, remainder)
683 }
684}
685
686/// When dealing with equality, we need to consider the case of the strings to achieve strict equality
687/// to handle cases like "a" < "A" instead of "a" == "A".
688impl Ord for NumericPrefixWithSuffix<'_> {
689 fn cmp(&self, other: &Self) -> Ordering {
690 match (self.0, other.0) {
691 (None, None) => UniCase::new(self.1)
692 .cmp(&UniCase::new(other.1))
693 .then_with(|| self.1.cmp(other.1).reverse()),
694 (None, Some(_)) => Ordering::Greater,
695 (Some(_), None) => Ordering::Less,
696 (Some(a), Some(b)) => a.cmp(&b).then_with(|| {
697 UniCase::new(self.1)
698 .cmp(&UniCase::new(other.1))
699 .then_with(|| self.1.cmp(other.1).reverse())
700 }),
701 }
702 }
703}
704
705impl PartialOrd for NumericPrefixWithSuffix<'_> {
706 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
707 Some(self.cmp(other))
708 }
709}
710
711fn emoji_regex() -> &'static Regex {
712 static EMOJI_REGEX: LazyLock<Regex> =
713 LazyLock::new(|| Regex::new("(\\p{Emoji}|\u{200D})").unwrap());
714 &EMOJI_REGEX
715}
716
717/// Returns true if the given string consists of emojis only.
718/// E.g. "👨👩👧👧👋" will return true, but "👋!" will return false.
719pub fn word_consists_of_emojis(s: &str) -> bool {
720 let mut prev_end = 0;
721 for capture in emoji_regex().find_iter(s) {
722 if capture.start() != prev_end {
723 return false;
724 }
725 prev_end = capture.end();
726 }
727 prev_end == s.len()
728}
729
730/// Similar to `str::split`, but also provides byte-offset ranges of the results. Unlike
731/// `str::split`, this is not generic on pattern types and does not return an `Iterator`.
732pub fn split_str_with_ranges<'s>(
733 s: &'s str,
734 pat: &dyn Fn(char) -> bool,
735) -> Vec<(Range<usize>, &'s str)> {
736 let mut result = Vec::new();
737 let mut start = 0;
738
739 for (i, ch) in s.char_indices() {
740 if pat(ch) {
741 if i > start {
742 result.push((start..i, &s[start..i]));
743 }
744 start = i + ch.len_utf8();
745 }
746 }
747
748 if s.len() > start {
749 result.push((start..s.len(), &s[start..s.len()]));
750 }
751
752 result
753}
754
755pub fn default<D: Default>() -> D {
756 Default::default()
757}
758
759#[derive(Debug)]
760pub enum ConnectionResult<O> {
761 Timeout,
762 ConnectionReset,
763 Result(anyhow::Result<O>),
764}
765
766impl<O> ConnectionResult<O> {
767 pub fn into_response(self) -> anyhow::Result<O> {
768 match self {
769 ConnectionResult::Timeout => anyhow::bail!("Request timed out"),
770 ConnectionResult::ConnectionReset => anyhow::bail!("Server reset the connection"),
771 ConnectionResult::Result(r) => r,
772 }
773 }
774}
775
776impl<O> From<anyhow::Result<O>> for ConnectionResult<O> {
777 fn from(result: anyhow::Result<O>) -> Self {
778 ConnectionResult::Result(result)
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785
786 #[test]
787 fn test_parse_os_release() {
788 let os_release =
789 "NAME=\"Ubuntu\"\nID=ubuntu\nVERSION_ID=\"24.04\"\nPRETTY_NAME=\"Ubuntu 24.04 LTS\"\n";
790 assert_eq!(
791 parse_os_release(os_release),
792 Some("ubuntu 24.04".to_string())
793 );
794
795 // VERSION_ID may be absent (e.g. rolling releases like Arch).
796 assert_eq!(parse_os_release("ID=arch\n"), Some("arch".to_string()));
797
798 // Without an ID there is nothing usable to report.
799 assert_eq!(parse_os_release("VERSION_ID=1\n"), None);
800 assert_eq!(parse_os_release(""), None);
801 }
802
803 #[test]
804 fn test_extend_sorted() {
805 let mut vec = vec![];
806
807 extend_sorted(&mut vec, vec![21, 17, 13, 8, 1, 0], 5, |a, b| b.cmp(a));
808 assert_eq!(vec, &[21, 17, 13, 8, 1]);
809
810 extend_sorted(&mut vec, vec![101, 19, 17, 8, 2], 8, |a, b| b.cmp(a));
811 assert_eq!(vec, &[101, 21, 19, 17, 13, 8, 2, 1]);
812
813 extend_sorted(&mut vec, vec![1000, 19, 17, 9, 5], 8, |a, b| b.cmp(a));
814 assert_eq!(vec, &[1000, 101, 21, 19, 17, 13, 9, 8]);
815 }
816
817 #[test]
818 fn test_truncate_to_bottom_n_sorted_by() {
819 let mut vec: Vec<u32> = vec![5, 2, 3, 4, 1];
820 truncate_to_bottom_n_sorted_by(&mut vec, 10, &u32::cmp);
821 assert_eq!(vec, &[1, 2, 3, 4, 5]);
822
823 vec = vec![5, 2, 3, 4, 1];
824 truncate_to_bottom_n_sorted_by(&mut vec, 5, &u32::cmp);
825 assert_eq!(vec, &[1, 2, 3, 4, 5]);
826
827 vec = vec![5, 2, 3, 4, 1];
828 truncate_to_bottom_n_sorted_by(&mut vec, 4, &u32::cmp);
829 assert_eq!(vec, &[1, 2, 3, 4]);
830
831 vec = vec![5, 2, 3, 4, 1];
832 truncate_to_bottom_n_sorted_by(&mut vec, 1, &u32::cmp);
833 assert_eq!(vec, &[1]);
834
835 vec = vec![5, 2, 3, 4, 1];
836 truncate_to_bottom_n_sorted_by(&mut vec, 0, &u32::cmp);
837 assert!(vec.is_empty());
838 }
839
840 #[test]
841 fn test_iife() {
842 fn option_returning_function() -> Option<()> {
843 None
844 }
845
846 let foo = maybe!({
847 option_returning_function()?;
848 Some(())
849 });
850
851 assert_eq!(foo, None);
852 }
853
854 #[test]
855 fn test_truncate_and_trailoff() {
856 assert_eq!(truncate_and_trailoff("", 5), "");
857 assert_eq!(truncate_and_trailoff("aaaaaa", 7), "aaaaaa");
858 assert_eq!(truncate_and_trailoff("aaaaaa", 6), "aaaaaa");
859 assert_eq!(truncate_and_trailoff("aaaaaa", 5), "aaaaa…");
860 assert_eq!(truncate_and_trailoff("èèèèèè", 7), "èèèèèè");
861 assert_eq!(truncate_and_trailoff("èèèèèè", 6), "èèèèèè");
862 assert_eq!(truncate_and_trailoff("èèèèèè", 5), "èèèèè…");
863 }
864
865 #[test]
866 fn test_truncate_and_remove_front() {
867 assert_eq!(truncate_and_remove_front("", 5), "");
868 assert_eq!(truncate_and_remove_front("aaaaaa", 7), "aaaaaa");
869 assert_eq!(truncate_and_remove_front("aaaaaa", 6), "aaaaaa");
870 assert_eq!(truncate_and_remove_front("aaaaaa", 5), "…aaaaa");
871 assert_eq!(truncate_and_remove_front("èèèèèè", 7), "èèèèèè");
872 assert_eq!(truncate_and_remove_front("èèèèèè", 6), "èèèèèè");
873 assert_eq!(truncate_and_remove_front("èèèèèè", 5), "…èèèèè");
874 }
875
876 #[test]
877 fn test_numeric_prefix_str_method() {
878 let target = "1a";
879 assert_eq!(
880 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
881 NumericPrefixWithSuffix(Some(1), "a")
882 );
883
884 let target = "12ab";
885 assert_eq!(
886 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
887 NumericPrefixWithSuffix(Some(12), "ab")
888 );
889
890 let target = "12_ab";
891 assert_eq!(
892 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
893 NumericPrefixWithSuffix(Some(12), "_ab")
894 );
895
896 let target = "1_2ab";
897 assert_eq!(
898 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
899 NumericPrefixWithSuffix(Some(1), "_2ab")
900 );
901
902 let target = "1.2";
903 assert_eq!(
904 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
905 NumericPrefixWithSuffix(Some(1), ".2")
906 );
907
908 let target = "1.2_a";
909 assert_eq!(
910 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
911 NumericPrefixWithSuffix(Some(1), ".2_a")
912 );
913
914 let target = "12.2_a";
915 assert_eq!(
916 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
917 NumericPrefixWithSuffix(Some(12), ".2_a")
918 );
919
920 let target = "12a.2_a";
921 assert_eq!(
922 NumericPrefixWithSuffix::from_numeric_prefixed_str(target),
923 NumericPrefixWithSuffix(Some(12), "a.2_a")
924 );
925 }
926
927 #[test]
928 fn test_numeric_prefix_with_suffix() {
929 let mut sorted = vec!["1-abc", "10", "11def", "2", "21-abc"];
930 sorted.sort_by_key(|s| NumericPrefixWithSuffix::from_numeric_prefixed_str(s));
931 assert_eq!(sorted, ["1-abc", "2", "10", "11def", "21-abc"]);
932
933 for numeric_prefix_less in ["numeric_prefix_less", "aaa", "~™£"] {
934 assert_eq!(
935 NumericPrefixWithSuffix::from_numeric_prefixed_str(numeric_prefix_less),
936 NumericPrefixWithSuffix(None, numeric_prefix_less),
937 "String without numeric prefix `{numeric_prefix_less}` should not be converted into NumericPrefixWithSuffix"
938 )
939 }
940 }
941
942 #[test]
943 fn test_word_consists_of_emojis() {
944 let words_to_test = vec![
945 ("👨👩👧👧👋🥒", true),
946 ("👋", true),
947 ("!👋", false),
948 ("👋!", false),
949 ("👋 ", false),
950 (" 👋", false),
951 ("Test", false),
952 ];
953
954 for (text, expected_result) in words_to_test {
955 assert_eq!(word_consists_of_emojis(text), expected_result);
956 }
957 }
958
959 #[test]
960 fn test_truncate_lines_and_trailoff() {
961 let text = r#"Line 1
962Line 2
963Line 3"#;
964
965 assert_eq!(
966 truncate_lines_and_trailoff(text, 2),
967 r#"Line 1
968…"#
969 );
970
971 assert_eq!(
972 truncate_lines_and_trailoff(text, 3),
973 r#"Line 1
974Line 2
975…"#
976 );
977
978 assert_eq!(
979 truncate_lines_and_trailoff(text, 4),
980 r#"Line 1
981Line 2
982Line 3"#
983 );
984 }
985
986 #[test]
987 fn test_expanded_and_wrapped_usize_range() {
988 // Neither wrap
989 assert_eq!(
990 expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::<Vec<usize>>(),
991 (1..5).collect::<Vec<usize>>()
992 );
993 // Start wraps
994 assert_eq!(
995 expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::<Vec<usize>>(),
996 ((0..5).chain(7..8)).collect::<Vec<usize>>()
997 );
998 // Start wraps all the way around
999 assert_eq!(
1000 expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::<Vec<usize>>(),
1001 (0..8).collect::<Vec<usize>>()
1002 );
1003 // Start wraps all the way around and past 0
1004 assert_eq!(
1005 expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::<Vec<usize>>(),
1006 (0..8).collect::<Vec<usize>>()
1007 );
1008 // End wraps
1009 assert_eq!(
1010 expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::<Vec<usize>>(),
1011 (0..1).chain(2..8).collect::<Vec<usize>>()
1012 );
1013 // End wraps all the way around
1014 assert_eq!(
1015 expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::<Vec<usize>>(),
1016 (0..8).collect::<Vec<usize>>()
1017 );
1018 // End wraps all the way around and past the end
1019 assert_eq!(
1020 expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::<Vec<usize>>(),
1021 (0..8).collect::<Vec<usize>>()
1022 );
1023 // Both start and end wrap
1024 assert_eq!(
1025 expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::<Vec<usize>>(),
1026 (0..8).collect::<Vec<usize>>()
1027 );
1028 }
1029
1030 #[test]
1031 fn test_wrapped_usize_outward_from() {
1032 // No wrapping
1033 assert_eq!(
1034 wrapped_usize_outward_from(4, 2, 2, 10).collect::<Vec<usize>>(),
1035 vec![4, 5, 3, 6, 2]
1036 );
1037 // Wrapping at end
1038 assert_eq!(
1039 wrapped_usize_outward_from(8, 2, 3, 10).collect::<Vec<usize>>(),
1040 vec![8, 9, 7, 0, 6, 1]
1041 );
1042 // Wrapping at start
1043 assert_eq!(
1044 wrapped_usize_outward_from(1, 3, 2, 10).collect::<Vec<usize>>(),
1045 vec![1, 2, 0, 3, 9, 8]
1046 );
1047 // All values wrap around
1048 assert_eq!(
1049 wrapped_usize_outward_from(5, 10, 10, 8).collect::<Vec<usize>>(),
1050 vec![5, 6, 4, 7, 3, 0, 2, 1]
1051 );
1052 // None before / after
1053 assert_eq!(
1054 wrapped_usize_outward_from(3, 0, 0, 8).collect::<Vec<usize>>(),
1055 vec![3]
1056 );
1057 // Starting point already wrapped
1058 assert_eq!(
1059 wrapped_usize_outward_from(15, 2, 2, 10).collect::<Vec<usize>>(),
1060 vec![5, 6, 4, 7, 3]
1061 );
1062 // wrap_length of 0
1063 assert_eq!(
1064 wrapped_usize_outward_from(4, 2, 2, 0).collect::<Vec<usize>>(),
1065 Vec::<usize>::new()
1066 );
1067 }
1068
1069 #[test]
1070 fn test_split_with_ranges() {
1071 let input = "hi";
1072 let result = split_str_with_ranges(input, &|c| c == ' ');
1073
1074 assert_eq!(result.len(), 1);
1075 assert_eq!(result[0], (0..2, "hi"));
1076
1077 let input = "héllo🦀world";
1078 let result = split_str_with_ranges(input, &|c| c == '🦀');
1079
1080 assert_eq!(result.len(), 2);
1081 assert_eq!(result[0], (0..6, "héllo")); // 'é' is 2 bytes
1082 assert_eq!(result[1], (10..15, "world")); // '🦀' is 4 bytes
1083 }
1084
1085 #[test]
1086 fn test_merge_json_values() {
1087 use serde_json::json;
1088
1089 let mut target = json!({
1090 "unchanged": 1,
1091 "replaced_scalar": "old",
1092 "replaced_array": ["default-1", "default-2"],
1093 "nulled": true,
1094 "array_becomes_object": [1, 2],
1095 "object_becomes_scalar": { "x": 1 },
1096 "nested": {
1097 "kept": true,
1098 "overridden": 2,
1099 "args": ["--default"],
1100 "deeper": { "list": [1, 2], "other": "kept" },
1101 },
1102 });
1103 let source = json!({
1104 "replaced_scalar": "new",
1105 "replaced_array": ["default-2", "user"],
1106 "nulled": null,
1107 "array_becomes_object": { "y": 2 },
1108 "object_becomes_scalar": 3,
1109 "inserted": ["brand-new"],
1110 "nested": {
1111 "overridden": 20,
1112 "args": ["--user"],
1113 "deeper": { "list": [3] },
1114 "inserted": { "z": true },
1115 },
1116 });
1117
1118 merge_json_value_into(source, &mut target);
1119
1120 assert_eq!(
1121 target,
1122 json!({
1123 "unchanged": 1,
1124 "replaced_scalar": "new",
1125 "replaced_array": ["default-2", "user"],
1126 "nulled": null,
1127 "array_becomes_object": { "y": 2 },
1128 "object_becomes_scalar": 3,
1129 "inserted": ["brand-new"],
1130 "nested": {
1131 "kept": true,
1132 "overridden": 20,
1133 "args": ["--user"],
1134 "deeper": { "list": [3], "other": "kept" },
1135 "inserted": { "z": true },
1136 },
1137 })
1138 );
1139 }
1140}
1141