fix(core): launch --dry-run 不再落盘(natives 只预览,零建目录/零写文件)
`ull launch --dry-run` 在「环境准备」段走的是真解压:即使只打印命令行,也会在 `versions/<id>/natives/` 下建目录、解 dll、写戳记,把别人的版本树改脏(父目录 mtime 也变)。失败路径(缺 jar 报 missing_components)照样留下这些副作用。 - natives 新增 `preview()`:与 `extract()` 共用条目筛选与戳记口径,但零落盘 (无 create_dir_all / clear_natives / write_stamp),只清点「将写下几个原生库」; jar 打不开照样报错,预览不藏坏包。 - `NativesReport` 增 `planned` / `dir_exists`:预览报告自报家门,人读行与 JSON 说清「未落盘」「目录尚不存在」。 - `Launcher::dry_run` 改走 `preview_environment`,`spawn` 仍走 `prepare_environment`。 - 报告类型拆到 `natives/report.rs`(mod.rs 曾破 400 行红线)。 - 回归测试(natives_dry_run_cases.rs):有 jar / 无 jar / 空 jar / 已解压过四条 零落盘判据,用「节点类型 + 纳秒 mtime」快照逐字比较版本树,并带一条判据自检 (瞬时建删目录必须被快照看见,防假绿)。 - help:`--dry-run` 的承诺补成「不写盘、不解压 natives、不建目录」。 证据(外部版本树 versions/1.16.5 + 真 natives jar): 修复前 dry-run 后 versions/1.16.5 mtime 1794342830.6861426 → 1794342832.4544542, 新增 natives/ + lwjgl.dll + OpenAL.dll + .ull-natives.json;修复后同一场景快照逐字 不变(mtime 1794343730.6618167),JSON natives.planned=true / dir_exists=false。
This commit is contained in:
1 parent
774a547aa0
commit
1fa30c4885
11 files changed
+583
-239
No files matched your search
@@ -7,6 +7,7 @@ mod exit_cases;
|
||||
mod graphics_backend_cases;
|
||||
mod natives_args_cases;
|
||||
mod natives_cases;
|
||||
mod natives_dry_run_cases;
|
||||
mod pipeline_cases;
|
||||
mod prelude;
|
||||
mod process_smoke;
|
||||
|
||||
@@ -2,44 +2,12 @@
|
||||
//!
|
||||
//! 假 jar 用 `zip` 的 Stored 模式现造(不依赖真下载),真机验收另有人工步骤。
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use zip::write::SimpleFileOptions;
|
||||
use zip::{CompressionMethod, ZipWriter};
|
||||
use std::path::Path;
|
||||
|
||||
use unlucky_core::launch::natives::{self, NativeJar, NativesEra, NativesPlan, STAMP_FILE};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// 造一个 natives jar:`entries` 是(条目名, 内容)。
|
||||
fn make_jar(path: &Path, entries: &[(&str, &[u8])]) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("建 jar 目录");
|
||||
}
|
||||
let file = File::create(path).expect("建 jar");
|
||||
let mut writer = ZipWriter::new(file);
|
||||
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
for (name, bytes) in entries {
|
||||
writer.start_file(*name, options).expect("写条目");
|
||||
writer.write_all(bytes).expect("写条目内容");
|
||||
}
|
||||
writer.finish().expect("收尾 jar");
|
||||
}
|
||||
|
||||
/// 一份 1.16.5 风格的 natives jar 的条目集(原生库 + 该被排除的噪音)。
|
||||
fn lwjgl_jar_entries() -> Vec<(&'static str, &'static [u8])> {
|
||||
vec![
|
||||
("lwjgl.dll", b"DLL-BYTES".as_slice()),
|
||||
("OpenAL.dll", b"OPENAL-BYTES".as_slice()),
|
||||
("META-INF/MANIFEST.MF", b"manifest".as_slice()),
|
||||
("META-INF/LWJGL.SF", b"signature".as_slice()),
|
||||
("lwjgl.dll.sha1", b"deadbeef".as_slice()),
|
||||
("README.txt", b"noise".as_slice()),
|
||||
]
|
||||
}
|
||||
|
||||
fn plan(jars: Vec<NativeJar>) -> NativesPlan {
|
||||
NativesPlan::new(jars)
|
||||
}
|
||||
@@ -208,74 +176,3 @@ fn natives_extracted_event_carries_report_fields() {
|
||||
.expect("dir 是字符串")
|
||||
.ends_with("natives"));
|
||||
}
|
||||
|
||||
/// `--dry-run` 也做环境准备:第二次跑要看到 up_to_date(真机验收的判定依据)。
|
||||
#[test]
|
||||
fn dry_run_prepares_natives_and_is_idempotent() {
|
||||
let root = temp_dir("natives-dry-run");
|
||||
let jar = root.join("natives-windows.jar");
|
||||
make_jar(&jar, &[("lwjgl.dll", b"DLL".as_slice())]);
|
||||
let natives_dir: PathBuf = root.join("versions/1.16.5/natives");
|
||||
|
||||
let (mut request, _) = legacy_forge_request();
|
||||
request.dirs.natives_dir = natives_dir.clone();
|
||||
request.natives = NativesPlan::new(vec![NativeJar::new(&jar, "lwjgl")]);
|
||||
request.manifest.legacy_arguments = Some("--username ${auth_player_name}".to_string());
|
||||
|
||||
let sessions = temp_sessions_root("natives-dry-run-sessions");
|
||||
let launcher = Launcher::new(LauncherConfig::new(&sessions));
|
||||
|
||||
let first = launcher.dry_run(&request).expect("首次 dry-run");
|
||||
let report = first.natives.expect("应有 natives 报告");
|
||||
assert_eq!(report.files, 1, "{report:?}");
|
||||
assert!(!report.up_to_date);
|
||||
assert!(natives_dir.join("lwjgl.dll").is_file());
|
||||
|
||||
let second = launcher.dry_run(&request).expect("二次 dry-run");
|
||||
let report = second.natives.expect("应有 natives 报告");
|
||||
assert!(report.up_to_date, "第二次应命中戳记:{report:?}");
|
||||
assert_eq!(report.skipped, 1);
|
||||
}
|
||||
|
||||
/// 缺 natives jar:适配层当成组件缺失(组装前显式失败),而不是等到 UnsatisfiedLinkError。
|
||||
#[test]
|
||||
fn missing_native_jar_becomes_missing_component() {
|
||||
let (mut request, ctx) = legacy_forge_request();
|
||||
request.natives = NativesPlan::new(vec![NativeJar::new(
|
||||
"C:/mc/libraries/missing-natives-windows.jar",
|
||||
"org.lwjgl:lwjgl:3.2.2:natives-windows",
|
||||
)]);
|
||||
// 直接验证适配层:给一个不存在的 natives jar,严格模式应产出 MissingNativeJar 告警。
|
||||
let libraries: Vec<LibraryInput> = request
|
||||
.classpath
|
||||
.iter()
|
||||
.map(|path| LibraryInput::new(path.clone(), "x"))
|
||||
.collect();
|
||||
let natives = request.natives.jars.clone();
|
||||
let snapshot = SnapshotInput {
|
||||
version_id: "1.16.5",
|
||||
main_class: Some("net.minecraft.client.main.Main"),
|
||||
libraries: &libraries,
|
||||
natives: &natives,
|
||||
..SnapshotInput::minimal("1.16.5", "net.minecraft.client.main.Main")
|
||||
};
|
||||
let options = AdapterOptions {
|
||||
java: JavaSelection::new("C:/java/bin/java.exe", 17, 64),
|
||||
..AdapterOptions::strict()
|
||||
};
|
||||
let output = build_request(&snapshot, options);
|
||||
assert!(
|
||||
output
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|warning| matches!(warning, AdapterWarning::MissingNativeJar { .. })),
|
||||
"应有 MissingNativeJar 告警:{:?}",
|
||||
output.warnings
|
||||
);
|
||||
assert_eq!(output.request.natives.era, NativesEra::Legacy);
|
||||
// 组装按既有语义直接失败(missing_components 非空)。
|
||||
assert!(matches!(
|
||||
assemble(&output.request, &ctx).err(),
|
||||
Some(LaunchError::Assembly(AssemblyError::MissingComponents(_)))
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
//! `--dry-run` 的零落盘契约(M231 / DRYRUNPURE):预览只读版本树,不建目录、不改 mtime。
|
||||
//!
|
||||
//! 判据是「版本树的节点类型 + mtime 快照逐字相等」而不是「natives 目录不存在」:
|
||||
//! 后者抓不住 `create_dir_all` 之后立刻删掉的情形(父目录 mtime 已经变了),
|
||||
//! 也抓不住「目录本来就在、但被重写/清空」的情形。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use unlucky_core::launch::natives::{self, NativeJar, NativesPlan};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// 版本树的「节点类型 + mtime(纳秒)+ 相对路径」快照,用来逐字比较 dry-run 前后。
|
||||
///
|
||||
/// 抓得住:新增/删除任何节点(含 `natives/` 目录本身 —— 根节点的 mtime 就是父目录 mtime)、
|
||||
/// 任何已有节点被重写或 mtime 被更新。目录节点的 mtime 会因「建子目录」而变,所以
|
||||
/// 「建了又删」这种瞬时写入也会被记到根节点上(只要快照跨过了那次操作)。
|
||||
/// 抓不住:内容变了但 mtime 被回填成原值(本仓没有这种写法);时间戳分辨率粗于操作间隔的
|
||||
/// 文件系统(FAT/exFAT 的 2s 粒度)上「建了又删」可能落进同一格 —— 那种情况只剩
|
||||
/// 「`natives` 目录不存在」这条判据兜底,瞬时的建-删会漏。
|
||||
///
|
||||
/// 时间戳口径:NTFS / ext4 的 mtime 精度是 100ns 级,快照取纳秒整数比较,所以同一秒内的
|
||||
/// 多次操作(建目录 → 删目录)照样分辨得出来,不需要 `sleep` 把两次操作推到不同秒。
|
||||
/// 下面 `tree_snapshot_notices_a_transient_directory` 就是这条判据的自检(防假绿)。
|
||||
fn tree_snapshot(root: &Path) -> Vec<String> {
|
||||
fn line(meta: &std::fs::Metadata, relative: &str) -> String {
|
||||
let nanos = meta
|
||||
.modified()
|
||||
.expect("mtime")
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("epoch 之后")
|
||||
.as_nanos();
|
||||
let kind = if meta.is_dir() { "d" } else { "f" };
|
||||
format!("{kind} {nanos} {relative}")
|
||||
}
|
||||
fn walk(dir: &Path, prefix: &str, out: &mut Vec<String>) {
|
||||
let mut entries: Vec<PathBuf> = std::fs::read_dir(dir)
|
||||
.expect("读目录")
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.collect();
|
||||
entries.sort();
|
||||
for path in entries {
|
||||
let meta = std::fs::symlink_metadata(&path).expect("元数据");
|
||||
let name = path
|
||||
.file_name()
|
||||
.expect("文件名")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let relative = format!("{prefix}{name}");
|
||||
out.push(line(&meta, &relative));
|
||||
if meta.is_dir() {
|
||||
walk(&path, &format!("{relative}/"), out);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = vec![line(
|
||||
&std::fs::symlink_metadata(root).expect("根元数据"),
|
||||
"<root>",
|
||||
)];
|
||||
walk(root, "", &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// `--dry-run` 零落盘(有 jar):只报「将解压几个」,版本树里一个节点都不新增、mtime 不动。
|
||||
#[test]
|
||||
fn dry_run_previews_natives_without_touching_disk() {
|
||||
let root = temp_dir("natives-dry-run-pure");
|
||||
let jar = root.join("libraries/lwjgl-natives-windows.jar");
|
||||
make_jar(&jar, &lwjgl_jar_entries());
|
||||
let version_dir = root.join("versions/1.16.5");
|
||||
std::fs::create_dir_all(&version_dir).expect("建版本目录");
|
||||
std::fs::write(version_dir.join("1.16.5.json"), b"{}").expect("放清单占位");
|
||||
let natives_dir = version_dir.join("natives");
|
||||
let before = tree_snapshot(&version_dir);
|
||||
|
||||
let (mut request, _) = legacy_forge_request();
|
||||
request.dirs.natives_dir = natives_dir.clone();
|
||||
request.natives = NativesPlan::new(vec![NativeJar::new(&jar, "lwjgl")]);
|
||||
request.manifest.legacy_arguments = Some("--username ${auth_player_name}".to_string());
|
||||
let sessions = temp_sessions_root("natives-dry-run-pure-sessions");
|
||||
let launcher = Launcher::new(LauncherConfig::new(&sessions));
|
||||
|
||||
let dry = launcher.dry_run(&request).expect("dry-run 应成功");
|
||||
assert_eq!(
|
||||
before,
|
||||
tree_snapshot(&version_dir),
|
||||
"dry-run 不许在版本树里加节点 / 改 mtime"
|
||||
);
|
||||
let report = dry.natives.expect("应有 natives 预览");
|
||||
assert!(report.planned, "预览报告要自报家门:{report:?}");
|
||||
assert!(!report.dir_exists, "natives 目录原本不在盘上:{report:?}");
|
||||
assert_eq!(report.files, 2, "要算出「将写下」的数量:{report:?}");
|
||||
assert_eq!(report.ignored, 4, "忽略口径要与真解压一致:{report:?}");
|
||||
assert!(!report.up_to_date, "{report:?}");
|
||||
assert!(
|
||||
report.summary().contains("未落盘"),
|
||||
"人读行得说清没写盘:{}",
|
||||
report.summary()
|
||||
);
|
||||
|
||||
assert!(!natives_dir.exists(), "natives 目录不该被建出来");
|
||||
}
|
||||
|
||||
/// `--dry-run` 零落盘(无 jar,1.19+ 常态):不建 natives 目录,人读行说清「尚不存在」。
|
||||
#[test]
|
||||
fn dry_run_with_no_natives_jar_creates_no_directory() {
|
||||
let root = temp_dir("natives-dry-run-nojar");
|
||||
let version_dir = root.join("versions/1.20.1");
|
||||
std::fs::create_dir_all(&version_dir).expect("建版本目录");
|
||||
let natives_dir = version_dir.join("natives");
|
||||
let before = tree_snapshot(&version_dir);
|
||||
|
||||
let (mut request, _) = legacy_forge_request();
|
||||
request.dirs.natives_dir = natives_dir.clone();
|
||||
request.natives = NativesPlan::default();
|
||||
request.manifest.legacy_arguments = Some("--username ${auth_player_name}".to_string());
|
||||
let sessions = temp_sessions_root("natives-dry-run-nojar-sessions");
|
||||
let launcher = Launcher::new(LauncherConfig::new(&sessions));
|
||||
|
||||
let dry = launcher.dry_run(&request).expect("dry-run 应成功");
|
||||
assert_eq!(before, tree_snapshot(&version_dir), "无 jar 也不许建目录");
|
||||
let report = dry.natives.expect("应有 natives 预览");
|
||||
assert_eq!(report.jars, 0, "{report:?}");
|
||||
assert_eq!(report.files, 0, "{report:?}");
|
||||
assert!(!report.dir_exists, "盘上没有这个目录:{report:?}");
|
||||
assert!(report.up_to_date, "无 jar = 无需解压:{report:?}");
|
||||
assert!(
|
||||
report.summary().contains("尚不存在"),
|
||||
"{}",
|
||||
report.summary()
|
||||
);
|
||||
|
||||
assert!(!natives_dir.exists(), "natives 目录不该被建出来");
|
||||
}
|
||||
|
||||
/// `--dry-run` 零落盘(空 jar:只有 `META-INF/` 与噪音):数得出 0 个原生库,仍不建目录。
|
||||
#[test]
|
||||
fn dry_run_with_empty_native_jar_creates_no_directory() {
|
||||
let root = temp_dir("natives-dry-run-emptyjar");
|
||||
let jar = root.join("natives-windows.jar");
|
||||
make_jar(
|
||||
&jar,
|
||||
&[
|
||||
("META-INF/MANIFEST.MF", b"manifest".as_slice()),
|
||||
("README.txt", b"noise".as_slice()),
|
||||
],
|
||||
);
|
||||
let version_dir = root.join("versions/1.16.5");
|
||||
std::fs::create_dir_all(&version_dir).expect("建版本目录");
|
||||
let natives_dir = version_dir.join("natives");
|
||||
let before = tree_snapshot(&version_dir);
|
||||
|
||||
let (mut request, _) = legacy_forge_request();
|
||||
request.dirs.natives_dir = natives_dir.clone();
|
||||
request.natives = NativesPlan::new(vec![NativeJar::new(&jar, "lwjgl")]);
|
||||
request.manifest.legacy_arguments = Some("--username ${auth_player_name}".to_string());
|
||||
let sessions = temp_sessions_root("natives-dry-run-emptyjar-sessions");
|
||||
let launcher = Launcher::new(LauncherConfig::new(&sessions));
|
||||
|
||||
let dry = launcher.dry_run(&request).expect("dry-run 应成功");
|
||||
assert_eq!(before, tree_snapshot(&version_dir), "空 jar 也不许建目录");
|
||||
let report = dry.natives.expect("应有 natives 预览");
|
||||
assert_eq!(report.files, 0, "空 jar 解不出原生库:{report:?}");
|
||||
assert_eq!(report.ignored, 2, "两条噪音都要数进忽略:{report:?}");
|
||||
assert!(!report.up_to_date, "{report:?}");
|
||||
assert!(!report.dir_exists, "{report:?}");
|
||||
|
||||
assert!(!natives_dir.exists(), "natives 目录不该被建出来");
|
||||
}
|
||||
|
||||
/// 目录已经真解压过:预览读戳记报「已就绪」,同样零写盘(幂等路径也不许碰盘)。
|
||||
#[test]
|
||||
fn dry_run_after_real_extract_reports_ready_without_writing() {
|
||||
let root = temp_dir("natives-dry-run-ready");
|
||||
let jar = root.join("natives-windows.jar");
|
||||
make_jar(&jar, &[("lwjgl.dll", b"DLL".as_slice())]);
|
||||
let version_dir = root.join("versions/1.16.5");
|
||||
let natives_dir = version_dir.join("natives");
|
||||
std::fs::create_dir_all(&version_dir).expect("建版本目录");
|
||||
natives::extract(
|
||||
&natives_dir,
|
||||
&NativesPlan::new(vec![NativeJar::new(&jar, "lwjgl")]),
|
||||
)
|
||||
.expect("先真解压一次");
|
||||
let before = tree_snapshot(&version_dir);
|
||||
|
||||
let (mut request, _) = legacy_forge_request();
|
||||
request.dirs.natives_dir = natives_dir.clone();
|
||||
request.natives = NativesPlan::new(vec![NativeJar::new(&jar, "lwjgl")]);
|
||||
request.manifest.legacy_arguments = Some("--username ${auth_player_name}".to_string());
|
||||
let sessions = temp_sessions_root("natives-dry-run-ready-sessions");
|
||||
let launcher = Launcher::new(LauncherConfig::new(&sessions));
|
||||
|
||||
let dry = launcher.dry_run(&request).expect("dry-run 应成功");
|
||||
assert_eq!(
|
||||
before,
|
||||
tree_snapshot(&version_dir),
|
||||
"预览不该重写已有的原生库或戳记"
|
||||
);
|
||||
let report = dry.natives.expect("应有 natives 预览");
|
||||
assert!(report.planned, "{report:?}");
|
||||
assert!(report.dir_exists, "{report:?}");
|
||||
assert!(report.up_to_date, "戳记命中应报已就绪:{report:?}");
|
||||
assert_eq!(report.files, 0, "预览不该再数一遍:{report:?}");
|
||||
assert_eq!(report.skipped, 1, "{report:?}");
|
||||
}
|
||||
|
||||
/// 判据自检(防假绿):快照必须看得见「建一个子目录又立刻删掉」留下的父目录 mtime 变化,
|
||||
/// 否则上面几条「快照逐字相等」等于没断言。
|
||||
#[test]
|
||||
fn tree_snapshot_notices_a_transient_directory() {
|
||||
let root = temp_dir("natives-snapshot-probe");
|
||||
let version_dir = root.join("versions/1.16.5");
|
||||
std::fs::create_dir_all(&version_dir).expect("建版本目录");
|
||||
std::fs::write(version_dir.join("1.16.5.json"), b"{}").expect("放清单占位");
|
||||
let before = tree_snapshot(&version_dir);
|
||||
|
||||
// 模拟 `create_dir_all(natives)` 之后目录又被删掉:节点集合复原,只有父目录 mtime 变了。
|
||||
let transient = version_dir.join("natives");
|
||||
std::fs::create_dir(&transient).expect("建目录");
|
||||
std::fs::remove_dir(&transient).expect("删目录");
|
||||
assert!(!transient.exists(), "目录应已删掉");
|
||||
|
||||
let after = tree_snapshot(&version_dir);
|
||||
assert_eq!(
|
||||
before.len(),
|
||||
after.len(),
|
||||
"节点集合应恢复原样,差异只该在 mtime 上:{before:?} vs {after:?}"
|
||||
);
|
||||
assert_ne!(
|
||||
before, after,
|
||||
"快照必须看得见瞬时建删留下的父目录 mtime 变化,否则零落盘断言是假绿"
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,12 @@
|
||||
//!
|
||||
//! **crate 名只在本文件出现**(tower 定案 `unlucky_core`;若 M11 改包名,只改这里)。
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use zip::write::SimpleFileOptions;
|
||||
use zip::{CompressionMethod, ZipWriter};
|
||||
|
||||
pub use unlucky_core::launch::args_table::{assemble, AssembledCommand, AssemblyWarning};
|
||||
pub use unlucky_core::launch::argument::{ArgumentEntry, Rule};
|
||||
@@ -147,3 +152,32 @@ pub fn read_events(path: &std::path::Path) -> Vec<serde_json::Value> {
|
||||
.map(|line| serde_json::from_str(line).expect("解析事件行"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 造一个假 jar(`zip` Stored 模式,不依赖真下载):`entries` 是(条目名, 内容)。
|
||||
///
|
||||
/// natives 用例共用(真解压与 `--dry-run` 预览两边都要现造 jar)。
|
||||
pub fn make_jar(path: &Path, entries: &[(&str, &[u8])]) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("建 jar 目录");
|
||||
}
|
||||
let file = File::create(path).expect("建 jar");
|
||||
let mut writer = ZipWriter::new(file);
|
||||
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
for (name, bytes) in entries {
|
||||
writer.start_file(*name, options).expect("写条目");
|
||||
writer.write_all(bytes).expect("写条目内容");
|
||||
}
|
||||
writer.finish().expect("收尾 jar");
|
||||
}
|
||||
|
||||
/// 一份 1.16.5 风格的 natives jar 的条目集(2 个原生库 + 4 条该被排除的噪音)。
|
||||
pub fn lwjgl_jar_entries() -> Vec<(&'static str, &'static [u8])> {
|
||||
vec![
|
||||
("lwjgl.dll", b"DLL-BYTES".as_slice()),
|
||||
("OpenAL.dll", b"OPENAL-BYTES".as_slice()),
|
||||
("META-INF/MANIFEST.MF", b"manifest".as_slice()),
|
||||
("META-INF/LWJGL.SF", b"signature".as_slice()),
|
||||
("lwjgl.dll.sha1", b"deadbeef".as_slice()),
|
||||
("README.txt", b"noise".as_slice()),
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user