1use crate::fail;
19use crate::prelude::HashMap;
20#[cfg(feature = "std")]
21use crate::prelude::Path;
22use crate::prelude::*;
23use aho_corasick::AhoCorasick;
24use alloc::collections::BTreeSet;
25use bon::Builder;
26use core::fmt;
27use core::iter::successors;
28use derive_more::Display;
29use fancy_regex::Regex;
30use fluent_uri::UriRef;
31use jiff::{tz::Offset, Timestamp};
32use jsonc_parser::{parse_to_serde_value, ParseOptions};
33#[cfg(feature = "std")]
34use nanoid::nanoid;
35use schemars::JsonSchema;
36use serde::de::DeserializeOwned;
37use serde::{Deserialize, Serialize};
38use serde_json::Value;
39use strum::EnumIs;
40use validator::Validate;
41pub mod assets;
42#[cfg(feature = "std")]
43pub mod cmd;
44pub mod constants;
45pub mod macros;
46pub mod portable;
47pub mod terminal;
48pub use assets::Constant;
49use constants::{CROCKFORD_BASE32_ALPHABET, LINE_SEPARATOR};
50pub use terminal::Label;
51#[cfg(feature = "std")]
52pub use terminal::{
53 print_changes, print_changes_with_color, print_values_as_table, text_diff_changes, text_diff_changes_with_color, values_as_table,
54};
55pub trait LinkedData {
57 fn with_context(&self) -> Self;
59}
60pub trait MarkdownSupport {
62 fn to_markdown(&self) -> String;
64 fn from_markdown(content: &str) -> Result<Self, String>
66 where
67 Self: Sized,
68 {
69 let _ = content;
70 Err("Markdown parsing is not implemented for this type".to_string())
71 }
72 fn from_front_matter(value: &str) -> Result<Self, String>
74 where
75 Self: DeserializeOwned + Sized,
76 {
77 serde_norway::from_str::<Value>(value)
78 .map_err(|why| why.to_string())
79 .and_then(|value| serde_json::from_value(value).map_err(|why| why.to_string()))
80 }
81 fn to_front_matter(&self) -> Result<String, String>
83 where
84 Self: Serialize,
85 {
86 serde_json::to_value(self)
87 .map_err(|why| why.to_string())
88 .and_then(|value| serde_norway::to_string(&value).map_err(|why| why.to_string()))
89 }
90 fn to_markdown_text(&self) -> String
92 where
93 Self: AsRef<str>,
94 {
95 self.as_ref().replace('&', "&").replace('\r', " ").replace('\n', " ")
96 }
97 fn decode_markdown_text(&self) -> String
99 where
100 Self: AsRef<str>,
101 {
102 self.as_ref().replace(" ", "\r").replace(" ", "\n").replace("&", "&")
103 }
104 fn normalize(&self) -> String
106 where
107 Self: AsRef<str>,
108 {
109 self.as_ref()
110 .chars()
111 .filter(|character| !matches!(character, '-' | '_' | '.' | ','))
112 .collect::<String>()
113 .replace('&', "and")
114 .trim()
115 .to_string()
116 }
117}
118pub trait Searchable<T> {
120 fn contains(&self, _value: &str) -> bool {
124 false
125 }
126 fn find_by_iso(&self, _value: impl Into<String>) -> Option<T> {
130 None
131 }
132 fn find_by_name(&self, value: impl Into<String>) -> Option<T>;
134}
135pub trait StringConversion {
137 fn normalized(&self) -> String;
139 fn to_cross_platform_path(&self) -> String;
141 fn file_name_with_parent(&self) -> String;
143 fn to_absolute_path(&self) -> String;
145}
146pub trait StringExt {
148 fn is_numeric(&self) -> bool;
150}
151pub trait StringInterpolation<T>
153where
154 T: AsRef<str> + ToString,
155{
156 fn replace_placeholder_with_string(&self, placeholder: &str, value: &str) -> String;
158 fn with_indent(&self, spaces: usize) -> String;
160 fn with_additional_indent(&self, spaces: usize) -> String;
162}
163pub trait ToProse {
165 fn to_prose(&self) -> String;
167}
168pub trait ToStrings {
170 fn to_strings(&self) -> Vec<String>;
183 fn to_absolute_strings(&self) -> Vec<String> {
185 vec![]
186 }
187}
188pub trait ToStringChunks<T>
190where
191 T: AsRef<str> + ToString,
192{
193 fn chunk(&self, size: usize) -> Vec<String>;
195}
196pub trait Unstructured {
198 fn content(&self) -> &str;
200}
201#[derive(Clone, Debug, Default, Display, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
205pub enum ChecksumAlgorithm {
206 #[default]
208 #[display("sha256")]
209 #[serde(rename = "SHA256", alias = "sha256")]
210 Sha256,
211 #[display("md2")]
213 #[serde(rename = "MD2", alias = "md2")]
214 Md2,
215 #[display("md4")]
217 #[serde(rename = "MD4", alias = "md4")]
218 Md4,
219 #[display("md5")]
221 #[serde(rename = "MD5", alias = "md5")]
222 Md5,
223 #[display("md6")]
225 #[serde(rename = "MD6", alias = "md6")]
226 Md6,
227 #[display("sha1")]
229 #[serde(rename = "SHA1", alias = "sha1")]
230 Sha1,
231 #[display("sha224")]
233 #[serde(rename = "SHA224", alias = "sha224")]
234 Sha224,
235 #[display("sha384")]
237 #[serde(rename = "SHA384", alias = "sha384")]
238 Sha384,
239 #[display("sha512")]
241 #[serde(rename = "SHA512", alias = "sha512")]
242 Sha512,
243}
244#[derive(Clone, Debug, Display, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
248pub enum License {
249 #[display("AGPL-3.0-only")]
251 #[serde(alias = "AGPL-3.0-only")]
252 Agpl3Only,
253 #[display("Apache-2.0")]
255 #[serde(alias = "Apache-2.0")]
256 Apache2,
257 #[display("BSD-3-Clause")]
259 #[serde(alias = "BSD-3-Clause")]
260 Bsd3Clause,
261 #[display("CC0-1.0")]
263 #[serde(alias = "CC0-1.0", alias = "Creative Commons CC-0")]
264 CreativeCommons,
265 #[display("GPL-2.0-only")]
267 #[serde(alias = "GPL-2.0-only")]
268 Gpl2Only,
269 #[display("GPL-2.0-with-classpath-exception")]
271 #[serde(alias = "GPL-2.0-with-classpath-exception")]
272 Gpl2WithClasspathException,
273 #[display("GPL-3.0-only")]
275 #[serde(alias = "GPL-3.0-only")]
276 Gpl3Only,
277 #[display("GPL-3.0-or-later")]
279 #[serde(alias = "GPL-3.0-or-later")]
280 Gpl3OrLater,
281 #[display("LGPL-2.1-only")]
283 #[serde(alias = "LGPL-2.1-only")]
284 Lgpl21Only,
285 #[display("LPPL-1.3c")]
287 #[serde(alias = "LPPL-1.3c")]
288 Lppl13c,
289 #[display("MIT")]
291 #[serde(alias = "MIT")]
292 Mit,
293 #[display("PostgreSQL")]
295 #[serde(alias = "PostgreSQL")]
296 PostgreSql,
297 #[display("Proprietary")]
299 #[serde(alias = "LicenseRef-Proprietary")]
300 Proprietary,
301 #[display("PSF-based")]
303 #[serde(alias = "PSF-based")]
304 PsfBased,
305 #[display("PSF-2.0")]
307 #[serde(alias = "PSF-2.0")]
308 Psf2,
309 #[display("Public Domain")]
311 #[serde(alias = "Public Domain")]
312 PublicDomain,
313 #[display("Unknown")]
315 Unknown,
316 #[display("Various")]
318 #[serde(alias = "Various")]
319 Various,
320 #[display("W3C")]
322 #[serde(alias = "W3C")]
323 W3C,
324}
325#[derive(Clone, Debug, Display, EnumIs, PartialEq)]
329pub enum MimeType {
330 #[display("application/yaml")]
336 Cff,
337 #[display("text/csv")]
339 Csv,
340 #[display("application/msword")]
342 Doc,
343 #[display("application/vnd.openxmlformats-officedocument.wordprocessingml.document")]
345 Docx,
346 #[display("application/epub+zip")]
348 Epub,
349 #[display("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")]
351 Excel,
352 #[display("application/vnd.gguf.model")]
356 Gguf,
357 #[display("application/gzip")]
359 Gzip,
360 #[display("application/ld+json")]
364 LdJson,
365 #[display("image/jpeg")]
367 Jpeg,
368 #[display("application/json")]
372 Json,
373 #[display("application/jsonc")]
377 Jsonc,
378 #[display("text/markdown")]
380 Markdown,
381 #[display("application/vnd.ai.modelcard.v1+json")]
383 ModelCard,
384 #[display("application/vnd.onnx.model")]
388 Onnx,
389 #[display("application/vnd.oasis.opendocument.presentation")]
391 Odp,
392 #[display("application/vnd.oasis.opendocument.spreadsheet")]
394 Ods,
395 #[display("application/vnd.oasis.opendocument.text")]
397 Odt,
398 #[display("font/otf")]
400 Otf,
401 #[display("application/x-parquet")]
405 Parquet,
406 #[display("application/pdf")]
408 Pdf,
409 #[display("image/png")]
411 Png,
412 #[display("application/vnd.ms-powerpoint")]
414 Ppt,
415 #[display("application/vnd.pytorch.model")]
419 Pytorch,
420 #[display("application/vnd.openxmlformats-officedocument.presentationml.presentation")]
424 Powerpoint,
425 #[display("application/vnd.ai.prompt.v1+json")]
429 Prompt,
430 #[display("application/rtf")]
432 Rtf,
433 #[display("text/rust")]
435 Rust,
436 #[display("application/vnd.safetensors")]
440 Safetensors,
441 #[display("application/spdx+json")]
445 Sbom,
446 #[display("application/x-7z-compressed")]
448 SevenZip,
449 #[display("image/svg+xml")]
451 Svg,
452 #[display("application/x-tar")]
454 Tar,
455 #[display("text/plain")]
459 Text,
460 #[display("application/toml")]
464 Toml,
465 #[display("font/ttf")]
467 Ttf,
468 #[display("application/yaml")]
472 Yaml,
473 #[display("application/zip")]
477 Zip,
478 #[display("application/vnd.{}", _0)]
480 Vendor(String),
481 #[display("application/octet-stream")]
483 Unknown(String),
484}
485#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
487pub struct Checksum {
488 pub algorithm: ChecksumAlgorithm,
490 #[serde(rename = "checksumValue")]
492 pub checksum_value: String,
493}
494#[derive(Builder, Clone, Copy, Debug, Deserialize, Display, Serialize, JsonSchema)]
507#[builder(start_fn = init)]
508#[display("{}.{}.{}", major, minor, patch)]
509pub struct SemanticVersion {
510 #[builder(default = 0)]
512 pub major: u32,
513 #[builder(default = 0)]
515 pub minor: u32,
516 #[builder(default = 0)]
518 pub patch: u32,
519}
520impl fmt::Display for Checksum {
521 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522 write!(f, "{}", self.checksum_value)
523 }
524}
525impl From<&str> for ChecksumAlgorithm {
526 fn from(value: &str) -> Self {
527 match value.to_lowercase().as_str() {
528 | "sha256" => ChecksumAlgorithm::Sha256,
529 | "md5" => ChecksumAlgorithm::Md5,
530 | "sha1" => ChecksumAlgorithm::Sha1,
531 | "sha512" => ChecksumAlgorithm::Sha512,
532 | "md2" => ChecksumAlgorithm::Md2,
533 | "md4" => ChecksumAlgorithm::Md4,
534 | "md6" => ChecksumAlgorithm::Md6,
535 | "sha224" => ChecksumAlgorithm::Sha224,
536 | "sha384" => ChecksumAlgorithm::Sha384,
537 | _ => ChecksumAlgorithm::default(),
538 }
539 }
540}
541impl From<String> for ChecksumAlgorithm {
542 fn from(value: String) -> Self {
543 ChecksumAlgorithm::from(value.as_str())
544 }
545}
546impl<T: AsRef<str>> From<T> for License
547where
548 T: ToString,
549{
550 fn from(value: T) -> Self {
557 match value.as_ref().to_lowercase().as_str() {
558 | "agpl-3.0-only" => License::Agpl3Only,
559 | "apache-2.0" => License::Apache2,
560 | "bsd-2-clause" | "bsd-3-clause" => License::Bsd3Clause,
561 | "cc0-1.0" | "creative commons cc-0" => License::CreativeCommons,
562 | "gpl-1.0-or-later" | "gpl-2.0-only" => License::Gpl2Only,
563 | "gpl-2.0-with-classpath-exception" => License::Gpl2WithClasspathException,
564 | "gpl-3.0-only" => License::Gpl3Only,
565 | "gpl-3.0-or-later" => License::Gpl3OrLater,
566 | "lgpl-2.1-only" => License::Lgpl21Only,
567 | "lppl-1.3c" => License::Lppl13c,
568 | "mit" => License::Mit,
569 | "postgresql" => License::PostgreSql,
570 | "proprietary" | "licenseref-proprietary" => License::Proprietary,
571 | "psf-based" => License::PsfBased,
572 | "psf-2.0" => License::Psf2,
573 | "public-domain" | "public domain" => License::PublicDomain,
574 | "various" => License::Various,
575 | "w3c" => License::W3C,
576 | _ => License::Unknown,
577 }
578 }
579}
580impl License {
581 #[allow(dead_code)]
582 fn from_technology(value: &str) -> Option<License> {
583 let data = Constant::csv("technology");
584 let result = data
585 .into_iter()
586 .map(|row| row.into_iter().take(5).collect::<Vec<String>>())
587 .find(|pair| pair.first().map(|s| s.as_str()) == Some(value));
588 match result {
589 | Some(pair) => pair.get(4).map(|s| License::from(s.clone())),
590 | None => None,
591 }
592 }
593 #[allow(dead_code)]
594 fn is_open_source(&self) -> bool {
595 let data = Constant::csv("technology");
596 let result = data
597 .into_iter()
598 .map(|row| row.into_iter().skip(4).take(2).collect::<Vec<String>>())
599 .find(|pair| pair.first().map(|s| s.as_str()) == Some(self.to_string().as_str()));
600 match result {
601 | Some(value) => value.get(1).map(|s| s.as_str()) == Some("true"),
602 | None => false,
603 }
604 }
605}
606impl From<&str> for MimeType {
607 fn from(value: &str) -> Self {
635 let name = value.to_lowercase();
636 match file_extension(name.clone()) {
637 | Some(value) => match value.as_str() {
638 | "cff" => MimeType::Cff,
639 | "csv" => MimeType::Csv,
640 | "doc" => MimeType::Doc,
641 | "docx" | "docm" => MimeType::Docx,
642 | "epub" => MimeType::Epub,
643 | "gguf" => MimeType::Gguf,
644 | "gz" => MimeType::Gzip,
645 | "jpg" | "jpeg" => MimeType::Jpeg,
646 | "json" => MimeType::Json,
647 | "jsonc" => MimeType::Jsonc,
648 | "jsonld" | "json-ld" => MimeType::LdJson,
649 | "md" | "markdown" => MimeType::Markdown,
650 | "onnx" => MimeType::Onnx,
651 | "odp" => MimeType::Odp,
652 | "ods" => MimeType::Ods,
653 | "odt" => MimeType::Odt,
654 | "otf" => MimeType::Otf,
655 | "ttf" => MimeType::Ttf,
656 | "parquet" => MimeType::Parquet,
657 | "pdf" => MimeType::Pdf,
658 | "png" => MimeType::Png,
659 | "pt" | "pth" => MimeType::Pytorch,
660 | "ppt" | "pps" | "pot" => MimeType::Ppt,
661 | "pptx" | "pptm" | "ppsx" | "ppsm" => MimeType::Powerpoint,
662 | "prompt" => MimeType::Prompt,
663 | "rtf" => MimeType::Rtf,
664 | "rs" => MimeType::Rust,
665 | "safetensors" => MimeType::Safetensors,
666 | "7z" => MimeType::SevenZip,
667 | "spdx.json" => MimeType::Sbom,
668 | "svg" => MimeType::Svg,
669 | "tar" => MimeType::Tar,
670 | "toml" => MimeType::Toml,
671 | "txt" => MimeType::Text,
672 | "xls" | "xlsb" | "xlsm" | "xlsx" => MimeType::Excel,
673 | "yml" | "yaml" => MimeType::Yaml,
674 | value => MimeType::Vendor(value.to_string()),
675 },
676 | None => MimeType::Unknown(name),
677 }
678 }
679}
680impl From<&String> for MimeType {
681 fn from(value: &String) -> Self {
682 Self::from(value.as_str())
683 }
684}
685impl From<String> for MimeType {
686 fn from(value: String) -> Self {
687 Self::from(value.as_str())
688 }
689}
690#[cfg(feature = "std")]
691impl From<anydoc::Format> for MimeType {
692 fn from(value: anydoc::Format) -> Self {
693 match value {
694 | anydoc::Format::Csv => Self::Csv,
695 | anydoc::Format::Doc => Self::Doc,
696 | anydoc::Format::Docx => Self::Docx,
697 | anydoc::Format::Epub => Self::Epub,
698 | anydoc::Format::Excel => Self::Excel,
699 | anydoc::Format::Odp => Self::Odp,
700 | anydoc::Format::Ods => Self::Ods,
701 | anydoc::Format::Odt => Self::Odt,
702 | anydoc::Format::Pdf => Self::Pdf,
703 | anydoc::Format::Ppt => Self::Ppt,
704 | anydoc::Format::Pptx => Self::Powerpoint,
705 | anydoc::Format::Rtf => Self::Rtf,
706 }
707 }
708}
709impl MimeType {
710 #[cfg(feature = "std")]
715 pub fn infer(bytes: &[u8]) -> Option<Self> {
716 anydoc::Format::from_bytes(bytes).map(Self::from).or_else(|| {
717 infer::get(bytes).and_then(|kind| match kind.mime_type() {
718 | "application/gzip" | "application/x-gzip" => Some(Self::Gzip),
719 | "application/pdf" => Some(Self::Pdf),
720 | "application/x-7z-compressed" => Some(Self::SevenZip),
721 | "application/zip" => Some(Self::Zip),
722 | "application/x-tar" => Some(Self::Tar),
723 | "image/jpeg" => Some(Self::Jpeg),
724 | "image/png" => Some(Self::Png),
725 | _ => None,
726 })
727 })
728 }
729 pub fn file_type(self) -> String {
738 match self {
739 | MimeType::Cff => "cff",
740 | MimeType::Csv => "csv",
741 | MimeType::Doc => "doc",
742 | MimeType::Docx => "docx",
743 | MimeType::Epub => "epub",
744 | MimeType::Excel => "xlsx",
745 | MimeType::Gguf => "gguf",
746 | MimeType::Gzip => "tar.gz",
747 | MimeType::Jpeg => "jpeg",
748 | MimeType::Json => "json",
749 | MimeType::Jsonc => "jsonc",
750 | MimeType::LdJson => "jsonld",
751 | MimeType::Markdown => "md",
752 | MimeType::ModelCard => "modelcard",
753 | MimeType::Onnx => "onnx",
754 | MimeType::Odp => "odp",
755 | MimeType::Ods => "ods",
756 | MimeType::Odt => "odt",
757 | MimeType::Otf => "otf",
758 | MimeType::Ttf => "ttf",
759 | MimeType::Parquet => "parquet",
760 | MimeType::Pdf => "pdf",
761 | MimeType::Png => "png",
762 | MimeType::Ppt => "ppt",
763 | MimeType::Pytorch => "pt",
764 | MimeType::Powerpoint => "pptx",
765 | MimeType::Prompt => "prompt",
766 | MimeType::Rtf => "rtf",
767 | MimeType::Rust => "rs",
768 | MimeType::Safetensors => "safetensors",
769 | MimeType::Sbom => "spdx.json",
770 | MimeType::SevenZip => "7z",
771 | MimeType::Svg => "svg",
772 | MimeType::Tar => "tar",
773 | MimeType::Text => "txt",
774 | MimeType::Toml => "toml",
775 | MimeType::Yaml => "yaml",
776 | MimeType::Zip => "zip",
777 | _ => "unknown-file-type",
778 }
779 .to_string()
780 }
781}
782impl Default for SemanticVersion {
783 fn default() -> Self {
784 SemanticVersion::init().build()
785 }
786}
787impl From<&str> for SemanticVersion {
788 fn from(value: &str) -> Self {
798 let token = value
799 .split(|c: char| !(c.is_ascii_digit() || c == '.'))
800 .find(|x: &&str| x.chars().any(|c: char| c.is_ascii_digit()))
801 .unwrap_or("");
802 let parts = token
803 .split('.')
804 .filter(|x: &&str| !x.is_empty())
805 .map(|x: &str| x.parse::<u32>())
806 .collect::<Vec<_>>();
807 match parts.as_slice() {
808 | [Ok(major), Ok(minor), Ok(patch)] => SemanticVersion::init().major(*major).minor(*minor).patch(*patch).build(),
809 | [Ok(major), Ok(minor)] => SemanticVersion::init().major(*major).minor(*minor).build(),
810 | [Ok(major)] => SemanticVersion::init().major(*major).build(),
811 | _ => SemanticVersion::default(),
812 }
813 }
814}
815impl SemanticVersion {
816 pub fn from_string(value: impl AsRef<str>) -> Self {
818 Self::from(value.as_ref())
819 }
820}
821impl<T: AsRef<str>> StringInterpolation<T> for T
822where
823 T: ToString,
824{
825 fn replace_placeholder_with_string(&self, placeholder: &str, value: &str) -> String {
826 match Regex::new(&format!(r"{{{{\s*{placeholder}\s*}}}}")) {
827 | Ok(re) => re.replace_all(self.as_ref(), value).to_string(),
828 | Err(err) => {
829 fail!("Regex replacement - {}", err);
830 self.to_string()
831 }
832 }
833 }
834 fn with_indent(&self, spaces: usize) -> String {
835 self.to_string()
836 .lines()
837 .map(|line| " ".repeat(spaces) + line.trim_start())
838 .collect::<Vec<_>>()
839 .join(LINE_SEPARATOR)
840 }
841 fn with_additional_indent(&self, spaces: usize) -> String {
842 let prefix = " ".repeat(spaces);
843 self.to_string()
844 .lines()
845 .map(|line| prefix.clone() + line)
846 .collect::<Vec<_>>()
847 .join(LINE_SEPARATOR)
848 }
849}
850impl MarkdownSupport for str {
851 fn to_markdown(&self) -> String {
852 self.to_string()
853 }
854}
855impl StringExt for str {
856 fn is_numeric(&self) -> bool {
857 self.chars().all(char::is_numeric)
858 }
859}
860impl StringExt for String {
861 fn is_numeric(&self) -> bool {
862 self.as_str().is_numeric()
863 }
864}
865impl MarkdownSupport for String {
866 fn to_markdown(&self) -> String {
867 self.clone()
868 }
869}
870impl<P: AsRef<str>> MarkdownSupport for Vec<P> {
871 fn to_markdown(&self) -> String {
872 if self.is_empty() {
873 "[]".to_string()
874 } else {
875 self.iter()
876 .map(|x| format!("{LINE_SEPARATOR}- {}", x.as_ref()))
877 .collect::<Vec<String>>()
878 .join("")
879 }
880 }
881}
882impl<P: AsRef<str>> MarkdownSupport for Option<Vec<P>> {
883 fn to_markdown(&self) -> String {
884 match &self {
885 | Some(values) => values.to_markdown(),
886 | None => "[]".to_string(),
887 }
888 }
889}
890impl<T: AsRef<str>> ToStringChunks<T> for T
891where
892 T: ToString,
893{
894 fn chunk(&self, size: usize) -> Vec<String> {
895 self.as_ref()
896 .as_bytes()
897 .chunks(size)
898 .filter_map(|chunk| String::from_utf8(chunk.to_vec()).ok())
899 .collect::<Vec<_>>()
900 }
901}
902pub fn base32_crockford_encode(value: u128) -> String {
914 if value == 0 {
915 "0".to_string()
916 } else {
917 const MODULUS: u128 = CROCKFORD_BASE32_ALPHABET.len() as u128;
918 successors(Some(value), |&n| (n >= MODULUS).then_some(n / MODULUS))
919 .map(|n| char::from(*CROCKFORD_BASE32_ALPHABET.get((n % MODULUS) as usize).unwrap_or(&0)))
920 .collect::<Vec<_>>()
921 .into_iter()
922 .rev()
923 .collect::<String>()
924 .to_ascii_lowercase()
925 }
926}
927pub fn base32_crockford_decode(value: impl AsRef<str>) -> Option<u128> {
942 const MODULUS: u128 = CROCKFORD_BASE32_ALPHABET.len() as u128;
943 value
944 .as_ref()
945 .chars()
946 .filter(|c| !c.is_whitespace() && *c != '-' && *c != '_')
947 .try_fold(0u128, |acc, c| {
948 let digit = crockford_digit(c)?;
949 match acc.checked_mul(MODULUS) {
950 | Some(value) => value.checked_add(digit),
951 | None => None,
952 }
953 })
954}
955pub fn constant_time_eq<A: AsRef<[u8]>, B: AsRef<[u8]>>(a: A, b: B) -> bool {
958 let (a, b) = (a.as_ref(), b.as_ref());
959 a.len() == b.len() && a.iter().zip(b.iter()).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
960}
961pub fn contains_any(patterns: &[&str], haystack: &str) -> bool {
963 match AhoCorasick::new(patterns) {
964 | Ok(matcher) => matcher.is_match(haystack),
965 | Err(_) => false,
966 }
967}
968pub fn contains_any_with_prefix(haystack: &str, prefix: &str, suffixes: &[&str]) -> bool {
970 haystack.contains(prefix) && contains_any(suffixes, haystack)
971}
972fn crockford_digit(value: char) -> Option<u128> {
973 let upper = value.to_ascii_uppercase();
974 let normalized = if upper == 'O' {
975 '0'
976 } else if upper == 'I' || upper == 'L' {
977 '1'
978 } else {
979 upper
980 };
981 let byte = normalized as u8;
982 CROCKFORD_BASE32_ALPHABET.iter().position(|&b| b == byte).map(|index| index as u128)
983}
984pub fn detect_json(text: impl ToString) -> bool {
986 let text = text.to_string();
987 let options = ParseOptions {
988 allow_comments: true,
989 allow_trailing_commas: true,
990 allow_loose_object_property_names: false,
991 allow_missing_commas: false,
992 allow_single_quoted_strings: false,
993 allow_hexadecimal_numbers: false,
994 allow_unary_plus_numbers: false,
995 };
996 serde_json::from_str::<Value>(&text).is_ok() || parse_to_serde_value::<Value>(&text, &options).is_ok()
997}
998pub fn detect_xml(text: impl ToString) -> bool {
1000 let content = text.to_string();
1001 let trimmed = content.trim();
1002 if trimmed.starts_with('<') {
1003 let mut reader = quick_xml::Reader::from_str(trimmed);
1004 let mut buf = vec![];
1005 loop {
1006 match reader.read_event_into(&mut buf) {
1007 | Ok(quick_xml::events::Event::Eof) => return true,
1008 | Err(_) => return false,
1009 | _ => buf.clear(),
1010 }
1011 }
1012 } else {
1013 false
1014 }
1015}
1016pub fn file_extension<S>(value: S) -> Option<String>
1028where
1029 S: Into<String>,
1030{
1031 let filename = value.into();
1032 let segments = filename.split('.').filter(|x| !x.is_empty()).collect::<Vec<_>>();
1033 if !segments.is_empty() {
1034 let last_segment = segments.last().map(|value| (*value).to_string());
1035 let has_extension = filename.contains(".") && segments.len() > 1;
1036 match last_segment {
1037 | Some(value) => {
1038 let is_filename = !(value.contains("/") || value.is_empty());
1039 if has_extension && is_filename {
1040 Some(value)
1041 } else {
1042 None
1043 }
1044 }
1045 | None => None,
1046 }
1047 } else {
1048 None
1049 }
1050}
1051pub fn find_first(values: Vec<(String, String)>, pattern: &str) -> Option<(String, String)> {
1062 let results = values
1063 .clone()
1064 .into_iter()
1065 .filter(|x| !x.1.is_empty())
1066 .find(|(key, _)| key.starts_with(pattern));
1067 match results {
1068 | Some(value) => Some(value),
1069 | None => None,
1070 }
1071}
1072pub fn format_bytes(bytes: u64) -> String {
1074 let units = ["B", "KB", "MB", "GB", "TB"];
1075 let (size, index) = successors(Some((bytes as f64, 0usize)), |(size, index)| {
1076 if *size >= 1024.0 && *index < units.len().saturating_sub(1) {
1077 Some((size / 1024.0, index.saturating_add(1)))
1078 } else {
1079 None
1080 }
1081 })
1082 .last()
1083 .unwrap_or((bytes as f64, 0));
1084 if index == 0 {
1085 format!("{} {}", bytes, units.get(index).unwrap_or(&""))
1086 } else {
1087 format!("{:.2} {}", size, units.get(index).unwrap_or(&""))
1088 }
1089}
1090pub fn frontmatter_and_body<S>(value: S) -> (Option<String>, String)
1107where
1108 S: AsRef<str>,
1109{
1110 let content = value.as_ref();
1111 let pattern = r"(?s)---\s*(?<frontmatter>.*?)\s*---\s*(?<body>.*)";
1112 let groups = vec!["frontmatter", "body"];
1113 let lookup = regex_capture_lookup(pattern, content, groups);
1114 (
1115 lookup.get("frontmatter").cloned().filter(|s| !s.is_empty()),
1116 lookup.get("body").cloned().unwrap_or_else(|| content.trim().to_string()),
1117 )
1118}
1119pub fn generate_guid() -> String {
1121 #[cfg(feature = "std")]
1122 {
1123 let alphabet = [
1124 '-', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'T', 'U', 'V', 'W', 'X', 'Y', 'a', 'b', 'c',
1125 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 't', 'w', 'x', 'y', 'z', '3', '4', '6', '7', '8', '9',
1126 ];
1127 nanoid!(10, &alphabet)
1128 }
1129 #[cfg(not(feature = "std"))]
1130 String::new()
1131}
1132pub fn glob_matches(path: &str, pattern: &str) -> bool {
1134 fn go(path: &[u8], pattern: &[u8]) -> bool {
1135 match pattern.split_first() {
1136 | None => path.is_empty(),
1137 | Some((b'*', rest)) => go(path, rest) || path.split_first().is_some_and(|(_, tail)| go(tail, pattern)),
1138 | Some((b'?', rest)) => path.split_first().is_some_and(|(_, tail)| go(tail, rest)),
1139 | Some((p, rest)) => path.split_first().is_some_and(|(q, tail)| p == q && go(tail, rest)),
1140 }
1141 }
1142 go(path.as_bytes(), pattern.as_bytes())
1143}
1144#[cfg(feature = "std")]
1148pub fn is_filetype<I, S, P>(extensions: I) -> impl Fn(&P) -> bool
1149where
1150 I: IntoIterator<Item = S>,
1151 S: AsRef<str>,
1152 P: AsRef<Path> + ?Sized,
1153{
1154 let extensions = extensions
1155 .into_iter()
1156 .map(|extension| extension.as_ref().trim_start_matches('.').to_ascii_lowercase())
1157 .collect::<Vec<_>>();
1158 move |path| {
1159 let path = path.as_ref();
1160 let filename = path.file_name().and_then(|value| value.to_str()).unwrap_or_default();
1161 let extension = path.extension().and_then(|value| value.to_str()).unwrap_or_default();
1162 extensions
1163 .iter()
1164 .any(|allowed| filename.eq_ignore_ascii_case(allowed) || extension.eq_ignore_ascii_case(allowed))
1165 }
1166}
1167pub fn is_uri_or_path(value: &str) -> bool {
1169 value.starts_with('/')
1170 || value.starts_with("./")
1171 || value.starts_with("../")
1172 || {
1173 #[cfg(feature = "std")]
1174 {
1175 Path::new(value).is_absolute()
1176 }
1177 #[cfg(not(feature = "std"))]
1178 {
1179 false
1180 }
1181 }
1182 || (if let Ok(uri) = UriRef::parse(value) {
1183 uri.scheme().is_some()
1184 } else {
1185 false
1186 })
1187}
1188pub fn merge<A, B, S, T>(a: A, b: Option<B>) -> Vec<String>
1192where
1193 A: IntoIterator<Item = S>,
1194 B: IntoIterator<Item = T>,
1195 S: AsRef<str>,
1196 T: AsRef<str>,
1197{
1198 let mut seen = HashSet::new();
1199 a.into_iter()
1200 .map(|value| value.as_ref().trim().to_string())
1201 .chain(b.into_iter().flatten().map(|value| value.as_ref().trim().to_string()))
1202 .filter(|value| !value.is_empty() && seen.insert(value.clone()))
1203 .collect()
1204}
1205pub fn merge_unique<T: Ord>(left: impl IntoIterator<Item = T>, right: impl IntoIterator<Item = T>) -> Vec<T> {
1207 left.into_iter().chain(right).collect::<BTreeSet<_>>().into_iter().collect()
1208}
1209pub fn regex_capture_lookup<S>(pattern: S, text: S, groups: Vec<S>) -> HashMap<S, String>
1226where
1227 S: Into<String> + AsRef<str> + Clone + core::cmp::Eq + core::hash::Hash,
1228{
1229 match Regex::new(pattern.as_ref()) {
1230 | Ok(re) => re
1231 .captures_iter(text.as_ref())
1232 .last()
1233 .and_then(Result::ok)
1234 .map(|captures| {
1235 captures
1236 .iter()
1237 .skip(1)
1238 .enumerate()
1239 .filter_map(|(index, data)| data.and_then(|results| groups.get(index).cloned().map(|key| (key, results.as_str().to_string()))))
1240 .collect::<HashMap<S, String>>()
1241 })
1242 .unwrap_or_default(),
1243 | Err(_) => HashMap::new(),
1244 }
1245}
1246pub fn regex_join(patterns: &[String]) -> Option<String> {
1248 let groups = patterns
1249 .iter()
1250 .filter(|pattern| !pattern.is_empty())
1251 .map(|pattern| format!("(?:{pattern})"))
1252 .collect::<Vec<String>>();
1253 match groups.is_empty() {
1254 | true => None,
1255 | false => Some(groups.join("|")),
1256 }
1257}
1258pub fn regex_inverse(pattern: impl AsRef<str>) -> String {
1260 format!("^(?!.*(?:{})).*$", pattern.as_ref())
1261}
1262pub fn regex_to_glob(pattern: impl AsRef<str>) -> Option<String> {
1272 let mut bytes = pattern.as_ref().as_bytes().iter().peekable();
1273 let mut glob = String::new();
1274 let mut ok = true;
1275 let mut anchored_start = false;
1276 let mut anchored_end = false;
1277 if bytes.peek() == Some(&&b'^') {
1278 anchored_start = true;
1279 bytes.next();
1280 }
1281 while let Some(&byte) = bytes.next() {
1282 match byte {
1283 | b'\\' => match bytes.next() {
1284 | Some(&b'.') => glob.push('.'),
1285 | Some(b'd' | b'D' | b'w' | b'W' | b's' | b'S' | b'b' | b'B') | None => ok = false,
1286 | Some(&other) => glob.push(other as char),
1287 },
1288 | b'.' if bytes.peek() == Some(&&b'*') => {
1289 bytes.next();
1290 glob.push('*');
1291 }
1292 | b'.' if bytes.peek() == Some(&&b'+') => {
1293 bytes.next();
1294 glob.push('?');
1295 glob.push('*');
1296 }
1297 | b'.' | b'?' | b'+' | b'[' | b'(' | b'|' | b'{' => ok = false,
1298 | b'$' if bytes.peek().is_some() => ok = false,
1299 | b'$' => anchored_end = true,
1300 | other => glob.push(other as char),
1301 }
1302 }
1303 if ok && !glob.is_empty() {
1304 if !anchored_start {
1305 glob.insert(0, '*');
1306 }
1307 if !anchored_end {
1308 glob.push('*');
1309 }
1310 Some(glob)
1311 } else {
1312 None
1313 }
1314}
1315pub fn strip_suffixes<'a>(values: &[&str], name: &'a str) -> &'a str {
1317 values.iter().find_map(|value| name.strip_suffix(value)).unwrap_or(name)
1318}
1319pub fn suffix<T>(value: T) -> String
1330where
1331 T: PartialEq + From<u8>,
1332{
1333 (if value == T::from(1) { "" } else { "s" }).to_string()
1334}
1335pub fn to_ascii_alphanumeric(value: &str) -> String {
1337 value
1338 .chars()
1339 .filter(|character| character.is_ascii_alphanumeric())
1340 .collect::<String>()
1341 .to_ascii_lowercase()
1342}
1343pub fn to_rfc3339(value: Timestamp) -> String {
1345 value.display_with_offset(Offset::UTC).to_string()
1346}
1347pub fn to_string(values: Vec<&str>) -> Vec<String> {
1349 values.iter().map(|s| s.to_string()).collect()
1350}
1351pub fn trim_unmatched_trailing_parentheses(value: &str) -> &str {
1353 let opening = value.chars().filter(|character| *character == '(').count();
1354 let closing = value.chars().filter(|character| *character == ')').count();
1355 let trailing = value.chars().rev().take_while(|character| *character == ')').count();
1356 let trim = closing.saturating_sub(opening).min(trailing);
1357 value.get(..value.len().saturating_sub(trim)).unwrap_or(value)
1358}
1359
1360#[cfg(test)]
1361mod tests;