summaryrefslogtreecommitdiffhomepage
path: root/mullvad-daemon/src/account_history.rs
blob: 6a6c0e3ddcc879a0e7b8f0ab52ecef8cd42a4a3e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
use mullvad_types::account::AccountNumber;
use regex::Regex;
use std::{path::Path, sync::LazyLock};
use talpid_types::ErrorExt;
use tokio::{
    fs,
    io::{self, AsyncReadExt, AsyncSeekExt, AsyncWriteExt},
};

pub type Result<T> = std::result::Result<T, Error>;

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("Unable to open or read account history file")]
    Read(#[source] io::Error),

    #[error("Failed to serialize account history")]
    Serialize(#[source] serde_json::Error),

    #[error("Unable to write account history file")]
    Write(#[source] io::Error),

    #[error("Write task panicked or was cancelled")]
    WriteCancelled(#[source] tokio::task::JoinError),
}

static ACCOUNT_HISTORY_FILE: &str = "account-history.json";

pub struct AccountHistory {
    file: io::BufWriter<fs::File>,
    number: Option<AccountNumber>,
}

static ACCOUNT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[0-9]+$").unwrap());

impl AccountHistory {
    pub async fn new(
        settings_dir: &Path,
        current_number: Option<AccountNumber>,
    ) -> Result<AccountHistory> {
        let mut options = fs::OpenOptions::new();
        #[cfg(unix)]
        {
            options.mode(0o600);
        }
        #[cfg(windows)]
        {
            // a share mode of zero ensures exclusive access to the file to *this* process
            options.share_mode(0);
        }

        let path = settings_dir.join(ACCOUNT_HISTORY_FILE);
        log::info!("Opening account history file in {}", path.display());
        let mut reader = options
            .write(true)
            .create(true)
            .read(true)
            .open(path)
            .await
            .map(io::BufReader::new)
            .map_err(Error::Read)?;

        let mut buffer = String::new();
        let (number, should_save): (Option<AccountNumber>, bool) =
            match reader.read_to_string(&mut buffer).await {
                Ok(_) if ACCOUNT_REGEX.is_match(&buffer) => (Some(buffer), false),
                Ok(0) => (current_number, true),
                Ok(_) | Err(_) => {
                    log::warn!("Failed to parse account history");
                    (current_number, true)
                }
            };

        let file = io::BufWriter::new(reader.into_inner());
        let mut history = AccountHistory { file, number };
        if should_save && let Err(error) = history.save_to_disk().await {
            log::error!(
                "{}",
                error.display_chain_with_msg("Failed to save account history after opening it")
            );
        }
        Ok(history)
    }

    /// Gets the account number in the history
    pub fn get(&self) -> Option<AccountNumber> {
        self.number.clone()
    }

    /// Replace the account number in the history
    pub async fn set(&mut self, new_entry: AccountNumber) -> Result<()> {
        self.number = Some(new_entry);
        self.save_to_disk().await
    }

    /// Remove account history
    pub async fn clear(&mut self) -> Result<()> {
        self.number = None;
        self.save_to_disk().await
    }

    async fn save_to_disk(&mut self) -> Result<()> {
        self.file.get_mut().set_len(0).await.map_err(Error::Write)?;
        self.file
            .seek(io::SeekFrom::Start(0))
            .await
            .map_err(Error::Write)?;
        if let Some(ref number) = self.number {
            self.file
                .write_all(number.as_bytes())
                .await
                .map_err(Error::Write)?;
        }
        self.file.flush().await.map_err(Error::Write)?;
        self.file.get_mut().sync_all().await.map_err(Error::Write)
    }
}