Skip to content

Commit

Permalink
MonitorSchedule constructor the validates crontab syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
szokeasaurusrex committed Nov 8, 2023
1 parent 80e5b13 commit 2049893
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 0 deletions.
79 changes: 79 additions & 0 deletions sentry-types/src/crontab_validator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use std::collections::HashSet;

#[derive(PartialEq, Eq, Hash)]
enum CronToken<'a> {
Numeric(u64),
Alphabetic(&'a str),
}

const MONTHS: &[&str] = &[
"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
];

const DAYS: &[&str] = &["sun", "mon", "tue", "wed", "thu", "fri", "sat"];

fn value_is_allowed(value: &str, allowed_values: &HashSet<CronToken>) -> bool {
match value.parse::<u64>() {
Ok(numeric_value) => allowed_values.contains(&CronToken::Numeric(numeric_value)),
Err(_) => allowed_values.contains(&CronToken::Alphabetic(&value.to_lowercase())),
}
}

fn validate_range(range: &str, allowed_values: &HashSet<CronToken>) -> bool {
range == "*"
|| range // TODO: Validate that the last range bound is after the previous one.
.splitn(2, "-")
.all(|bound| value_is_allowed(bound, allowed_values))
}

/// A valid step is None or Some positive value
fn validate_step(step: &Option<&str>) -> bool {
match *step {
Some(value) => match value.parse::<u64>() {
Ok(value) => value > 0,
Err(_) => false,
},
None => true,
}
}

fn validate_steprange(steprange: &str, allowed_values: &HashSet<CronToken>) -> bool {
let mut steprange_split = steprange.splitn(2, "/");
let range = match steprange_split.next() {
Some(range) => range,
None => {
return false;
}
};
let range_is_valid = validate_range(range, allowed_values);
let step = steprange_split.next();

range_is_valid && validate_step(&step)
}

fn validate_segment(segment: &str, allowed_values: &HashSet<CronToken>) -> bool {
segment
.split(",")
.all(|steprange| validate_steprange(steprange, &allowed_values))
}

pub fn validate(crontab: &str) -> bool {
let allowed_values = vec![
(0..60).map(CronToken::Numeric).collect(),
(0..24).map(CronToken::Numeric).collect(),
(1..32).map(CronToken::Numeric).collect(),
(1..13)
.map(CronToken::Numeric)
.chain(MONTHS.iter().map(|&month| CronToken::Alphabetic(month)))
.collect(),
(0..8)
.map(CronToken::Numeric)
.chain(DAYS.iter().map(|&day| CronToken::Alphabetic(day)))
.collect(),
];

crontab
.split_whitespace()
.zip(allowed_values)
.all(|(segment, allowed_values)| validate_segment(segment, &allowed_values))
}
1 change: 1 addition & 0 deletions sentry-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
mod macros;

mod auth;
mod crontab_validator;
mod dsn;
mod project_id;
pub mod protocol;
Expand Down
17 changes: 17 additions & 0 deletions sentry-types/src/protocol/monitor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use serde::{Deserialize, Serialize, Serializer};
use uuid::Uuid;

use crate::crontab_validator;

/// Represents the status of the monitor check-in
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -39,6 +41,21 @@ pub enum MonitorSchedule {
},
}

impl MonitorSchedule {
/// Attempts to create a MonitorSchedule from a provided crontab_str. If the crontab_str is a
/// valid crontab schedule, we return the MonitorSchedule wrapped in a Some variant. Otherwise,
/// we return None.
pub fn from_crontab(crontab_str: &str) -> Option<Self> {
if crontab_validator::validate(crontab_str) {
Some(Self::Crontab {
value: String::from(crontab_str),
})
} else {
None
}
}
}

/// The unit for the interval schedule type
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
Expand Down

0 comments on commit 2049893

Please sign in to comment.