use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
pub const DEFAULT_GRACE_PERIOD: u64 = 7 * 24 * 60 * 60;
pub const NO_LIMIT: u64 = 0;
pub const QUOTA_BLOCK_SIZE: u64 = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum QuotaType {
User = 1,
Group = 2,
Project = 3,
}
impl QuotaType {
pub fn from_u8(val: u8) -> Option<Self> {
match val {
1 => Some(Self::User),
2 => Some(Self::Group),
3 => Some(Self::Project),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
Self::User => "user",
Self::Group => "group",
Self::Project => "project",
}
}
}
impl fmt::Display for QuotaType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct QuotaKey {
pub quota_type: QuotaType,
pub id: u32,
}
impl QuotaKey {
pub fn new(quota_type: QuotaType, id: u32) -> Self {
Self { quota_type, id }
}
pub fn user(uid: u32) -> Self {
Self::new(QuotaType::User, uid)
}
pub fn group(gid: u32) -> Self {
Self::new(QuotaType::Group, gid)
}
pub fn project(project_id: u32) -> Self {
Self::new(QuotaType::Project, project_id)
}
}
impl fmt::Display for QuotaKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.quota_type, self.id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QuotaLimits {
pub soft_bytes: u64,
pub hard_bytes: u64,
pub soft_inodes: u64,
pub hard_inodes: u64,
pub grace_period: u64,
}
impl QuotaLimits {
pub fn unlimited() -> Self {
Self {
soft_bytes: NO_LIMIT,
hard_bytes: NO_LIMIT,
soft_inodes: NO_LIMIT,
hard_inodes: NO_LIMIT,
grace_period: DEFAULT_GRACE_PERIOD,
}
}
pub fn bytes(soft: u64, hard: u64) -> Self {
Self {
soft_bytes: soft,
hard_bytes: hard,
soft_inodes: NO_LIMIT,
hard_inodes: NO_LIMIT,
grace_period: DEFAULT_GRACE_PERIOD,
}
}
pub fn inodes(soft: u64, hard: u64) -> Self {
Self {
soft_bytes: NO_LIMIT,
hard_bytes: NO_LIMIT,
soft_inodes: soft,
hard_inodes: hard,
grace_period: DEFAULT_GRACE_PERIOD,
}
}
pub fn full(soft_bytes: u64, hard_bytes: u64, soft_inodes: u64, hard_inodes: u64) -> Self {
Self {
soft_bytes,
hard_bytes,
soft_inodes,
hard_inodes,
grace_period: DEFAULT_GRACE_PERIOD,
}
}
pub fn with_grace_period(mut self, seconds: u64) -> Self {
self.grace_period = seconds;
self
}
pub fn has_byte_limits(&self) -> bool {
self.soft_bytes > 0 || self.hard_bytes > 0
}
pub fn has_inode_limits(&self) -> bool {
self.soft_inodes > 0 || self.hard_inodes > 0
}
pub fn has_any_limits(&self) -> bool {
self.has_byte_limits() || self.has_inode_limits()
}
}
impl Default for QuotaLimits {
fn default() -> Self {
Self::unlimited()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct QuotaUsage {
pub bytes_used: u64,
pub inodes_used: u64,
pub soft_bytes_exceeded_at: u64,
pub soft_inodes_exceeded_at: u64,
}
impl QuotaUsage {
pub fn zero() -> Self {
Self::default()
}
pub fn new(bytes: u64, inodes: u64) -> Self {
Self {
bytes_used: bytes,
inodes_used: inodes,
soft_bytes_exceeded_at: 0,
soft_inodes_exceeded_at: 0,
}
}
pub fn add_bytes(&mut self, bytes: u64) {
self.bytes_used = self.bytes_used.saturating_add(bytes);
}
pub fn sub_bytes(&mut self, bytes: u64) {
self.bytes_used = self.bytes_used.saturating_sub(bytes);
}
pub fn add_inode(&mut self) {
self.inodes_used = self.inodes_used.saturating_add(1);
}
pub fn sub_inode(&mut self) {
self.inodes_used = self.inodes_used.saturating_sub(1);
}
pub fn is_bytes_in_grace(&self, now: u64, grace_period: u64) -> bool {
if self.soft_bytes_exceeded_at == 0 {
return false;
}
let elapsed = now.saturating_sub(self.soft_bytes_exceeded_at);
elapsed < grace_period
}
pub fn is_inodes_in_grace(&self, now: u64, grace_period: u64) -> bool {
if self.soft_inodes_exceeded_at == 0 {
return false;
}
let elapsed = now.saturating_sub(self.soft_inodes_exceeded_at);
elapsed < grace_period
}
pub fn is_bytes_grace_expired(&self, now: u64, grace_period: u64) -> bool {
if self.soft_bytes_exceeded_at == 0 {
return false;
}
let elapsed = now.saturating_sub(self.soft_bytes_exceeded_at);
elapsed >= grace_period
}
pub fn is_inodes_grace_expired(&self, now: u64, grace_period: u64) -> bool {
if self.soft_inodes_exceeded_at == 0 {
return false;
}
let elapsed = now.saturating_sub(self.soft_inodes_exceeded_at);
elapsed >= grace_period
}
}
#[derive(Debug, Clone)]
pub struct Quota {
pub key: QuotaKey,
pub limits: QuotaLimits,
pub usage: QuotaUsage,
}
impl Quota {
pub fn new(key: QuotaKey, limits: QuotaLimits) -> Self {
Self {
key,
limits,
usage: QuotaUsage::zero(),
}
}
pub fn user(uid: u32, limits: QuotaLimits) -> Self {
Self::new(QuotaKey::user(uid), limits)
}
pub fn group(gid: u32, limits: QuotaLimits) -> Self {
Self::new(QuotaKey::group(gid), limits)
}
pub fn bytes_percent(&self) -> u8 {
if self.limits.hard_bytes == 0 {
return 0;
}
let pct = (self.usage.bytes_used as u128 * 100) / self.limits.hard_bytes as u128;
(pct as u8).min(100)
}
pub fn inodes_percent(&self) -> u8 {
if self.limits.hard_inodes == 0 {
return 0;
}
let pct = (self.usage.inodes_used as u128 * 100) / self.limits.hard_inodes as u128;
(pct as u8).min(100)
}
pub fn is_bytes_over_soft(&self) -> bool {
self.limits.soft_bytes > 0 && self.usage.bytes_used > self.limits.soft_bytes
}
pub fn is_bytes_over_hard(&self) -> bool {
self.limits.hard_bytes > 0 && self.usage.bytes_used > self.limits.hard_bytes
}
pub fn is_inodes_over_soft(&self) -> bool {
self.limits.soft_inodes > 0 && self.usage.inodes_used > self.limits.soft_inodes
}
pub fn is_inodes_over_hard(&self) -> bool {
self.limits.hard_inodes > 0 && self.usage.inodes_used > self.limits.hard_inodes
}
pub fn bytes_remaining(&self) -> u64 {
if self.limits.hard_bytes == 0 {
return u64::MAX;
}
self.limits.hard_bytes.saturating_sub(self.usage.bytes_used)
}
pub fn inodes_remaining(&self) -> u64 {
if self.limits.hard_inodes == 0 {
return u64::MAX;
}
self.limits
.hard_inodes
.saturating_sub(self.usage.inodes_used)
}
pub fn update_exceeded_timestamps(&mut self, now: u64) {
if self.is_bytes_over_soft() && self.usage.soft_bytes_exceeded_at == 0 {
self.usage.soft_bytes_exceeded_at = now;
} else if !self.is_bytes_over_soft() {
self.usage.soft_bytes_exceeded_at = 0;
}
if self.is_inodes_over_soft() && self.usage.soft_inodes_exceeded_at == 0 {
self.usage.soft_inodes_exceeded_at = now;
} else if !self.is_inodes_over_soft() {
self.usage.soft_inodes_exceeded_at = 0;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuotaStatus {
Ok,
SoftLimitWarning,
SoftLimitExceeded,
HardLimitExceeded,
}
impl QuotaStatus {
pub fn is_allowed(&self) -> bool {
matches!(self, Self::Ok | Self::SoftLimitWarning)
}
pub fn is_warning(&self) -> bool {
matches!(self, Self::SoftLimitWarning)
}
pub fn is_error(&self) -> bool {
matches!(self, Self::SoftLimitExceeded | Self::HardLimitExceeded)
}
}
impl fmt::Display for QuotaStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ok => write!(f, "OK"),
Self::SoftLimitWarning => write!(f, "SOFT_LIMIT_WARNING"),
Self::SoftLimitExceeded => write!(f, "SOFT_LIMIT_EXCEEDED"),
Self::HardLimitExceeded => write!(f, "HARD_LIMIT_EXCEEDED"),
}
}
}
#[derive(Debug, Clone)]
pub struct QuotaCheckResult {
pub status: QuotaStatus,
pub user_status: Option<QuotaStatus>,
pub group_status: Option<QuotaStatus>,
pub bytes_allowed: u64,
pub inodes_allowed: u64,
pub warnings: Vec<String>,
}
impl QuotaCheckResult {
pub fn ok() -> Self {
Self {
status: QuotaStatus::Ok,
user_status: None,
group_status: None,
bytes_allowed: u64::MAX,
inodes_allowed: u64::MAX,
warnings: Vec::new(),
}
}
pub fn ok_with_limits(bytes_allowed: u64, inodes_allowed: u64) -> Self {
Self {
status: QuotaStatus::Ok,
user_status: Some(QuotaStatus::Ok),
group_status: Some(QuotaStatus::Ok),
bytes_allowed,
inodes_allowed,
warnings: Vec::new(),
}
}
pub fn is_allowed(&self) -> bool {
self.status.is_allowed()
}
pub fn add_warning(&mut self, msg: String) {
self.warnings.push(msg);
}
}
#[derive(Debug, Clone, Default)]
pub struct QuotaReport {
pub dataset: String,
pub total_entries: u32,
pub over_soft: u32,
pub over_hard: u32,
pub grace_expired: u32,
pub entries: Vec<QuotaReportEntry>,
}
#[derive(Debug, Clone)]
pub struct QuotaReportEntry {
pub key: QuotaKey,
pub bytes_used: u64,
pub bytes_soft: u64,
pub bytes_hard: u64,
pub inodes_used: u64,
pub inodes_soft: u64,
pub inodes_hard: u64,
pub status: QuotaStatus,
}
impl QuotaReportEntry {
pub fn from_quota(quota: &Quota, now: u64) -> Self {
let status = if quota.is_bytes_over_hard() || quota.is_inodes_over_hard() {
QuotaStatus::HardLimitExceeded
} else if quota
.usage
.is_bytes_grace_expired(now, quota.limits.grace_period)
|| quota
.usage
.is_inodes_grace_expired(now, quota.limits.grace_period)
{
QuotaStatus::SoftLimitExceeded
} else if quota.is_bytes_over_soft() || quota.is_inodes_over_soft() {
QuotaStatus::SoftLimitWarning
} else {
QuotaStatus::Ok
};
Self {
key: quota.key,
bytes_used: quota.usage.bytes_used,
bytes_soft: quota.limits.soft_bytes,
bytes_hard: quota.limits.hard_bytes,
inodes_used: quota.usage.inodes_used,
inodes_soft: quota.limits.soft_inodes,
inodes_hard: quota.limits.hard_inodes,
status,
}
}
}
#[derive(Debug, Clone)]
pub enum QuotaError {
NotFound(QuotaKey),
AlreadyExists(QuotaKey),
DatasetNotFound(String),
HardLimitExceeded {
key: QuotaKey,
limit_type: &'static str,
current: u64,
limit: u64,
},
GraceExpired {
key: QuotaKey,
limit_type: &'static str,
},
InvalidLimits(String),
ScanError(String),
Internal(String),
}
impl fmt::Display for QuotaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound(key) => write!(f, "quota not found: {}", key),
Self::AlreadyExists(key) => write!(f, "quota already exists: {}", key),
Self::DatasetNotFound(ds) => write!(f, "dataset not found: {}", ds),
Self::HardLimitExceeded {
key,
limit_type,
current,
limit,
} => {
write!(
f,
"{} hard limit exceeded for {}: {} > {}",
limit_type, key, current, limit
)
}
Self::GraceExpired { key, limit_type } => {
write!(f, "{} grace period expired for {}", limit_type, key)
}
Self::InvalidLimits(msg) => write!(f, "invalid limits: {}", msg),
Self::ScanError(msg) => write!(f, "scan error: {}", msg),
Self::Internal(msg) => write!(f, "internal error: {}", msg),
}
}
}
pub type QuotaResult<T> = Result<T, QuotaError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quota_type() {
assert_eq!(QuotaType::User.name(), "user");
assert_eq!(QuotaType::from_u8(1), Some(QuotaType::User));
assert_eq!(QuotaType::from_u8(99), None);
}
#[test]
fn test_quota_key() {
let key = QuotaKey::user(1000);
assert_eq!(key.quota_type, QuotaType::User);
assert_eq!(key.id, 1000);
let key2 = QuotaKey::group(100);
assert_eq!(key2.quota_type, QuotaType::Group);
}
#[test]
fn test_quota_limits() {
let limits = QuotaLimits::bytes(1_000_000, 2_000_000);
assert!(limits.has_byte_limits());
assert!(!limits.has_inode_limits());
assert!(limits.has_any_limits());
let unlimited = QuotaLimits::unlimited();
assert!(!unlimited.has_any_limits());
}
#[test]
fn test_quota_usage() {
let mut usage = QuotaUsage::zero();
usage.add_bytes(1000);
assert_eq!(usage.bytes_used, 1000);
usage.sub_bytes(500);
assert_eq!(usage.bytes_used, 500);
usage.add_inode();
assert_eq!(usage.inodes_used, 1);
}
#[test]
fn test_quota_grace_period() {
let mut usage = QuotaUsage::new(1000, 10);
usage.soft_bytes_exceeded_at = 1000;
assert!(usage.is_bytes_in_grace(2000, 3600));
assert!(!usage.is_bytes_grace_expired(2000, 3600));
assert!(!usage.is_bytes_in_grace(10000, 3600));
assert!(usage.is_bytes_grace_expired(10000, 3600));
}
#[test]
fn test_quota() {
let mut quota = Quota::user(1000, QuotaLimits::bytes(100, 200));
quota.usage.bytes_used = 150;
assert!(quota.is_bytes_over_soft());
assert!(!quota.is_bytes_over_hard());
assert_eq!(quota.bytes_remaining(), 50);
assert_eq!(quota.bytes_percent(), 75);
}
#[test]
fn test_quota_status() {
assert!(QuotaStatus::Ok.is_allowed());
assert!(QuotaStatus::SoftLimitWarning.is_allowed());
assert!(!QuotaStatus::SoftLimitExceeded.is_allowed());
assert!(!QuotaStatus::HardLimitExceeded.is_allowed());
}
#[test]
fn test_quota_check_result() {
let result = QuotaCheckResult::ok();
assert!(result.is_allowed());
assert_eq!(result.bytes_allowed, u64::MAX);
}
#[test]
fn test_update_exceeded_timestamps() {
let mut quota = Quota::user(1000, QuotaLimits::bytes(100, 200));
quota.usage.bytes_used = 150;
quota.update_exceeded_timestamps(1000);
assert_eq!(quota.usage.soft_bytes_exceeded_at, 1000);
quota.update_exceeded_timestamps(2000);
assert_eq!(quota.usage.soft_bytes_exceeded_at, 1000);
quota.usage.bytes_used = 50;
quota.update_exceeded_timestamps(3000);
assert_eq!(quota.usage.soft_bytes_exceeded_at, 0);
}
#[test]
fn test_quota_report_entry() {
let quota = Quota::user(1000, QuotaLimits::bytes(100, 200));
let entry = QuotaReportEntry::from_quota("a, 0);
assert_eq!(entry.status, QuotaStatus::Ok);
}
#[test]
fn test_quota_error_display() {
let err = QuotaError::NotFound(QuotaKey::user(1000));
let msg = err.to_string();
assert!(msg.contains("not found"));
}
}