mcvm 0.25.0

A fast, extensible, and powerful Minecraft launcher
Documentation
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
use crate::io::paths::Paths;
use mcvm_core::net::download;
use mcvm_pkg::repo::{
	get_api_url, get_index_url, PackageFlag, RepoIndex, RepoMetadata, RepoPkgEntry,
};
use mcvm_pkg::PackageContentType;
use mcvm_shared::later::Later;

use anyhow::{bail, Context};
use mcvm_shared::output::{MCVMOutput, MessageContents, MessageLevel};
use mcvm_shared::translate;
use reqwest::Client;

use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use std::fs::File;
use std::io::{BufReader, Cursor};
use std::path::PathBuf;

use super::core::{
	get_all_core_packages, get_core_package_content_type, get_core_package_count, is_core_package,
};
use super::PkgLocation;

/// A remote source for mcvm packages
#[derive(Debug)]
pub struct PkgRepo {
	/// The identifier for the repository
	pub id: String,
	location: PkgRepoLocation,
	index: Later<RepoIndex>,
}

/// Location for a PkgRepo
#[derive(Debug)]
pub enum PkgRepoLocation {
	/// A repository on a remote device
	Remote(String),
	/// A repository on the local filesystem
	Local(PathBuf),
	/// The internal core repository
	Core,
}

impl Display for PkgRepoLocation {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Self::Remote(url) => write!(f, "{url}"),
			Self::Local(path) => write!(f, "{path:?}"),
			Self::Core => write!(f, "internal"),
		}
	}
}

impl PkgRepo {
	/// Create a new PkgRepo
	pub fn new(id: &str, location: PkgRepoLocation) -> Self {
		Self {
			id: id.to_owned(),
			location,
			index: Later::new(),
		}
	}

	/// Create the core repository
	pub fn core() -> Self {
		Self::new("core", PkgRepoLocation::Core)
	}

	/// Create the std repository
	pub fn std() -> Self {
		Self::new(
			"std",
			PkgRepoLocation::Remote("https://mcvm-launcher.github.io/packages/std".into()),
		)
	}

	/// Get the default set of repositories
	pub fn default_repos(enable_core: bool, enable_std: bool) -> Vec<Self> {
		let mut out = Vec::new();
		// We don't want std overriding core
		if enable_core {
			out.push(Self::core());
		}
		if enable_std {
			out.push(Self::std());
		}
		out
	}

	/// The cached path of the index
	pub fn get_path(&self, paths: &Paths) -> PathBuf {
		paths.pkg_index_cache.join(format!("{}.json", &self.id))
	}

	/// Gets the location of the repository
	pub fn get_location(&self) -> &PkgRepoLocation {
		&self.location
	}

	/// Set the index to serialized json text
	fn set_index(&mut self, index: &mut impl std::io::Read) -> anyhow::Result<()> {
		let parsed = simd_json::from_reader(index)?;
		self.index.fill(parsed);
		Ok(())
	}

	/// Update the currently cached index file
	pub async fn sync(&mut self, paths: &Paths, client: &Client) -> anyhow::Result<()> {
		match &self.location {
			PkgRepoLocation::Local(path) => {
				let bytes = tokio::fs::read(path).await?;
				tokio::fs::write(self.get_path(paths), &bytes).await?;
				let mut cursor = Cursor::new(&bytes);
				self.set_index(&mut cursor).context("Failed to set index")?;
			}
			PkgRepoLocation::Remote(url) => {
				let bytes = download::bytes(get_index_url(url), client)
					.await
					.context("Failed to download index")?;
				tokio::fs::write(self.get_path(paths), &bytes)
					.await
					.context("Failed to write index to cached file")?;
				let mut cursor = Cursor::new(&bytes);
				self.set_index(&mut cursor).context("Failed to set index")?;
			}
			PkgRepoLocation::Core => {}
		}

		Ok(())
	}

	/// Make sure that the repository index is downloaded
	pub async fn ensure_index(
		&mut self,
		paths: &Paths,
		client: &Client,
		o: &mut impl MCVMOutput,
	) -> anyhow::Result<()> {
		// The core repository doesn't have an index
		if let PkgRepoLocation::Core = &self.location {
			return Ok(());
		}

		if self.index.is_empty() {
			let path = self.get_path(paths);
			if path.exists() {
				let file = File::open(&path).context("Failed to open cached index")?;
				let mut file = BufReader::new(file);
				match self.set_index(&mut file) {
					Ok(..) => {}
					Err(..) => {
						self.sync(paths, client)
							.await
							.context("Failed to sync index")?;
					}
				};
			} else {
				self.sync(paths, client)
					.await
					.context("Failed to sync index")?;
			}

			self.check_index(o);
		}

		Ok(())
	}

	/// Checks the index. It must be already loaded.
	fn check_index(&self, o: &mut impl MCVMOutput) {
		let repo_version = &self.index.get().metadata.mcvm_version;
		if let Some(repo_version) = repo_version {
			let repo_version = version_compare::Version::from(repo_version);
			let program_version = version_compare::Version::from(crate::VERSION);
			if let (Some(repo_version), Some(program_version)) = (repo_version, program_version) {
				if repo_version > program_version {
					o.display(
						MessageContents::Warning(translate!(
							o,
							RepoVersionWarning,
							"repo" = &self.id
						)),
						MessageLevel::Important,
					);
				}
			}
		}
	}

