1#.
19
20"
21)]
22use std::fmt;
43
44const UNLIMITED_NAME: &str = "unlimited";
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub struct Limit(Option<u64>);
54
55impl Ord for Limit {
61 fn cmp(&self, other: &Limit) -> std::cmp::Ordering {
62 match (self.0, other.0) {
63 (Some(left), Some(right)) => left.cmp(&right),
64 (None, None) => std::cmp::Ordering::Equal,
65 (None, Some(_)) => std::cmp::Ordering::Greater,
66 (Some(_), None) => std::cmp::Ordering::Less,
67 }
68 }
69}
70
71impl PartialOrd for Limit {
72 fn partial_cmp(&self, other: &Limit) -> Option<std::cmp::Ordering> {
73 Some(self.cmp(other))
74 }
75}
76
77impl Limit {
78 pub const UNLIMITED: Limit = Limit(None);
83
84 pub const fn of(amount: u64) -> Limit {
90 if amount == u64::MAX {
91 Limit::UNLIMITED
92 } else {
93 Limit(Some(amount))
94 }
95 }
96
97 pub const fn amount(self) -> Option<u64> {
100 self.0
101 }
102
103 pub(crate) fn fits_within(self, other: Limit) -> bool {
105 match (self.0, other.0) {
106 (_, None) => true,
107 (None, Some(_)) => false,
108 (Some(soft), Some(hard)) => soft <= hard,
109 }
110 }
111}
112
113impl From<u64> for Limit {
114 fn from(amount: u64) -> Limit {
115 Limit::of(amount)
116 }
117}
118
119impl fmt::Display for Limit {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 match self.0 {
122 Some(amount) => write!(f, "{amount}"),
123 None => f.write_str(UNLIMITED_NAME),
124 }
125 }
126}
127
128#[cfg(feature = "serde")]
130impl serde::Serialize for Limit {
131 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
132 match self.0 {
133 Some(amount) => serializer.serialize_u64(amount),
134 None => serializer.serialize_str(UNLIMITED_NAME),
135 }
136 }
137}
138
139#[cfg(feature = "serde")]
141impl<'de> serde::Deserialize<'de> for Limit {
142 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
143 deserializer.deserialize_any(LimitVisitor)
144 }
145}
146
147#[cfg(feature = "serde")]
148struct LimitVisitor;
149
150#[cfg(feature = "serde")]
151impl serde::de::Visitor<'_> for LimitVisitor {
152 type Value = Limit;
153
154 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 write!(f, "a non-negative amount or {UNLIMITED_NAME:?}")
156 }
157
158 fn visit_u64<E: serde::de::Error>(self, amount: u64) -> Result<Limit, E> {
159 Ok(Limit::of(amount))
160 }
161
162 fn visit_i64<E: serde::de::Error>(self, amount: i64) -> Result<Limit, E> {
164 u64::try_from(amount)
165 .map(Limit::of)
166 .map_err(|_| E::custom(format!("a resource limit cannot be negative: {amount}")))
167 }
168
169 fn visit_str<E: serde::de::Error>(self, text: &str) -> Result<Limit, E> {
170 if text == UNLIMITED_NAME {
171 Ok(Limit::UNLIMITED)
172 } else {
173 Err(E::custom(format!(
174 "expected an amount or {UNLIMITED_NAME:?}, not {text:?}"
175 )))
176 }
177 }
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
201#[cfg_attr(
202 feature = "serde",
203 derive(serde::Serialize, serde::Deserialize),
204 serde(rename_all = "kebab-case")
205)]
206#[non_exhaustive]
207pub enum Resource {
208 AddressSpace,
210 CoreDump,
212 CpuTime,
215 Data,
218 FileSize,
221 LockedMemory,
223 OpenFiles,
226 PendingSignals,
229 Processes,
232 Stack,
234}
235
236impl Resource {
237 pub const ALL: &'static [Resource] = &[
245 Resource::AddressSpace,
246 Resource::CoreDump,
247 Resource::CpuTime,
248 Resource::Data,
249 Resource::FileSize,
250 Resource::LockedMemory,
251 Resource::OpenFiles,
252 Resource::PendingSignals,
253 Resource::Processes,
254 Resource::Stack,
255 ];
256
257 pub fn spelling(self) -> &'static str {
264 match self {
265 Resource::AddressSpace => "address-space",
266 Resource::CoreDump => "core-dump",
267 Resource::CpuTime => "cpu-time",
268 Resource::Data => "data",
269 Resource::FileSize => "file-size",
270 Resource::LockedMemory => "locked-memory",
271 Resource::OpenFiles => "open-files",
272 Resource::PendingSignals => "pending-signals",
273 Resource::Processes => "processes",
274 Resource::Stack => "stack",
275 }
276 }
277
278 pub(crate) fn to_kernel(self) -> rustix::process::Resource {
280 use rustix::process::Resource as Kernel;
281 match self {
282 Resource::AddressSpace => Kernel::As,
283 Resource::CoreDump => Kernel::Core,
284 Resource::CpuTime => Kernel::Cpu,
285 Resource::Data => Kernel::Data,
286 Resource::FileSize => Kernel::Fsize,
287 Resource::LockedMemory => Kernel::Memlock,
288 Resource::OpenFiles => Kernel::Nofile,
289 Resource::PendingSignals => Kernel::Sigpending,
290 Resource::Processes => Kernel::Nproc,
291 Resource::Stack => Kernel::Stack,
292 }
293 }
294
295 pub(crate) fn kernel_name(self) -> &'static str {
297 match self {
298 Resource::AddressSpace => "RLIMIT_AS",
299 Resource::CoreDump => "RLIMIT_CORE",
300 Resource::CpuTime => "RLIMIT_CPU",
301 Resource::Data => "RLIMIT_DATA",
302 Resource::FileSize => "RLIMIT_FSIZE",
303 Resource::LockedMemory => "RLIMIT_MEMLOCK",
304 Resource::OpenFiles => "RLIMIT_NOFILE",
305 Resource::PendingSignals => "RLIMIT_SIGPENDING",
306 Resource::Processes => "RLIMIT_NPROC",
307 Resource::Stack => "RLIMIT_STACK",
308 }
309 }
310}
311
312impl fmt::Display for Resource {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 f.write_str(self.kernel_name())
315 }
316}
317
318#[cfg(feature = "serde")]
327pub(crate) mod serde_rlimits {
328 use std::fmt;
329
330 use super::{Limit, Resource};
331 use serde::Deserialize as _;
332 use serde::de::{Error as _, MapAccess, Visitor};
333 use serde::ser::SerializeMap;
334 use std::collections::BTreeMap;
335
336 #[derive(serde::Serialize)]
338 struct Pair {
339 soft: Limit,
340 hard: Limit,
341 }
342
343 pub(crate) fn serialize<S: serde::Serializer>(
344 rlimits: &BTreeMap<Resource, (Limit, Limit)>,
345 serializer: S,
346 ) -> Result<S::Ok, S::Error> {
347 let mut map = serializer.serialize_map(Some(rlimits.len()))?;
348 for (resource, &(soft, hard)) in rlimits {
349 if soft == hard {
350 map.serialize_entry(resource, &soft)?;
351 } else {
352 map.serialize_entry(resource, &Pair { soft, hard })?;
353 }
354 }
355 map.end()
356 }
357
358 pub(crate) fn deserialize<'de, D: serde::Deserializer<'de>>(
359 deserializer: D,
360 ) -> Result<BTreeMap<Resource, (Limit, Limit)>, D::Error> {
361 let settings = BTreeMap::<Resource, Setting>::deserialize(deserializer)?;
362 Ok(settings
363 .into_iter()
364 .map(|(resource, Setting(soft, hard))| (resource, (soft, hard)))
365 .collect())
366 }
367
368 struct Setting(Limit, Limit);
370
371 impl<'de> serde::Deserialize<'de> for Setting {
372 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
373 deserializer.deserialize_any(SettingVisitor)
374 }
375 }
376
377 struct SettingVisitor;
378
379 impl<'de> Visitor<'de> for SettingVisitor {
380 type Value = Setting;
381
382 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383 f.write_str(
384 "a resource limit: one value for both the soft and the hard limit, \
385 or a table of `soft` and `hard`",
386 )
387 }
388
389 fn visit_u64<E: serde::de::Error>(self, amount: u64) -> Result<Setting, E> {
392 let limit = super::LimitVisitor.visit_u64(amount)?;
393 Ok(Setting(limit, limit))
394 }
395
396 fn visit_i64<E: serde::de::Error>(self, amount: i64) -> Result<Setting, E> {
397 let limit = super::LimitVisitor.visit_i64(amount)?;
398 Ok(Setting(limit, limit))
399 }
400
401 fn visit_str<E: serde::de::Error>(self, text: &str) -> Result<Setting, E> {
402 let limit = super::LimitVisitor.visit_str(text)?;
403 Ok(Setting(limit, limit))
404 }
405
406 fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Setting, M::Error> {
407 let (mut soft, mut hard) = (None, None);
408 while let Some(key) = map.next_key::<String>()? {
409 let slot = match key.as_str() {
410 "soft" => &mut soft,
411 "hard" => &mut hard,
412 other => return Err(M::Error::unknown_field(other, &["soft", "hard"])),
413 };
414 if slot.is_some() {
415 return Err(M::Error::custom(format!("duplicate field `{key}`")));
416 }
417 *slot = Some(map.next_value::<Limit>()?);
418 }
419 Ok(Setting(
420 soft.ok_or_else(|| M::Error::missing_field("soft"))?,
421 hard.ok_or_else(|| M::Error::missing_field("hard"))?,
422 ))
423 }
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use std::cmp::Ordering;
430
431 use super::*;
432
433 #[test]
434 fn the_comparison_operators_agree_with_fits_within() {
435 assert!(Limit::UNLIMITED > Limit::of(0));
440 assert!(Limit::UNLIMITED > Limit::of(u64::MAX - 1));
441 assert!(Limit::of(1) < Limit::of(2));
442 assert_eq!(Limit::UNLIMITED.cmp(&Limit::UNLIMITED), Ordering::Equal);
443 for (soft, hard) in [
444 (Limit::of(1), Limit::UNLIMITED),
445 (Limit::UNLIMITED, Limit::UNLIMITED),
446 (Limit::UNLIMITED, Limit::of(u64::MAX - 1)),
447 (Limit::of(2), Limit::of(1)),
448 (Limit::of(1), Limit::of(2)),
449 ] {
450 assert_eq!(
451 soft.fits_within(hard),
452 soft <= hard,
453 "{soft} within {hard} disagrees with the ordering",
454 );
455 }
456 }
457
458 #[test]
459 fn unlimited_is_the_ceiling_in_both_directions() {
460 assert!(Limit::of(1).fits_within(Limit::UNLIMITED));
461 assert!(Limit::UNLIMITED.fits_within(Limit::UNLIMITED));
462 assert!(!Limit::UNLIMITED.fits_within(Limit::of(u64::MAX - 1)));
465 }
466
467 #[test]
468 fn a_finite_soft_limit_must_not_exceed_its_hard_limit() {
469 assert!(Limit::of(4).fits_within(Limit::of(4)));
470 assert!(Limit::of(4).fits_within(Limit::of(5)));
471 assert!(!Limit::of(5).fits_within(Limit::of(4)));
472 }
473
474 #[test]
475 fn a_limit_renders_its_amount_or_the_word() {
476 assert_eq!(Limit::of(64).to_string(), "64");
477 assert_eq!(Limit::UNLIMITED.to_string(), "unlimited");
478 }
479
480 fn successor(resource: Resource) -> Option<Resource> {
488 match resource {
489 Resource::AddressSpace => Some(Resource::CoreDump),
490 Resource::CoreDump => Some(Resource::CpuTime),
491 Resource::CpuTime => Some(Resource::Data),
492 Resource::Data => Some(Resource::FileSize),
493 Resource::FileSize => Some(Resource::LockedMemory),
494 Resource::LockedMemory => Some(Resource::OpenFiles),
495 Resource::OpenFiles => Some(Resource::PendingSignals),
496 Resource::PendingSignals => Some(Resource::Processes),
497 Resource::Processes => Some(Resource::Stack),
498 Resource::Stack => None,
499 }
500 }
501
502 #[test]
509 fn the_roster_holds_every_resource_in_the_documented_order() {
510 let mut chain = vec![Resource::AddressSpace];
511 while let Some(next) = successor(*chain.last().expect("the chain starts somewhere")) {
512 chain.push(next);
513 }
514 assert_eq!(Resource::ALL, chain);
515 }
516
517 #[cfg(feature = "serde")]
523 #[test]
524 fn every_resources_spelling_is_the_name_a_profile_accepts() {
525 use serde::Deserialize as _;
526 for resource in Resource::ALL {
527 let parsed = Resource::deserialize(serde::de::value::StrDeserializer::<
528 serde::de::value::Error,
529 >::new(resource.spelling()))
530 .unwrap_or_else(|err| panic!("{:?} should parse: {err}", resource.spelling()));
531 assert_eq!(parsed, *resource);
532 }
533 }
534
535 #[test]
536 fn the_kernels_infinity_is_no_limit_rather_than_an_amount() {
537 assert_eq!(Limit::of(u64::MAX), Limit::UNLIMITED);
540 assert_eq!(Limit::of(u64::MAX).amount(), None);
541 assert_eq!(Limit::of(u64::MAX - 1).amount(), Some(u64::MAX - 1));
542 }
543}