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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
use std::{
    borrow::Borrow,
    collections::HashMap,
    env,
    fs::File,
    io::prelude::*,
    path::{Path, PathBuf},
    sync::{Arc, RwLock},
};

use bevy::{
    asset::{AssetIo, AssetIoError},
    utils::BoxedFuture,
};
use tar::Archive;

use super::path_info::ArchivePathInfo;
use crate::AssetBundlingOptions;

type ParentDirToPathInfo = HashMap<String, Vec<ArchivePathInfo>>;

#[derive(Default)]
pub struct BundledAssetIo {
    options: AssetBundlingOptions,
    parent_dir_to_path_info: Option<Arc<RwLock<ParentDirToPathInfo>>>,
}

impl From<AssetBundlingOptions> for BundledAssetIo {
    fn from(options: AssetBundlingOptions) -> Self {
        Self {
            options,
            parent_dir_to_path_info: None,
        }
    }
}

impl BundledAssetIo {
    pub fn ensure_loaded(&mut self) -> anyhow::Result<()> {
        if self.parent_dir_to_path_info.is_none() {
            let bundle_path = self.get_bundle_path()?;
            info!("Loading asset bundle: {:?}", bundle_path);
            let file = File::open(bundle_path)?;
            let mut archive = Archive::new(file);
            let mut mappings: ParentDirToPathInfo = HashMap::new();
            let mut n_entries = 0;
            for entry in archive.entries()?.flatten() {
                n_entries += 1;
                let path = entry.path()?;
                let decoded_path = if self.options.encode_file_names {
                    self.options.try_decode_path(path.borrow())?
                    // PathBuf::from(self.options.try_decode_string(path.to_str().unwrap())?)
                } else {
                    path.to_path_buf()
                };
                // let is_dir = path.is_dir();
                let mut parent_dir = decoded_path.clone();
                let parent_dir_str = if parent_dir.pop() {
                    normalize_path(&parent_dir)
                } else {
                    "".into()
                };
                debug!("Loading asset file {:?}, dir:{:?}", path, parent_dir);
                let path_info = ArchivePathInfo::new(decoded_path);
                if let Some(vec) = mappings.get_mut(&parent_dir_str) {
                    vec.push(path_info);
                } else {
                    mappings.insert(parent_dir_str, vec![path_info]);
                }
            }
            info!("{} asset files loaded.", n_entries);
            self.parent_dir_to_path_info = Some(Arc::new(RwLock::new(mappings)));
            Ok(())
        } else {
            Err(anyhow::Error::msg("Entity file is not found"))
        }
    }

    fn get_bundle_path(&self) -> anyhow::Result<PathBuf, AssetIoError> {
        let mut bundle_path = env::current_exe().map_err(AssetIoError::Io)?;
        bundle_path.pop();
        bundle_path.push(self.options.asset_bundle_name.clone());
        Ok(bundle_path)
    }
}

impl AssetIo for BundledAssetIo {
    fn load_path<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<Vec<u8>, AssetIoError>> {
        info!("load_path: {:?}", path);
        Box::pin(async move {
            let bundle_path = self.get_bundle_path()?;
            let file = File::open(bundle_path)?;
            let encoded_entry_path = if self.options.encode_file_names {
                self.options.try_encode_path(path).map_err(map_error)?
            } else {
                PathBuf::from(normalize_path(path))
            };
            let mut archive = Archive::new(file);
            for mut entry in archive.entries()?.flatten() {
                let entry_path = entry.path()?;
                if entry_path.eq(&encoded_entry_path) {
                    let mut vec = Vec::new();
                    entry.read_to_end(&mut vec)?;
                    #[cfg(feature = "encryption")]
                    if let Some(decrypted) = self.options.try_decrypt(&vec).map_err(map_error)? {
                        return Ok(decrypted);
                    }
                    return Ok(vec);
                }
            }
            Err(AssetIoError::NotFound(path.to_path_buf()))
        })
    }

    fn read_directory(
        &self,
        path: &Path,
    ) -> Result<Box<dyn Iterator<Item = PathBuf>>, AssetIoError> {
        info!("[read_directory] {:?}", path);
        if let Some(lock) = self.parent_dir_to_path_info.clone() {
            let mappings = lock.read().unwrap();
            let path_str = normalize_path(path);
            if let Some(entries) = mappings.get(&path_str) {
                #[allow(clippy::needless_collect)]
                let vec: Vec<_> = entries.iter().map(|e| e.path()).collect();
                return Ok(Box::new(vec.into_iter()));
            }
        }
        Err(AssetIoError::NotFound(path.to_path_buf()))
    }

    fn watch_path_for_changes(
        &self,
        _to_watch: &Path,
        _to_reload: Option<PathBuf>,
    ) -> Result<(), AssetIoError> {
        Ok(())
    }

    fn watch_for_changes(&self) -> Result<(), AssetIoError> {
        Ok(())
    }

    fn get_metadata(&self, path: &Path) -> Result<bevy::asset::Metadata, AssetIoError> {
        info!("get_metadata: {:?}", path);
        if let Some(lock) = self.parent_dir_to_path_info.clone() {
            let mappings = lock.read().unwrap();
            let path_str = normalize_path(path);
            if mappings.contains_key(&path_str) {
                Ok(bevy::asset::Metadata::new(bevy::asset::FileType::Directory))
            } else {
                for v in mappings.values() {
                    for info in v {
                        if info.path() == path {
                            return Ok(bevy::asset::Metadata::new(bevy::asset::FileType::File));
                        }
                    }
                }
                Err(AssetIoError::NotFound(path.to_path_buf()))
            }
        } else {
            Err(AssetIoError::NotFound(path.to_path_buf()))
        }
    }
}

fn map_error(err: anyhow::Error) -> AssetIoError {
    AssetIoError::Io(std::io::Error::new(
        std::io::ErrorKind::Other,
        format!("{}", err),
    ))
}

fn normalize_path(path: &Path) -> String {
    path.to_str().unwrap_or("").replace('\\', "/")
}