Skip to main content

gix_config_value/
integer.rs

1use std::{borrow::Cow, fmt::Display, str::FromStr};
2
3use bstr::{BStr, BString, ByteSlice};
4use gix_error::{ErrorExt, Message, ResultExt, validation};
5
6use crate::Integer;
7
8impl Integer {
9    /// Canonicalize values as simple decimal numbers.
10    /// An optional suffix of k, m, or g (case-insensitive), will cause the
11    /// value to be multiplied by 1024 (k), 1048576 (m), or 1073741824 (g) respectively.
12    ///
13    /// Returns the result if there is no multiplication overflow.
14    pub fn to_decimal(&self) -> Option<i64> {
15        match self.suffix {
16            None => Some(self.value),
17            Some(suffix) => match suffix {
18                Suffix::Kibi => self.value.checked_mul(1024),
19                Suffix::Mebi => self.value.checked_mul(1024 * 1024),
20                Suffix::Gibi => self.value.checked_mul(1024 * 1024 * 1024),
21            },
22        }
23    }
24}
25
26impl Display for Integer {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(f, "{}", self.value)?;
29        if let Some(suffix) = self.suffix {
30            write!(f, "{suffix}")
31        } else {
32            Ok(())
33        }
34    }
35}
36
37#[cfg(feature = "serde")]
38impl serde::Serialize for Integer {
39    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
40    where
41        S: serde::Serializer,
42    {
43        if let Some(suffix) = self.suffix {
44            serializer.serialize_i64(self.value << suffix.bitwise_offset())
45        } else {
46            serializer.serialize_i64(self.value)
47        }
48    }
49}
50
51fn int_err(input: impl Into<BString>) -> Message {
52    validation("Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G")
53        .with("input", gix_error::MetadataValue::Bytes(input.into()))
54}
55
56/// Parse `input` the way `git_parse_signed()` does, which hands the value to
57/// `strtoimax()` with a base of `0`: an optional sign, then hexadecimal behind a `0x`
58/// prefix, binary behind a `0b` prefix, octal behind a `0` prefix, and decimal otherwise.
59fn parse_like_git(input: &str) -> Option<i64> {
60    let (negative, rest) = match input.as_bytes().first() {
61        Some(b'+') => (false, &input[1..]),
62        Some(b'-') => (true, &input[1..]),
63        _ => (false, input),
64    };
65
66    let Some(prefixed) = rest.strip_prefix('0') else {
67        return input.parse().ok();
68    };
69    let (digits, radix) = if let Some(hexadecimal) = prefixed.strip_prefix(['x', 'X']) {
70        (hexadecimal, 16)
71    } else if let Some(binary) = prefixed.strip_prefix(['b', 'B']) {
72        (binary, 2)
73    } else if !prefixed.is_empty() {
74        (prefixed, 8)
75    } else {
76        return input.parse().ok();
77    };
78
79    if digits.starts_with('+') || digits.starts_with('-') {
80        return None;
81    }
82    let magnitude = i128::from_str_radix(digits, radix).ok()?;
83    i64::try_from(if negative { -magnitude } else { magnitude }).ok()
84}
85
86impl TryFrom<&BStr> for Integer {
87    type Error = gix_error::Exn<gix_error::Message>;
88
89    fn try_from(s: &BStr) -> Result<Self, Self::Error> {
90        let s = std::str::from_utf8(s).or_raise(|| int_err(s))?;
91        if let Some(value) = parse_like_git(s) {
92            return Ok(Self { value, suffix: None });
93        }
94
95        if s.len() <= 1 {
96            return Err(int_err(s).raise());
97        }
98
99        let last_idx = s.len() - 1;
100        if !s.is_char_boundary(last_idx) {
101            return Err(int_err(s).raise());
102        }
103
104        let (number, suffix) = s.split_at(s.len() - 1);
105        if let (Some(value), Ok(suffix)) = (parse_like_git(number), suffix.parse()) {
106            Ok(Self {
107                value,
108                suffix: Some(suffix),
109            })
110        } else {
111            Err(int_err(s).raise())
112        }
113    }
114}
115
116impl TryFrom<&str> for Integer {
117    type Error = gix_error::Exn<gix_error::Message>;
118
119    fn try_from(value: &str) -> Result<Self, Self::Error> {
120        Self::try_from(BStr::new(value))
121    }
122}
123
124impl TryFrom<Cow<'_, BStr>> for Integer {
125    type Error = gix_error::Exn<gix_error::Message>;
126
127    fn try_from(c: Cow<'_, BStr>) -> Result<Self, Self::Error> {
128        Self::try_from(c.as_ref())
129    }
130}
131
132impl TryFrom<BString> for Integer {
133    type Error = gix_error::Exn<gix_error::Message>;
134
135    fn try_from(value: BString) -> Result<Self, Self::Error> {
136        Self::try_from(BStr::new(&value))
137    }
138}
139
140/// Integer suffixes that are supported by `git-config`.
141///
142/// These values are base-2 unit of measurements, not the base-10 variants.
143#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
144pub enum Suffix {
145    /// Multiply the value by 2^10.
146    Kibi,
147    /// Multiply the value by 2^20.
148    Mebi,
149    /// Multiply the value by 2^30.
150    Gibi,
151}
152
153impl Suffix {
154    /// Returns the number of bits that the suffix shifts left by.
155    #[must_use]
156    pub const fn bitwise_offset(self) -> usize {
157        match self {
158            Self::Kibi => 10,
159            Self::Mebi => 20,
160            Self::Gibi => 30,
161        }
162    }
163}
164
165impl Display for Suffix {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        match self {
168            Self::Kibi => write!(f, "k"),
169            Self::Mebi => write!(f, "m"),
170            Self::Gibi => write!(f, "g"),
171        }
172    }
173}
174
175#[cfg(feature = "serde")]
176impl serde::Serialize for Suffix {
177    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
178    where
179        S: serde::Serializer,
180    {
181        serializer.serialize_str(match self {
182            Self::Kibi => "k",
183            Self::Mebi => "m",
184            Self::Gibi => "g",
185        })
186    }
187}
188
189impl FromStr for Suffix {
190    type Err = ();
191
192    fn from_str(s: &str) -> Result<Self, Self::Err> {
193        Self::try_from(BStr::new(s))
194    }
195}
196
197impl TryFrom<&BStr> for Suffix {
198    type Error = ();
199
200    fn try_from(s: &BStr) -> Result<Self, Self::Error> {
201        match s.as_bytes() {
202            b"k" | b"K" => Ok(Self::Kibi),
203            b"m" | b"M" => Ok(Self::Mebi),
204            b"g" | b"G" => Ok(Self::Gibi),
205            _ => Err(()),
206        }
207    }
208}