hive_asar/
archive.rs

1use crate::header::{Directory, Entry, FileMetadata};
2use crate::private::Sealed;
3use crate::{cfg_fs, cfg_integrity, split_path};
4use async_trait::async_trait;
5use pin_project::pin_project;
6use std::io::{Cursor, SeekFrom};
7use std::pin::Pin;
8use std::task::{Context, Poll};
9use tokio::io::{self, AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, Take};
10
11cfg_fs! {
12  use std::path::{Path, PathBuf};
13  use tokio::fs::File as TokioFile;
14}
15
16cfg_integrity! {
17  use sha2::digest::Digest;
18  use sha2::Sha256;
19}
20
21/// Generic asar archive reader.
22///
23/// It supports any reader that implements [`AsyncRead`], [`AsyncSeek`] and
24/// [`Unpin`], and adds more methods if the reader implements [`Send`] or
25/// ([`Local`](LocalDuplicable))[`Duplicable`].
26#[derive(Debug)]
27pub struct Archive<R: AsyncRead + AsyncSeek + Unpin> {
28  pub(crate) offset: u64,
29  pub(crate) header: Directory,
30  pub(crate) reader: R,
31}
32
33/// Checks if a file is in asar format by reading and checking first 16 bytes.
34///
35/// Returns `Some(header_len)` if it is an asar archive, or `None` if it isn't.
36pub async fn check_asar_format(reader: &mut (impl AsyncRead + Unpin)) -> io::Result<Option<u32>> {
37  let [mut four, mut i1, mut i2, mut header_len] = [0; 4];
38  for x in [&mut four, &mut i1, &mut i2, &mut header_len] {
39    *x = reader.read_u32_le().await?;
40  }
41
42  let padding = match header_len % 4 {
43    0 => 0,
44    r => 4 - r,
45  };
46
47  let i1_e = header_len + padding + 8;
48  let i2_e = header_len + padding + 4;
49
50  if four == 4 && i1 == i1_e && i2 == i2_e {
51    Ok(Some(header_len))
52  } else {
53    Ok(None)
54  }
55}
56
57impl<R: AsyncRead + AsyncSeek + Unpin> Archive<R> {
58  /// Parses an asar archive into `Archive`.
59  pub async fn new(mut reader: R) -> io::Result<Self> {
60    let header_len = check_asar_format(&mut reader)
61      .await?
62      .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "file format check failed"))?;
63
64    let mut header_bytes = vec![0; header_len as _];
65    reader.read_exact(&mut header_bytes).await?;
66
67    let header = serde_json::from_slice(&header_bytes).map_err(io::Error::from)?;
68    let offset = match header_len % 4 {
69      0 => header_len + 16,
70      r => header_len + 16 + 4 - r,
71    } as u64;
72
73    Ok(Self {
74      offset,
75      header,
76      reader,
77    })
78  }
79
80  /// Returns a reference to its inner reader.
81  pub fn reader(&self) -> &R {
82    &self.reader
83  }
84
85  /// Returns mutable reference to its inner reader.
86  ///
87  /// It is mostly OK to seek the reader's cursor, since every time accessing
88  /// its file will reset the cursor to the file's position. However, write
89  /// access will compromise [`Archive`]'s functionality. Use with great care!
90  pub fn reader_mut(&mut self) -> &mut R {
91    &mut self.reader
92  }
93
94  /// Drops the inner state and returns the reader.
95  pub fn into_reader(self) -> R {
96    self.reader
97  }
98}
99
100cfg_fs! {
101  impl Archive<DuplicableFile> {
102    /// Opens a file and parses it into [`Archive`].
103    pub async fn new_from_file(path: impl Into<PathBuf>) -> io::Result<Self> {
104      Self::new(DuplicableFile::open(path).await?).await
105    }
106  }
107}
108
109impl<R: AsyncRead + AsyncSeek + Unpin> Archive<R> {
110  /// Returns a file from the archive by taking mutable reference.
111  pub async fn get(&mut self, path: &str) -> io::Result<File<&mut R>> {
112    let entry = self.header.search_segments(&split_path(path));
113    match entry {
114      Some(Entry::File(metadata)) => {
115        (self.reader)
116          .seek(SeekFrom::Start(self.offset + metadata.offset()?))
117          .await?;
118        Ok(File {
119          offset: self.offset,
120          metadata: metadata.clone(),
121          content: (&mut self.reader).take(metadata.size),
122        })
123      }
124      Some(Entry::Directory(_)) => Err(io::Error::from_raw_os_error(libc::EISDIR)),
125      None => Err(io::ErrorKind::NotFound.into()),
126    }
127  }
128
129  /// Returns the entry ("metadata") of specified path.
130  pub fn get_entry(&self, path: &str) -> Option<&Entry> {
131    self.header.search_segments(&split_path(path))
132  }
133}
134
135macro_rules! impl_get_owned {
136  (
137    $(#[$attr:ident $($args:tt)*])*
138    $get_owned:ident,
139    $duplicate:ident $(,)?
140  ) => {
141    impl<R: AsyncRead + AsyncSeek + $duplicate + Unpin> Archive<R> {
142      $(#[$attr $($args)*])*
143      pub async fn $get_owned(&self, path: &str) -> io::Result<File<R>> {
144        let entry = self.header.search_segments(&split_path(path));
145        match entry {
146          Some(Entry::File(metadata)) => {
147            let mut file = self.reader.duplicate().await?;
148            let seek_from = SeekFrom::Start(self.offset + metadata.offset()?);
149            file.seek(seek_from).await?;
150            Ok(File {
151              offset: self.offset,
152              metadata: metadata.clone(),
153              content: file.take(metadata.size),
154            })
155          }
156          Some(_) => Err(io::Error::from_raw_os_error(libc::EISDIR)),
157          None => Err(io::Error::from_raw_os_error(libc::ENOENT)),
158        }
159      }
160    }
161  }
162}
163
164impl_get_owned! {
165  /// Returns a file from the archive by duplicating the inner reader.
166  ///
167  /// Contrary to [`Archive::get`], it allows multiple read access over a single
168  /// archive by creating a new file handle for every file. Useful when building a
169  /// virtual file system like how Electron does.
170  get_owned,
171  Duplicable,
172}
173
174impl_get_owned! {
175  /// Returns a file from the archive by duplicating the inner reader, without `Sync`.
176  ///
177  /// See [`Archive::get_owned`] for more information.
178  get_owned_local,
179  LocalDuplicable,
180}
181
182cfg_fs! {
183  impl<R: AsyncRead + AsyncSeek + Send + Unpin> Archive<R> {
184    /// Extracts the archive to a folder.
185    pub async fn extract(&mut self, path: impl AsRef<Path>) -> io::Result<()> {
186      let path = path.as_ref();
187      for (name, entry) in self.header.files.iter() {
188        crate::extract::extract_entry(&mut self.reader, self.offset, name, entry, path).await?;
189      }
190      Ok(())
191    }
192  }
193
194  impl<R: AsyncRead + AsyncSeek + Unpin> Archive<R> {
195    /// Extracts the archive to a folder.
196    ///
197    /// This method is intended for `R: !Send`. Otherwise, use
198    /// [`Archive::extract`] instead.
199    pub async fn extract_local(&mut self, path: impl AsRef<Path>) -> io::Result<()> {
200      let path = path.as_ref();
201      for (name, entry) in self.header.files.iter() {
202        crate::extract::extract_entry_local(&mut self.reader, self.offset, name, entry, path).await?;
203      }
204      Ok(())
205    }
206  }
207}
208
209/// File from an asar archive.
210#[pin_project]
211pub struct File<R: AsyncRead + AsyncSeek + Unpin> {
212  pub(crate) offset: u64,
213  pub(crate) metadata: FileMetadata,
214  #[pin]
215  pub(crate) content: Take<R>,
216}
217
218impl<R: AsyncRead + AsyncSeek + Unpin> File<R> {
219  /// Gets the metadata of the file.
220  pub fn metadata(&self) -> &FileMetadata {
221    &self.metadata
222  }
223
224  cfg_integrity! {
225    pub async fn check_integrity(&mut self) -> io::Result<bool> {
226      if let Some(integrity) = &self.metadata.integrity {
227        let block_size = integrity.block_size;
228        let mut block = Vec::with_capacity(block_size as _);
229        let mut global_state = Sha256::new();
230        let mut size = 0;
231
232        for block_hash in integrity.blocks.iter() {
233          let read_size = (&mut self.content)
234            .take(block_size as _)
235            .read_to_end(&mut block)
236            .await?;
237          if read_size == 0 || *Sha256::digest(&block) != **block_hash {
238            self.rewind().await?;
239            return Ok(false);
240          }
241          size += read_size;
242          global_state.update(&block);
243          block.clear();
244        }
245        if self.metadata.size != size as u64 || *global_state.finalize() != *integrity.hash {
246          self.rewind().await?;
247          return Ok(false);
248        }
249
250        self.rewind().await?;
251      }
252      Ok(true)
253    }
254  }
255}
256
257impl<R: AsyncRead + AsyncSeek + Unpin> AsyncRead for File<R> {
258  fn poll_read(
259    self: Pin<&mut Self>,
260    cx: &mut Context<'_>,
261    buf: &mut io::ReadBuf<'_>,
262  ) -> Poll<io::Result<()>> {
263    self.project().content.poll_read(cx, buf)
264  }
265}
266
267impl<R: AsyncRead + AsyncSeek + Unpin> AsyncSeek for File<R> {
268  fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> {
269    let current_relative_pos = self.metadata.size - self.content.limit();
270    let offset = self.offset + self.metadata.offset()?;
271    let absolute_pos = match position {
272      SeekFrom::Start(pos) => SeekFrom::Start(offset + self.metadata.size.min(pos)),
273      SeekFrom::Current(pos) if -pos as u64 > current_relative_pos => {
274        return Err(io::Error::from_raw_os_error(libc::EINVAL))
275      }
276      SeekFrom::Current(pos) => {
277        let relative_pos = pos.min((self.metadata.size - current_relative_pos) as i64);
278        SeekFrom::Current(relative_pos)
279      }
280      SeekFrom::End(pos) if pos > 0 => SeekFrom::Start(offset + self.metadata.size),
281      SeekFrom::End(pos) if -pos as u64 > self.metadata.size => {
282        return Err(io::Error::from_raw_os_error(libc::EINVAL))
283      }
284      SeekFrom::End(pos) => SeekFrom::Start(offset + self.metadata.size - (-pos as u64)),
285    };
286    Pin::new(self.content.get_mut()).start_seek(absolute_pos)
287  }
288
289  fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
290    let result = Pin::new(self.content.get_mut()).poll_complete(cx);
291    match result {
292      Poll::Ready(Ok(result)) => {
293        let new_relative_pos = result - self.offset - self.metadata.offset()?;
294        let new_limit = self.metadata.size - new_relative_pos;
295        self.content.set_limit(new_limit);
296        Poll::Ready(Ok(new_relative_pos))
297      }
298      other => other,
299    }
300  }
301}
302
303/// Ability to duplicate asynchronously.
304///
305/// [`Duplicable`] is like `Clone` with `async` and [`io::Result`]. However,
306/// resulting object **must not share common state** with the original one.
307///
308/// This trait is currently for internal use only. You should not rely on
309/// its implementations.
310#[async_trait]
311pub trait Duplicable: Sealed + Sized {
312  async fn duplicate(&self) -> io::Result<Self>;
313}
314
315#[async_trait]
316impl<T: Clone + Sync> Duplicable for Cursor<T> {
317  async fn duplicate(&self) -> io::Result<Self> {
318    Ok(self.clone())
319  }
320}
321
322/// Ability to duplicate asynchronously without `Sync`.
323///
324/// See [`Duplicable`] for more information.
325#[async_trait(?Send)]
326pub trait LocalDuplicable: Sealed + Sized {
327  async fn duplicate(&self) -> io::Result<Self>;
328}
329
330#[async_trait(?Send)]
331impl<T: Clone> LocalDuplicable for Cursor<T> {
332  async fn duplicate(&self) -> io::Result<Self> {
333    Ok(self.clone())
334  }
335}
336
337cfg_fs! {
338  /// [`TokioFile`] with path that implements [`Duplicable`].
339  ///
340  /// A new file handle with different internal state cannot be created from an
341  /// existing one. [`TokioFile::try_clone`] shares its internal cursor,
342  /// and thus cannot be [`Duplicable`]. `TokioFileWithPath`, however, opens a
343  /// new file handle every time [`Duplicable::duplicate`] is called.
344  #[pin_project]
345  pub struct DuplicableFile {
346    #[pin]
347    inner: TokioFile,
348    path: PathBuf,
349  }
350
351  impl DuplicableFile {
352    pub async fn open(path: impl Into<PathBuf>) -> io::Result<Self> {
353      let path = path.into();
354      let inner = TokioFile::open(&path).await?;
355      Ok(Self { inner, path })
356    }
357
358    pub async fn path(&self) -> &Path {
359      &self.path
360    }
361
362    pub fn into_inner(self) -> (TokioFile, PathBuf) {
363      (self.inner, self.path)
364    }
365
366    pub async fn rename(&mut self, new_path: impl Into<PathBuf>) -> io::Result<()> {
367      let new_path = new_path.into();
368      tokio::fs::rename(&self.path, &new_path).await?;
369      self.path = new_path;
370      Ok(())
371    }
372  }
373
374  impl AsyncRead for DuplicableFile {
375    fn poll_read(
376      self: Pin<&mut Self>,
377      cx: &mut Context<'_>,
378      buf: &mut io::ReadBuf<'_>,
379    ) -> Poll<std::io::Result<()>> {
380      self.project().inner.poll_read(cx, buf)
381    }
382  }
383
384  impl AsyncSeek for DuplicableFile {
385    fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> std::io::Result<()> {
386      self.project().inner.start_seek(position)
387    }
388
389    fn poll_complete(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<u64>> {
390      self.project().inner.poll_complete(cx)
391    }
392  }
393
394  #[async_trait]
395  impl Duplicable for DuplicableFile {
396    async fn duplicate(&self) -> io::Result<Self> {
397      Ok(Self {
398        inner: TokioFile::open(&self.path).await?,
399        path: self.path.clone(),
400      })
401    }
402  }
403
404  #[async_trait(?Send)]
405  impl LocalDuplicable for DuplicableFile {
406    async fn duplicate(&self) -> io::Result<Self> {
407      Ok(Self {
408        inner: TokioFile::open(&self.path).await?,
409        path: self.path.clone(),
410      })
411    }
412  }
413}