hive_asar/
header.rs

1//! Structures that describes asar's header.
2//!
3//! Asar's header is represented using a single root [`Directory`], with tree
4//! structures similar to what the file system looks like.
5
6use serde::de::{Error, Unexpected};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use std::collections::HashMap;
9use std::fmt::{self, Debug, Display, Formatter};
10use std::ops::Deref;
11use tokio::io;
12
13/// Entry of either a file or a directory.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(untagged)]
16pub enum Entry {
17  /// A file.
18  File(FileMetadata),
19
20  /// A directory.
21  Directory(Directory),
22}
23
24impl Entry {
25  pub(crate) fn search_segments(&self, segments: &[&str]) -> Option<&Entry> {
26    match self {
27      _ if segments.is_empty() => Some(self),
28      Self::File(_) => None,
29      Self::Directory(dir) => dir.search_segments(segments),
30    }
31  }
32}
33
34/// Metadata of a file.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct FileMetadata {
37  /// Where the file is located.
38  #[serde(flatten)]
39  pub pos: FilePosition,
40
41  /// The file's size.
42  ///
43  /// According to [official repository], this field should not be larger than
44  /// `9007199254740991`, which is JavaScript's `Number.MAX_SAFE_INTEGER` and
45  /// about 8PB in size.
46  ///
47  /// [official repository]: https://github.com/electron/asar#format
48  pub size: u64,
49
50  /// Whether the file is an executable.
51  #[serde(default)]
52  #[serde(skip_serializing_if = "bool::clone")]
53  pub executable: bool,
54
55  /// Optional integrity information of the file.
56  pub integrity: Option<Integrity>,
57}
58
59impl FileMetadata {
60  pub(crate) fn offset(&self) -> io::Result<u64> {
61    if let FilePosition::Offset(x) = self.pos {
62      Ok(x)
63    } else {
64      Err(io::Error::new(
65        io::ErrorKind::Other,
66        "unpacked file is currently not supported",
67      ))
68    }
69  }
70}
71
72/// Whether the file is stored in the archive or is unpacked.
73#[derive(Debug, Clone, Copy)]
74pub enum FilePosition {
75  /// Offset of the file in the archive, indicates the file is stored in it.
76  Offset(u64),
77
78  /// Indicates the file is stored outside the archive.
79  Unpacked,
80}
81
82#[derive(Serialize, Deserialize)]
83#[serde(untagged)]
84enum Helper<'a> {
85  Offset {
86    #[serde(skip_serializing_if = "Option::is_none")]
87    unpacked: Option<bool>,
88    offset: &'a str,
89  },
90  Unpacked {
91    unpacked: bool,
92  },
93}
94
95impl Serialize for FilePosition {
96  fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
97    let offset_string;
98    let helper = match self {
99      Self::Offset(offset) => {
100        offset_string = offset.to_string();
101        Helper::Offset {
102          unpacked: None,
103          offset: &offset_string,
104        }
105      }
106      Self::Unpacked => Helper::Unpacked { unpacked: true },
107    };
108
109    helper.serialize(ser)
110  }
111}
112
113impl<'de> Deserialize<'de> for FilePosition {
114  fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
115    match Helper::deserialize(de)? {
116      Helper::Offset { unpacked, .. } if matches!(unpacked, Some(true)) => {
117        Err(Error::custom("got both 'unpacked' and 'offset' field"))
118      }
119      Helper::Offset { offset, .. } => offset
120        .parse()
121        .map(Self::Offset)
122        .map_err(|_| Error::invalid_value(Unexpected::Str(offset), &"valid u64 string")),
123      Helper::Unpacked { unpacked: true } => Ok(Self::Unpacked),
124      Helper::Unpacked { unpacked: false } => {
125        Err(Error::invalid_value(Unexpected::Bool(false), &"true"))
126      }
127    }
128  }
129}
130
131/// Integrity information of a file.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct Integrity {
134  /// Hashing algorithm used.
135  pub algorithm: Algorithm,
136
137  /// The hash of the entire file.
138  pub hash: Hash,
139
140  /// Indicates the size of each block of the hashes in `blocks`.
141  #[serde(rename = "blockSize")]
142  pub block_size: u32,
143
144  /// Hashes of blocks.
145  pub blocks: Vec<Hash>,
146}
147
148#[derive(Clone, Serialize, Deserialize)]
149pub struct Hash(#[serde(with = "hex::serde")] pub(crate) Vec<u8>);
150
151impl From<Vec<u8>> for Hash {
152  fn from(x: Vec<u8>) -> Self {
153    Self(x)
154  }
155}
156
157impl From<Hash> for Vec<u8> {
158  fn from(x: Hash) -> Self {
159    x.0
160  }
161}
162
163impl AsRef<[u8]> for Hash {
164  fn as_ref(&self) -> &[u8] {
165    &self.0
166  }
167}
168
169impl Deref for Hash {
170  type Target = [u8];
171
172  fn deref(&self) -> &Self::Target {
173    self.as_ref()
174  }
175}
176
177impl Debug for Hash {
178  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
179    <Self as Display>::fmt(self, f)
180  }
181}
182
183impl Display for Hash {
184  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
185    f.write_str(&hex::encode(&self.0))
186  }
187}
188
189/// Hashing algorithm used to check files' integrity.
190///
191/// Currently only SHA256 is officially supported.
192#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
193pub enum Algorithm {
194  SHA256,
195}
196
197/// A directory, containing files.
198#[derive(Debug, Clone, Default, Serialize, Deserialize)]
199pub struct Directory {
200  pub files: HashMap<Box<str>, Entry>,
201}
202
203impl Directory {
204  pub(crate) fn search_segments(&self, segments: &[&str]) -> Option<&Entry> {
205    (self.files)
206      .get(segments[0])
207      .and_then(|x| x.search_segments(&segments[1..]))
208  }
209}