	/// Ask if the index has a package and return the url and version for that package if it exists
	pub async fn query(
		&mut self,
		id: &str,
		paths: &Paths,
		client: &Client,
		o: &mut impl MCVMOutput,
	) -> anyhow::Result<Option<RepoQueryResult>> {
		// Get from the core
		if let PkgRepoLocation::Core = &self.location {
			if is_core_package(id) {
				Ok(Some(RepoQueryResult {
					location: PkgLocation::Core,
					content_type: get_core_package_content_type(id)
						.expect("Core package exists and should have a content type"),
					flags: HashSet::new(),
				}))
			} else {
				Ok(None)
			}
		} else {
			self.ensure_index(paths, client, o).await?;
			let index = self.index.get();
			if let Some(entry) = index.packages.get(id) {
				let location = get_package_location(entry, &self.location, &self.id)
					.context("Failed to get location of package")?;
				return Ok(Some(RepoQueryResult {
					location,
					content_type: get_content_type(entry).await,
					flags: entry.flags.clone(),
				}));
			}
			Ok(None)
		}
	}

	/// Get all packages from this repo
	pub async fn get_all_packages(
		&mut self,
		paths: &Paths,
		client: &Client,
		o: &mut impl MCVMOutput,
	) -> anyhow::Result<Vec<(String, RepoPkgEntry)>> {
		self.ensure_index(paths, client, o).await?;
		// Get list from core
		if let PkgRepoLocation::Core = &self.location {
			Ok(get_all_core_packages())
		} else {
			let index = self.index.get();
			Ok(index
				.packages
				.iter()
				.map(|(id, entry)| (id.clone(), entry.clone()))
				.collect())
		}
	}

	/// Get the number of packages in the repo
	pub async fn get_package_count(
		&mut self,
		paths: &Paths,
		client: &Client,
		o: &mut impl MCVMOutput,
	) -> anyhow::Result<usize> {
		self.ensure_index(paths, client, o).await?;

		if let PkgRepoLocation::Core = &self.location {
			Ok(get_core_package_count())
		} else {
			Ok(self.index.get().packages.len())
		}
	}

	/// Get the repo's metadata
	pub async fn get_metadata(
		&mut self,
		paths: &Paths,
		client: &Client,
		o: &mut impl MCVMOutput,
	) -> anyhow::Result<Cow<RepoMetadata>> {
		self.ensure_index(paths, client, o).await?;

		if let PkgRepoLocation::Core = &self.location {
			let meta = RepoMetadata {
				name: Some(translate!(o, CoreRepoName)),
				description: Some(translate!(o, CoreRepoDescription)),
				mcvm_version: Some(crate::VERSION.into()),
			};

			Ok(Cow::Owned(meta))
		} else {
			Ok(Cow::Borrowed(&self.index.get().metadata))
		}
	}
}

/// Query a list of repos
pub async fn query_all(
	repos: &mut [PkgRepo],
	id: &str,
	paths: &Paths,
	client: &Client,
	o: &mut impl MCVMOutput,
) -> anyhow::Result<Option<RepoQueryResult>> {
	for repo in repos {
		let query = match repo.query(id, paths, client, o).await {
			Ok(val) => val,
			Err(e) => {
				o.display(
					MessageContents::Error(e.to_string()),
					MessageLevel::Important,
				);
				continue;
			}
		};
		if query.is_some() {
			return Ok(query);
		}
	}
	Ok(None)
}

/// Get all packages from a list of repositories with the normal priority order
pub async fn get_all_packages(
	repos: &mut [PkgRepo],
	paths: &Paths,
	client: &Client,
	o: &mut impl MCVMOutput,
) -> anyhow::Result<HashMap<String, RepoPkgEntry>> {
	let mut out = HashMap::new();
	// Iterate in reverse to make sure that repos at the beginning take precendence
	for repo in repos.iter_mut().rev() {
		let packages = repo
			.get_all_packages(paths, client, o)
			.await
			.with_context(|| format!("Failed to get all packages from repository '{}'", repo.id))?;
		out.extend(packages);
	}

	Ok(out)
}

/// Result from repository querying. This represents an entry
/// for a package that can be accessed
pub struct RepoQueryResult {
	/// The location to copy the package from
	pub location: PkgLocation,
	/// The content type of the package
	pub content_type: PackageContentType,
	/// The flags for the package
	pub flags: HashSet<PackageFlag>,
}

/// Get the content type of a package from the repository
pub async fn get_content_type(entry: &RepoPkgEntry) -> PackageContentType {
	if let Some(content_type) = &entry.content_type {
		*content_type
	} else {
		PackageContentType::Script
	}
}

/// Gets the location of a package from it's repository entry in line with url and path rules
pub fn get_package_location(
	entry: &RepoPkgEntry,
	repo_location: &PkgRepoLocation,
	repo_id: &str,
) -> anyhow::Result<PkgLocation> {
	if let Some(url) = &entry.url {
		Ok(PkgLocation::Remote {
			url: Some(url.clone()),
			repo_id: repo_id.to_string(),
		})
	} else if let Some(path) = &entry.path {
		let path = PathBuf::from(path);
		match &repo_location {
			// Relative paths on remote repositories
			PkgRepoLocation::Remote(url) => {
				if path.is_relative() {
					// Trim the Path
					let path = path.to_string_lossy();
					let trimmed = path.trim_start_matches("./");

					let url = get_api_url(url);
					// Ensure a slash at the end
					let url = if url.ends_with('/') {
						url.clone()
					} else {
						url.clone() + "/"
					};
					Ok(PkgLocation::Remote {
						url: Some(url.to_owned() + trimmed),
						repo_id: repo_id.to_string(),
					})
				} else {
					bail!("Package path on remote repository is non-relative")
				}
			}
			// Local paths
			PkgRepoLocation::Local(repo_path) => {
				let path = if path.is_relative() {
					repo_path.join(path)
				} else {
					path
				};

				Ok(PkgLocation::Local(path))
			}
			PkgRepoLocation::Core => Ok(PkgLocation::Core),
		}
	} else {
		bail!("Neither url nor path entry present in package")
	}
}