Skip to main content

cloudiful_redactor/session/
state.rs

1use std::collections::{HashMap, HashSet};
2
3use anyhow::{Result, anyhow};
4use serde::{Deserialize, Deserializer, Serialize};
5
6use crate::replace::parse_token;
7use crate::{RedactionSession, RestorationEntry, RestorePermit, RestoreResult};
8
9use super::permit::{PERMIT_VERSION, create_restore_permit};
10use super::{RestoreContext, StreamingRestoreContext};
11
12const SESSION_VERSION: u32 = 2;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
15pub struct RestoreState {
16    session: RedactionSession,
17    permits: Vec<RestorePermit>,
18}
19
20impl RestoreState {
21    pub fn new(session: RedactionSession) -> Result<Self> {
22        validate_session(&session)?;
23        let permits = if session.issued_tokens.is_empty() {
24            Vec::new()
25        } else {
26            vec![create_restore_permit(&session)]
27        };
28        Self::from_parts(session, permits)
29    }
30
31    pub fn from_parts(session: RedactionSession, permits: Vec<RestorePermit>) -> Result<Self> {
32        validate_session(&session)?;
33        let permits = normalize_permits(&session, permits)?;
34        Ok(Self { session, permits })
35    }
36
37    pub fn advance(&self, session: RedactionSession) -> Result<Self> {
38        validate_session(&session)?;
39        validate_continuity(&self.session, &session)?;
40
41        let mut permits = self.permits.clone();
42        if !session.issued_tokens.is_empty() {
43            permits.push(create_restore_permit(&session));
44        }
45        Self::from_parts(session, permits)
46    }
47
48    pub fn session(&self) -> &RedactionSession {
49        &self.session
50    }
51
52    pub fn permits(&self) -> &[RestorePermit] {
53        &self.permits
54    }
55
56    pub fn restore_context(&self) -> Result<RestoreContext<'_>> {
57        RestoreContext::with_permits(&self.session, &self.permits)
58    }
59
60    pub fn streaming_restore_context(&self) -> Result<StreamingRestoreContext<'_>> {
61        StreamingRestoreContext::with_permits(&self.session, &self.permits)
62    }
63
64    pub fn restore_text(&self, text: &str) -> Result<RestoreResult> {
65        Ok(self.restore_context()?.restore_text(text))
66    }
67
68    pub fn has_authorized_tokens(&self) -> bool {
69        self.permits
70            .iter()
71            .any(|permit| !permit.issued_tokens.is_empty())
72    }
73}
74
75impl<'de> Deserialize<'de> for RestoreState {
76    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
77    where
78        D: Deserializer<'de>,
79    {
80        #[derive(Deserialize)]
81        struct Parts {
82            session: RedactionSession,
83            permits: Vec<RestorePermit>,
84        }
85
86        let parts = Parts::deserialize(deserializer)?;
87        Self::from_parts(parts.session, parts.permits).map_err(serde::de::Error::custom)
88    }
89}
90
91fn validate_session(session: &RedactionSession) -> Result<()> {
92    if session.version != SESSION_VERSION {
93        return Err(anyhow!(
94            "unsupported redaction session version {}",
95            session.version
96        ));
97    }
98    if session.session_id.is_empty() {
99        return Err(anyhow!("redaction session ID must not be empty"));
100    }
101    if session.scope_id.is_empty() {
102        return Err(anyhow!("redaction session scope must not be empty"));
103    }
104
105    let mut entries = HashSet::new();
106    for entry in &session.entries {
107        if !entries.insert(entry.token.as_str()) {
108            return Err(anyhow!("duplicate session entry token `{}`", entry.token));
109        }
110        validate_entry(session, entry)?;
111    }
112
113    let mut issued = HashSet::new();
114    for token in &session.issued_tokens {
115        if !issued.insert(token.as_str()) {
116            return Err(anyhow!("duplicate issued token `{token}`"));
117        }
118        if !entries.contains(token.as_str()) {
119            return Err(anyhow!(
120                "issued token `{token}` is missing from session entries"
121            ));
122        }
123    }
124    Ok(())
125}
126
127fn validate_entry(session: &RedactionSession, entry: &RestorationEntry) -> Result<()> {
128    let parsed = parse_token(&entry.token)
129        .map_err(|error| anyhow!("invalid session entry token `{}`: {error}", entry.token))?;
130    if parsed.scope_id != session.scope_id {
131        return Err(anyhow!("session entry token scope does not match session"));
132    }
133    if parsed.kind != entry.kind {
134        return Err(anyhow!(
135            "session entry token kind does not match entry kind"
136        ));
137    }
138    Ok(())
139}
140
141fn normalize_permits(
142    session: &RedactionSession,
143    permits: Vec<RestorePermit>,
144) -> Result<Vec<RestorePermit>> {
145    let known = session
146        .entries
147        .iter()
148        .map(|entry| entry.token.as_str())
149        .collect::<HashSet<_>>();
150    let mut permit_ids = HashSet::new();
151    let mut authorized = HashSet::new();
152    let mut normalized = Vec::new();
153
154    for mut permit in permits {
155        validate_permit(session, &permit, &known, &mut permit_ids)?;
156        permit
157            .issued_tokens
158            .retain(|token| authorized.insert(token.clone()));
159        if !permit.issued_tokens.is_empty() {
160            normalized.push(permit);
161        }
162    }
163    Ok(normalized)
164}
165
166fn validate_permit(
167    session: &RedactionSession,
168    permit: &RestorePermit,
169    known: &HashSet<&str>,
170    permit_ids: &mut HashSet<String>,
171) -> Result<()> {
172    if permit.version != PERMIT_VERSION {
173        return Err(anyhow!(
174            "unsupported restore permit version {}",
175            permit.version
176        ));
177    }
178    if permit.permit_id.is_empty() {
179        return Err(anyhow!("restore permit ID must not be empty"));
180    }
181    if !permit_ids.insert(permit.permit_id.clone()) {
182        return Err(anyhow!(
183            "duplicate restore permit ID `{}`",
184            permit.permit_id
185        ));
186    }
187    if permit.scope_id != session.scope_id || permit.external_id != session.external_id {
188        return Err(anyhow!("restore permit does not match session context"));
189    }
190    for token in &permit.issued_tokens {
191        if !known.contains(token.as_str()) {
192            return Err(anyhow!("restore permit authorizes unknown token `{token}`"));
193        }
194    }
195    Ok(())
196}
197
198fn validate_continuity(previous: &RedactionSession, next: &RedactionSession) -> Result<()> {
199    if previous.version != next.version {
200        return Err(anyhow!("redaction session version changed"));
201    }
202    if previous.scope_id != next.scope_id {
203        return Err(anyhow!("redaction session scope changed"));
204    }
205    if previous.external_id != next.external_id {
206        return Err(anyhow!("redaction session external ID changed"));
207    }
208
209    let next_entries = next
210        .entries
211        .iter()
212        .map(|entry| (entry.token.as_str(), entry))
213        .collect::<HashMap<_, _>>();
214    for old in &previous.entries {
215        let Some(new) = next_entries.get(old.token.as_str()) else {
216            return Err(anyhow!("new session removed entry `{}`", old.token));
217        };
218        if old.original != new.original
219            || old.kind != new.kind
220            || old.replacement_hint != new.replacement_hint
221        {
222            return Err(anyhow!("new session changed entry mapping `{}`", old.token));
223        }
224        if new.occurrences < old.occurrences {
225            return Err(anyhow!(
226                "new session reduced occurrences for `{}`",
227                old.token
228            ));
229        }
230    }
231    Ok(())
232}