use super::funejson::Value; use super::time_iso_to_unix::iso_to_unix; const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; // Bytes, not chars: every delimiter is ASCII, so slicing at one is always on a // character boundary, and a multi-byte character never passes a digit check. fn at(b: &[u8], i: usize) -> u8 { if i < b.len() { b[i] } else { 0 } } fn digits(b: &[u8], from: usize, count: usize) -> Option { let mut n = 0i64; for i in from..from + count { let c = at(b, i); if !c.is_ascii_digit() { return None; } n = n * 10 + i64::from(c - b'0'); } Some(n) } fn find(b: &[u8], needle: &[u8], from: usize) -> Option { if from > b.len() { return None; } b[from..].windows(needle.len()).position(|w| w == needle).map(|p| p + from) } fn dash(field: &str) -> Option { if field == "-" { None } else { Some(field.to_string()) } } /// "[10/Oct/2000:13:55:36 -0700]" starting at `i` to Unix seconds, or None. fn parse_time(t: &str, i: usize) -> Option { let b = t.as_bytes(); for (off, ch) in [(0, b'['), (3, b'/'), (7, b'/'), (12, b':'), (15, b':'), (18, b':'), (21, b' '), (27, b']')] { if at(b, i + off) != ch { return None; } } let day = digits(b, i + 1, 2)?; let month = MONTHS.iter().position(|m| m.as_bytes() == &b[i + 4..i + 7])? as i64 + 1; let year = digits(b, i + 8, 4)?; let hour = digits(b, i + 13, 2)?; let minute = digits(b, i + 16, 2)?; let second = digits(b, i + 19, 2)?; let sign = at(b, i + 22); let oh = digits(b, i + 23, 2)?; let om = digits(b, i + 25, 2)?; if year < 1 || hour > 23 || minute > 59 || second > 59 || oh > 23 || om > 59 { return None; } if sign != b'+' && sign != b'-' { return None; } let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; let month_days = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][(month - 1) as usize]; if day < 1 || day > month_days { return None; } // Every field is checked above, so iso_to_unix cannot panic here. Some(iso_to_unix(&format!( "{}-{:02}-{}T{}{}{}:{}", &t[i + 8..i + 12], month, &t[i + 1..i + 3], &t[i + 13..i + 21], sign as char, &t[i + 23..i + 25], &t[i + 25..i + 27] ))) } /// The index of the quote closing a field that opens at `i`, honouring \" escapes. fn closing_quote(b: &[u8], i: usize) -> Option { if at(b, i) != b'"' { return None; } let mut j = i + 1; while j < b.len() { if b[j] == b'\\' { j += 1; } else if b[j] == b'"' { return Some(j); } j += 1; } None } fn is_method(m: &str) -> bool { !m.is_empty() && m.bytes().all(|c| c.is_ascii_uppercase()) } /// One Common or Combined Log Format line, or None when the line is in /// neither format: logs hold junk, and a reader should skip it rather than stop. pub fn parse_access_log(line: &str) -> Option { let t = line.trim_end_matches(|c| c == '\n' || c == '\r'); let b = t.as_bytes(); let host_end = find(b, b" ", 0)?; if host_end < 1 { return None; } let ident_end = find(b, b" ", host_end + 1)?; if ident_end < host_end + 2 { return None; } let user_end = find(b, b" [", ident_end + 1)?; if user_end < ident_end + 2 { return None; } let when = parse_time(t, user_end + 1)?; let mut i = user_end + 29; if at(b, i) != b' ' { return None; } let req_end = closing_quote(b, i + 1)?; let request = &t[i + 2..req_end]; i = req_end + 1; // A three-digit status, then the byte count: both are required. if at(b, i) != b' ' || at(b, i + 4) != b' ' { return None; } let status = digits(b, i + 1, 3)?; if !(100..=599).contains(&status) { return None; } i += 4; let bytes_end = find(b, b" ", i + 1).unwrap_or(b.len()); let bytes_text = &t[i + 1..bytes_end]; let mut size: Option = None; if bytes_text != "-" { if bytes_text.is_empty() || bytes_text.len() > 15 { return None; } size = Some(digits(bytes_text.as_bytes(), 0, bytes_text.len())?); } let mut referer: Option = None; let mut user_agent: Option = None; if bytes_end < b.len() { let ref_end = closing_quote(b, bytes_end + 1)?; if at(b, ref_end + 1) != b' ' { return None; } let ua_end = closing_quote(b, ref_end + 2)?; if ua_end + 1 < b.len() && b[ua_end + 1] != b' ' { return None; } referer = dash(&t[bytes_end + 2..ref_end]); user_agent = dash(&t[ref_end + 3..ua_end]); } let parts: Vec<&str> = request.split(' ').collect(); let (method, path, protocol) = if parts.len() == 3 && is_method(parts[0]) && !parts[1].is_empty() && parts[2].starts_with("HTTP/") { (Some(parts[0].to_string()), Some(parts[1].to_string()), Some(parts[2].to_string())) } else if parts.len() == 2 && is_method(parts[0]) && !parts[1].is_empty() { (Some(parts[0].to_string()), Some(parts[1].to_string()), None) } else { (None, None, None) }; Some(AccessLogEntry { remote_host: t[..host_end].to_string(), ident: dash(&t[host_end + 1..ident_end]), user: dash(&t[ident_end + 1..user_end]), at: when, method, path, protocol, status, bytes: size, referer, user_agent, }) } fn opt_str(v: &Option) -> Value { match v { Some(s) => Value::str(s), None => Value::Null, } } pub fn access_log_entry_to_value(e: &AccessLogEntry) -> Value { Value::obj(vec![ ("remoteHost", Value::str(&e.remote_host)), ("ident", opt_str(&e.ident)), ("user", opt_str(&e.user)), ("at", Value::Int(e.at)), ("method", opt_str(&e.method)), ("path", opt_str(&e.path)), ("protocol", opt_str(&e.protocol)), ("status", Value::Int(e.status)), ("bytes", match e.bytes { Some(n) => Value::Int(n), None => Value::Null, }), ("referer", opt_str(&e.referer)), ("userAgent", opt_str(&e.user_agent)), ]) } pub fn fune_vector(args: &[Value]) -> Value { match parse_access_log(args[0].as_str()) { Some(e) => access_log_entry_to_value(&e), None => Value::Null, } }