bpro/
file.rs

1// Rust bitcoin wallet library for professional use.
2//
3// Written in 2022 by
4//     Dr. Maxim Orlovsky <orlovsky@pandoraprime.ch>
5//
6// Copyright (C) 2022 by Pandora Prime SA, Switzerland.
7//
8// This software is distributed without any warranty. You should have received
9// a copy of the AGPL-3.0 License along with this software. If not, see
10// <https://www.gnu.org/licenses/agpl-3.0-standalone.html>.
11
12use std::io::{Seek, Write};
13use std::path::{Path, PathBuf};
14use std::{fs, io};
15
16use strict_encoding::{StrictDecode, StrictEncode};
17
18use crate::{Wallet, WalletSettings};
19
20/// Equals to first 4 bytes of SHA256("mycitadel:wallet:v1")
21/// = a4546a8ef3a51f1faf2dab1517346e9d84b249f7f52d29339b4ee53fe870d14f
22/// Check with `echo -n "mycitadel:wallet:v1" | shasum -a 256`
23const WALLET_DOC_MAGIC: [u8; 4] = [0xa4, 0x54, 0x6a, 0x8e];
24
25pub struct RefWrap<'doc, T>(pub(self) &'doc T)
26where T: StrictEncode;
27
28impl<'doc, T> StrictEncode for RefWrap<'doc, T>
29where T: StrictEncode
30{
31    fn strict_encode<E: Write>(&self, e: E) -> Result<usize, strict_encoding::Error> {
32        self.0.strict_encode(e)
33    }
34}
35
36#[derive(StrictDecode)]
37pub struct DocReader<T>
38where T: StrictDecode
39{
40    pub(self) magic: [u8; 4],
41    pub(self) data: T,
42}
43
44impl<T> DocReader<T>
45where T: StrictDecode
46{
47    pub fn magic_u32(&self) -> u32 { u32::from_be_bytes(self.magic) }
48}
49
50#[derive(StrictEncode)]
51pub struct DocWriter<'doc, T>
52where T: StrictEncode
53{
54    pub(self) magic: [u8; 4],
55    pub(self) data: RefWrap<'doc, T>,
56}
57
58impl<'doc, T> DocWriter<'doc, T>
59where
60    T: StrictEncode,
61    RefWrap<'doc, T>: StrictEncode,
62{
63    pub fn with(magic: [u8; 4], data: &'doc T) -> Self {
64        DocWriter {
65            magic,
66            data: RefWrap(data),
67        }
68    }
69}
70
71#[derive(Debug, Error, From, Display)]
72#[display(inner)]
73pub enum Error {
74    #[from]
75    File(io::Error),
76    #[from]
77    Encoding(strict_encoding::Error),
78    #[display("incorrect file format or future version (expected {expected:#X}, got {actual:#X})")]
79    Magic { expected: u32, actual: u32 },
80    #[display("extra data after the end of file")]
81    DataNotEntirelyConsumed,
82}
83
84pub trait FileDocument
85where Self: From<Self::FallbackDocType>
86{
87    const DOC_MAGIC: [u8; 4];
88
89    const FILE_EXT: &'static str;
90
91    type FallbackDocType: StrictDecode;
92
93    fn magic_u32() -> u32 { u32::from_be_bytes(Self::DOC_MAGIC) }
94
95    fn file_name(base: &str, order_no: usize) -> String {
96        let mut path = PathBuf::from(format!("{}-{}", base, order_no));
97        path.set_extension(Self::FILE_EXT);
98        path.display().to_string()
99    }
100
101    fn read_file(path: impl AsRef<Path>) -> Result<Self, Error>
102    where Self: StrictDecode {
103        let mut file = fs::OpenOptions::new()
104            .create(false)
105            .write(false)
106            .read(true)
107            .open(&path)?;
108        let doc = DocReader::<Self>::strict_decode(&mut file)
109            .map_err(Error::from)
110            .and_then(|doc| {
111                if fs::metadata(path)?.len() != file.stream_position()? {
112                    return Err(Error::DataNotEntirelyConsumed);
113                }
114                Ok(doc)
115            })
116            .or_else(|_| {
117                file.rewind()?;
118                DocReader::<Self::FallbackDocType>::strict_decode(&mut file).map(|r| DocReader::<
119                    Self,
120                > {
121                    magic: r.magic,
122                    data: r.data.into(),
123                })
124            })?;
125        if doc.magic != Self::DOC_MAGIC {
126            return Err(Error::Magic {
127                expected: Self::magic_u32(),
128                actual: doc.magic_u32(),
129            });
130        }
131        Ok(doc.data)
132    }
133
134    fn write_file(&self, path: impl AsRef<Path>) -> Result<usize, Error>
135    where Self: Sized + StrictEncode {
136        let doc = DocWriter::with(Self::DOC_MAGIC, self);
137        let file = fs::File::create(path)?;
138        doc.strict_encode(file).map_err(Error::Encoding)
139    }
140}
141
142impl FileDocument for Wallet {
143    const DOC_MAGIC: [u8; 4] = WALLET_DOC_MAGIC;
144    const FILE_EXT: &'static str = "mcw";
145    type FallbackDocType = WalletSettings;
146}