1use crate::{CtLog, CtLogConfig, Version, utils::base64::Base64};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use url::Url;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct LogList {
8 version: String,
9 log_list_timestamp: DateTime<Utc>,
10 operators: Vec<Operators>,
11}
12
13impl LogList {
14 pub fn currently_active_logs(&self) -> Vec<CtLog> {
15 self.active_logs(Utc::now())
16 }
17
18 pub fn active_logs(&self, time: DateTime<Utc>) -> Vec<CtLog> {
19 self.logs(
20 |interval| {
24 interval
25 .as_ref()
26 .is_some_and(|interval| interval.end_exclusive > time)
27 },
28 |state| {
31 state.as_ref().is_some_and(|state| {
32 matches!(
33 state,
34 State::Qualified { .. } | State::Usable { .. } | State::Readonly { .. }
35 )
36 })
37 },
38 |log_type| {
40 log_type
41 .as_ref()
42 .is_none_or(|log_type| matches!(log_type, LogType::Prod))
43 },
44 )
45 }
46
47 pub fn all_logs(&self) -> Vec<CtLog> {
48 self.logs(|_| true, |_| true, |_| true)
49 }
50
51 fn logs<TF, SF, TYF>(&self, time_filter: TF, state_filter: SF, type_filter: TYF) -> Vec<CtLog>
52 where
53 TF: Fn(&Option<Interval>) -> bool,
54 SF: Fn(&Option<State>) -> bool,
55 TYF: Fn(&Option<LogType>) -> bool,
56 {
57 self.operators
58 .iter()
59 .flat_map(|op| op.logs.iter().chain(op.tiled_logs.iter()))
60 .filter(|&log| time_filter(&log.temporal_interval))
61 .filter(|&log| state_filter(&log.state))
62 .filter(|&log| type_filter(&log.log_type))
63 .filter_map(|log| {
64 let config = CtLogConfig {
65 description: log.description.clone(),
66 version: Version::V1,
67 url: match &log.url {
68 LogUrl::Log { url } => url.clone(),
69
70 LogUrl::TiledLog { submission_url, .. } => submission_url.clone(),
71 },
72 tile_url: match &log.url {
73 LogUrl::Log { .. } => None,
74 LogUrl::TiledLog { monitoring_url, .. } => Some(monitoring_url.clone()),
75 },
76 key: log.key.clone(),
77 mmd: log.mmd,
78 };
79 let log = CtLog::new(config);
80
81 if log.log_id() == &log.log_id {
82 Some(log)
83 } else {
84 None
85 }
86 })
87 .collect()
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92struct Operators {
93 name: String,
94 email: Vec<String>,
95 logs: Vec<Logs>,
96 tiled_logs: Vec<Logs>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100struct Logs {
101 description: String,
102 log_id: Base64<Vec<u8>>,
103 key: Base64<Vec<u8>>,
104 mmd: u64,
105 dns: Option<String>,
106 state: Option<State>,
107 temporal_interval: Option<Interval>,
108 log_type: Option<LogType>,
109 #[serde(skip_serializing_if = "Vec::is_empty", default)]
110 previous_owners: Vec<PreviousOwner>,
111 #[serde(flatten)]
112 url: LogUrl,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(untagged)]
117enum LogUrl {
118 Log {
119 url: Url,
120 },
121 TiledLog {
122 submission_url: Url,
123 monitoring_url: Url,
124 },
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case")]
129enum State {
130 Pending {
131 timestamp: DateTime<Utc>,
132 },
133 Qualified {
134 timestamp: DateTime<Utc>,
135 },
136 Usable {
137 timestamp: DateTime<Utc>,
138 },
139 Readonly {
140 timestamp: DateTime<Utc>,
141 final_tree_head: FinalTreeHead,
142 },
143 Retired {
144 timestamp: DateTime<Utc>,
145 },
146 Rejected {
147 timestamp: DateTime<Utc>,
148 },
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152struct Interval {
153 start_inclusive: DateTime<Utc>,
154 end_exclusive: DateTime<Utc>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159enum LogType {
160 Prod,
161 Test,
162 MonitoringOnly,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166struct PreviousOwner {
167 name: String,
168 end_time: DateTime<Utc>,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172struct FinalTreeHead {
173 sha256_root_hash: Base64<Vec<u8>>,
174 tree_size: u64,
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use chrono::{NaiveDate, TimeZone};
181
182 const ALL_LOG_LIST: &str = include_str!("../../../testdata/all_logs_list.json");
183
184 #[test]
185 fn parse_log_list() {
186 let time = Utc
187 .from_local_datetime(
188 &NaiveDate::from_ymd_opt(2025, 12, 14)
189 .unwrap()
190 .and_hms_milli_opt(1, 0, 0, 0)
191 .unwrap(),
192 )
193 .unwrap();
194
195 let log_list: LogList = serde_json::from_str(ALL_LOG_LIST).unwrap();
196 let all_logs = log_list.all_logs();
197 assert_eq!(all_logs.len(), 247);
198
199 let active_logs = log_list.active_logs(time);
200 assert_eq!(active_logs.len(), 71);
201 }
202}