Release v0.2.9
This commit is contained in:
parent
6c54ec8139
commit
e413fd1a78
8 changed files with 330 additions and 12 deletions
|
|
@ -57,6 +57,8 @@ standalone images, and videos.
|
|||
- Save favorites for local and remote folders and files, and preserve the saved
|
||||
location profile needed to reconnect to remote favorites.
|
||||
- Save progress per ZIP and save the most recently read ZIP for resume support.
|
||||
- Let users update from the About window by downloading the fixed Cafe24 latest
|
||||
bundle and checksum, verifying SHA-256, and reusing the atomic installer.
|
||||
|
||||
## Data Locations
|
||||
|
||||
|
|
|
|||
25
Cargo.lock
generated
25
Cargo.lock
generated
|
|
@ -182,17 +182,19 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
|||
|
||||
[[package]]
|
||||
name = "comicviewer"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"filetime",
|
||||
"flate2",
|
||||
"gtk4",
|
||||
"image",
|
||||
"percent-encoding",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"sha2",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"zip",
|
||||
|
|
@ -1211,6 +1213,17 @@ dependencies = [
|
|||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.5"
|
||||
|
|
@ -1475,6 +1488,16 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.54"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "comicviewer"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
edition = "2024"
|
||||
description = "A comic archive viewer for local and remote locations"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
|
@ -11,11 +11,13 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
|||
gtk = { package = "gtk4", version = "0.10", features = ["v4_10"] }
|
||||
image = { version = "0.25", default-features = false, features = ["avif-native", "bmp", "gif", "jpeg", "png", "webp"] }
|
||||
filetime = "0.2"
|
||||
flate2 = "1.0"
|
||||
percent-encoding = "2.3"
|
||||
rusqlite = { version = "0.37", features = ["bundled"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha2 = "0.10"
|
||||
tempfile = "3.20"
|
||||
tar = "0.4"
|
||||
toml = "0.9"
|
||||
zip = { version = "8", default-features = false, features = ["deflate"] }
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
</provides>
|
||||
<content_rating type="oars-1.1" />
|
||||
<releases>
|
||||
<release version="0.2.9" date="2026-07-24" />
|
||||
<release version="0.2.8" date="2026-07-24" />
|
||||
<release version="0.2.7" date="2026-07-24" />
|
||||
<release version="0.2.6" date="2026-07-23" />
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Comicviewer는 CachyOS와 Arch Linux에서 사용할 수 있는 만화·미디
|
|||
설치합니다. 설치 과정에서 `sudo` 암호와 pacman 확인을 요청할 수 있습니다.
|
||||
|
||||
```sh
|
||||
sudo pacman -S fuse2 gtk4 dav1d gvfs gvfs-smb gstreamer \
|
||||
sudo pacman -S fuse2 gtk4 dav1d gvfs gvfs-smb gstreamer curl \
|
||||
gst-plugins-base gst-plugins-good gst-plugins-bad gst-libav
|
||||
```
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ sudo pacman -S fuse2 gtk4 dav1d gvfs gvfs-smb gstreamer \
|
|||
- `gvfs`: FTP와 SFTP 접근
|
||||
- `gvfs-smb`: SMB 공유 접근
|
||||
- `gstreamer`, `gst-plugins-*`, `gst-libav`: MP4 등 동영상 재생
|
||||
- `curl`: About 창에서 최신 업데이트 다운로드
|
||||
|
||||
## 2. 권장 방법: 통합 번들로 설치 또는 업데이트
|
||||
|
||||
|
|
@ -92,6 +93,7 @@ Comicviewer는 XDG 표준 경로를 사용합니다.
|
|||
- 동영상에서 `↑`와 `↓`: 음량을 5%씩 조절
|
||||
- `L`: 우측 파일 목록 표시 또는 숨기기
|
||||
- 파일 목록 상단 검색: 현재 재생 목록을 임시로 필터링하며, 입력을 지우면 전체 목록 복원
|
||||
- About 창의 `업데이트`: Cafe24에서 최신 번들을 내려받아 검증 후 사용자 영역에 설치
|
||||
- 이미지에서 `+`와 `-`: 현재 화면의 이미지를 임시로 10%씩 확대 또는 축소
|
||||
- 이미지에서 `Ctrl` + 마우스 휠: 현재 화면의 이미지를 임시로 10%씩 확대 또는 축소
|
||||
- 전체 화면에서 마우스 클릭: 숨겨진 컨트롤을 5초 동안 표시
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ AppImage 빌드가 끝나면 다음 스크립트로 체크섬과 통합 `tar.gz`
|
|||
|
||||
스크립트는 `Cargo.toml`에서 버전을 읽고 다음 파일을 생성합니다.
|
||||
|
||||
- `dist/Comicviewer-0.2.8-x86_64.AppImage.sha256`
|
||||
- `dist/Comicviewer-0.2.9-x86_64.AppImage.sha256`
|
||||
- `dist/Comicviewer-latest-x86_64.tar.gz`
|
||||
- `dist/Comicviewer-latest-x86_64.tar.gz.sha256`
|
||||
|
||||
|
|
@ -78,10 +78,10 @@ sha256sum -c Comicviewer-latest-x86_64.tar.gz.sha256
|
|||
|
||||
```sh
|
||||
git add Cargo.toml Cargo.lock data packaging README.md docs
|
||||
git commit -m "Release v0.2.8"
|
||||
git tag -a v0.2.8 -m "Comicviewer v0.2.8"
|
||||
git commit -m "Release v0.2.9"
|
||||
git tag -a v0.2.9 -m "Comicviewer v0.2.9"
|
||||
git push origin main
|
||||
git push origin v0.2.8
|
||||
git push origin v0.2.9
|
||||
```
|
||||
|
||||
이미 원격에 공개한 태그는 이동하거나 덮어쓰지 않습니다. 수정이 필요하면 새
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ install_dependencies() {
|
|||
fi
|
||||
|
||||
set --
|
||||
for package in fuse2 gtk4 dav1d gvfs gvfs-smb gstreamer gst-plugins-base \
|
||||
for package in fuse2 gtk4 dav1d gvfs gvfs-smb gstreamer curl gst-plugins-base \
|
||||
gst-plugins-good gst-plugins-bad gst-libav; do
|
||||
if ! pacman -Q "${package}" >/dev/null 2>&1; then
|
||||
set -- "$@" "${package}"
|
||||
|
|
|
|||
296
src/window.rs
296
src/window.rs
|
|
@ -1,15 +1,22 @@
|
|||
use std::cell::RefCell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use chrono::{DateTime, Local};
|
||||
use flate2::read::GzDecoder;
|
||||
use gtk::gio;
|
||||
use gtk::gio::prelude::*;
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tar::Archive;
|
||||
|
||||
use crate::browser::{
|
||||
BrowserEntry, EntryKind, SortDirection, SortField, classify_file, sort_entries,
|
||||
|
|
@ -29,6 +36,9 @@ use crate::viewer::{
|
|||
use crate::zip_cache::ZipCache;
|
||||
|
||||
const MOUSE_BACK_BUTTON: u32 = 8;
|
||||
const UPDATE_BUNDLE_URL: &str =
|
||||
"https://burningtuna.cafe24.com/www/Downloads/Comicviewer/Comicviewer-latest-x86_64.tar.gz";
|
||||
const UPDATE_CHECKSUM_URL: &str = "https://burningtuna.cafe24.com/www/Downloads/Comicviewer/Comicviewer-latest-x86_64.tar.gz.sha256";
|
||||
|
||||
pub struct ComicWindow {
|
||||
controller: Rc<BrowserController>,
|
||||
|
|
@ -163,6 +173,7 @@ impl ComicWindow {
|
|||
database: Rc::new(database),
|
||||
zip_cache,
|
||||
listing_task: RefCell::new(None),
|
||||
update_in_progress: Rc::new(Cell::new(false)),
|
||||
});
|
||||
|
||||
controller.rebuild_profiles();
|
||||
|
|
@ -224,6 +235,7 @@ struct BrowserController {
|
|||
database: Rc<Database>,
|
||||
zip_cache: Rc<ZipCache>,
|
||||
listing_task: RefCell<Option<glib::JoinHandle<()>>>,
|
||||
update_in_progress: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl BrowserController {
|
||||
|
|
@ -1613,18 +1625,72 @@ impl BrowserController {
|
|||
"사용한 라이브러리\n\
|
||||
GTK4 / GIO · GStreamer · image · zip · rusqlite / SQLite\n\
|
||||
serde · toml · anyhow · chrono · filetime · percent-encoding\n\
|
||||
sha2 · tempfile",
|
||||
sha2 · tempfile · flate2 · tar",
|
||||
));
|
||||
libraries.set_xalign(0.0);
|
||||
libraries.set_wrap(true);
|
||||
let close_button = gtk::Button::with_label("닫기");
|
||||
close_button.set_halign(gtk::Align::End);
|
||||
let update_button = gtk::Button::with_label("업데이트");
|
||||
let update_status = gtk::Label::new(None);
|
||||
update_status.set_wrap(true);
|
||||
update_status.set_xalign(0.0);
|
||||
let actions = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
actions.set_halign(gtk::Align::End);
|
||||
actions.append(&update_button);
|
||||
actions.append(&close_button);
|
||||
if self.update_in_progress.get() {
|
||||
update_button.set_sensitive(false);
|
||||
update_status.set_text("다른 업데이트 작업이 진행 중입니다.");
|
||||
}
|
||||
let dialog_to_close = dialog.clone();
|
||||
close_button.connect_clicked(move |_| dialog_to_close.close());
|
||||
let status = update_status.clone();
|
||||
let update_in_progress = Rc::clone(&self.update_in_progress);
|
||||
update_button.connect_clicked(move |button| {
|
||||
if update_in_progress.replace(true) {
|
||||
return;
|
||||
}
|
||||
button.set_sensitive(false);
|
||||
status.set_text("업데이트를 다운로드하고 검증하는 중...");
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
thread::spawn(move || {
|
||||
let result = install_latest_update().map_err(|error| format!("{error:#}"));
|
||||
let _ = sender.send(result);
|
||||
});
|
||||
let button = button.clone();
|
||||
let status = status.clone();
|
||||
let update_in_progress = Rc::clone(&update_in_progress);
|
||||
glib::timeout_add_local(Duration::from_millis(100), move || {
|
||||
match receiver.try_recv() {
|
||||
Ok(Ok(())) => {
|
||||
update_in_progress.set(false);
|
||||
status.set_text(
|
||||
"업데이트를 완료했습니다. 앱을 종료한 뒤 ~/.local/bin/comicviewer로 다시 실행해 주세요.",
|
||||
);
|
||||
button.set_label("업데이트 완료");
|
||||
glib::ControlFlow::Break
|
||||
}
|
||||
Ok(Err(error)) => {
|
||||
update_in_progress.set(false);
|
||||
status.set_text(&format!("업데이트 실패: {error}"));
|
||||
button.set_sensitive(true);
|
||||
glib::ControlFlow::Break
|
||||
}
|
||||
Err(mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
||||
Err(mpsc::TryRecvError::Disconnected) => {
|
||||
update_in_progress.set(false);
|
||||
status.set_text("업데이트 작업이 예기치 않게 종료되었습니다.");
|
||||
button.set_sensitive(true);
|
||||
glib::ControlFlow::Break
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
content.append(&name);
|
||||
content.append(&contact);
|
||||
content.append(&libraries);
|
||||
content.append(&close_button);
|
||||
content.append(&update_status);
|
||||
content.append(&actions);
|
||||
dialog.set_child(Some(&content));
|
||||
dialog.present();
|
||||
}
|
||||
|
|
@ -1855,6 +1921,150 @@ impl BrowserController {
|
|||
}
|
||||
}
|
||||
|
||||
fn install_latest_update() -> Result<()> {
|
||||
let directory = tempfile::tempdir().context("업데이트 임시 폴더를 만들 수 없습니다")?;
|
||||
let archive = directory.path().join("Comicviewer-latest-x86_64.tar.gz");
|
||||
let checksum = directory
|
||||
.path()
|
||||
.join("Comicviewer-latest-x86_64.tar.gz.sha256");
|
||||
download_update_file(UPDATE_BUNDLE_URL, &archive)?;
|
||||
download_update_file(UPDATE_CHECKSUM_URL, &checksum)?;
|
||||
|
||||
let checksum_text =
|
||||
fs::read_to_string(&checksum).context("업데이트 체크섬을 읽을 수 없습니다")?;
|
||||
let expected = parse_update_checksum(&checksum_text)?;
|
||||
let actual = sha256_file(&archive)?;
|
||||
if actual != expected {
|
||||
bail!("다운로드한 업데이트 체크섬이 일치하지 않습니다");
|
||||
}
|
||||
|
||||
let extraction = directory.path().join("extracted");
|
||||
fs::create_dir(&extraction).context("업데이트 압축 해제 폴더를 만들 수 없습니다")?;
|
||||
extract_update_archive(&archive, &extraction)?;
|
||||
let installer = extraction
|
||||
.join("Comicviewer-latest-x86_64")
|
||||
.join("install-or-update.sh");
|
||||
if !installer.is_file() {
|
||||
bail!("업데이트 번들에 설치 스크립트가 없습니다");
|
||||
}
|
||||
run_update_command(
|
||||
Command::new("sh")
|
||||
.arg(&installer)
|
||||
.env("COMICVIEWER_SKIP_DEPENDENCIES", "1"),
|
||||
"업데이트 설치에 실패했습니다",
|
||||
)
|
||||
}
|
||||
|
||||
fn download_update_file(url: &str, destination: &Path) -> Result<()> {
|
||||
let url = format!("{url}?from={}", env!("CARGO_PKG_VERSION"));
|
||||
run_update_command(
|
||||
Command::new("curl")
|
||||
.arg("--fail")
|
||||
.arg("--silent")
|
||||
.arg("--show-error")
|
||||
.arg("--location")
|
||||
.arg("--proto")
|
||||
.arg("=https")
|
||||
.arg("--proto-redir")
|
||||
.arg("=https")
|
||||
.arg("--connect-timeout")
|
||||
.arg("15")
|
||||
.arg("--max-time")
|
||||
.arg("300")
|
||||
.arg("--max-filesize")
|
||||
.arg((512_u64 * 1024 * 1024).to_string())
|
||||
.arg("--retry")
|
||||
.arg("3")
|
||||
.arg("--output")
|
||||
.arg(destination)
|
||||
.arg(url),
|
||||
"업데이트를 다운로드할 수 없습니다. curl 설치와 네트워크 연결을 확인해 주세요",
|
||||
)
|
||||
}
|
||||
|
||||
fn run_update_command(command: &mut Command, context: &str) -> Result<()> {
|
||||
let output = command.output().with_context(|| context.to_owned())?;
|
||||
if output.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
let stderr = String::from_utf8_lossy(&output.stderr)
|
||||
.trim()
|
||||
.chars()
|
||||
.take(600)
|
||||
.collect::<String>();
|
||||
if stderr.is_empty() {
|
||||
bail!("{context}");
|
||||
}
|
||||
bail!("{context}: {stderr}")
|
||||
}
|
||||
|
||||
fn extract_update_archive(archive_path: &Path, destination: &Path) -> Result<()> {
|
||||
const MAX_ENTRIES: usize = 32;
|
||||
const MAX_EXTRACTED_BYTES: u64 = 512 * 1024 * 1024;
|
||||
|
||||
let file = fs::File::open(archive_path).context("업데이트 압축 파일을 열 수 없습니다")?;
|
||||
let decoder = GzDecoder::new(file);
|
||||
let mut archive = Archive::new(decoder);
|
||||
let mut count = 0_usize;
|
||||
let mut extracted_bytes = 0_u64;
|
||||
for entry in archive
|
||||
.entries()
|
||||
.context("업데이트 압축 목록을 읽을 수 없습니다")?
|
||||
{
|
||||
let mut entry = entry.context("업데이트 압축 항목을 읽을 수 없습니다")?;
|
||||
count += 1;
|
||||
if count > MAX_ENTRIES {
|
||||
bail!("업데이트 압축 항목 수가 안전 제한을 초과합니다");
|
||||
}
|
||||
let entry_type = entry.header().entry_type();
|
||||
if !entry_type.is_file() && !entry_type.is_dir() {
|
||||
bail!("업데이트 압축에 허용되지 않은 파일 종류가 있습니다");
|
||||
}
|
||||
let path = entry
|
||||
.path()
|
||||
.context("업데이트 압축 경로가 올바르지 않습니다")?;
|
||||
if !path.starts_with("Comicviewer-latest-x86_64") {
|
||||
bail!("업데이트 압축 경로가 올바르지 않습니다");
|
||||
}
|
||||
extracted_bytes = extracted_bytes.saturating_add(entry.header().size().unwrap_or(u64::MAX));
|
||||
if extracted_bytes > MAX_EXTRACTED_BYTES {
|
||||
bail!("업데이트 압축 크기가 안전 제한을 초과합니다");
|
||||
}
|
||||
if !entry
|
||||
.unpack_in(destination)
|
||||
.context("업데이트 압축을 풀 수 없습니다")?
|
||||
{
|
||||
bail!("업데이트 압축 경로가 임시 폴더를 벗어납니다");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_update_checksum(contents: &str) -> Result<String> {
|
||||
let checksum = contents
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.filter(|checksum| {
|
||||
checksum.len() == 64 && checksum.chars().all(|value| value.is_ascii_hexdigit())
|
||||
})
|
||||
.context("업데이트 체크섬 파일이 올바르지 않습니다")?;
|
||||
Ok(checksum.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn sha256_file(path: &Path) -> Result<String> {
|
||||
let mut file = fs::File::open(path).context("다운로드한 업데이트를 읽을 수 없습니다")?;
|
||||
let mut digest = Sha256::new();
|
||||
let mut buffer = [0_u8; 64 * 1024];
|
||||
loop {
|
||||
let count = file.read(&mut buffer)?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
digest.update(&buffer[..count]);
|
||||
}
|
||||
Ok(format!("{:x}", digest.finalize()))
|
||||
}
|
||||
|
||||
fn location_kind_index(kind: LocationKind) -> u32 {
|
||||
match kind {
|
||||
LocationKind::Local => 0,
|
||||
|
|
@ -2227,6 +2437,84 @@ mod tests {
|
|||
assert!(!browser_search_match(&nested, "comic"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_checksum_parser_validates_sha256_text() {
|
||||
let checksum =
|
||||
"ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789 bundle.tar.gz\n";
|
||||
assert_eq!(
|
||||
parse_update_checksum(checksum).unwrap(),
|
||||
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
|
||||
);
|
||||
assert!(parse_update_checksum("not-a-checksum").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_sha256_matches_known_content() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let file = directory.path().join("content");
|
||||
fs::write(&file, b"abc").unwrap();
|
||||
assert_eq!(
|
||||
sha256_file(&file).unwrap(),
|
||||
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_archive_extracts_regular_files_and_rejects_symlinks() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let valid_archive = directory.path().join("valid.tar.gz");
|
||||
{
|
||||
let file = fs::File::create(&valid_archive).unwrap();
|
||||
let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
|
||||
let mut archive = tar::Builder::new(encoder);
|
||||
let content = b"#!/bin/sh\n";
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(content.len() as u64);
|
||||
header.set_mode(0o755);
|
||||
header.set_cksum();
|
||||
archive
|
||||
.append_data(
|
||||
&mut header,
|
||||
"Comicviewer-latest-x86_64/install-or-update.sh",
|
||||
&content[..],
|
||||
)
|
||||
.unwrap();
|
||||
archive.into_inner().unwrap().finish().unwrap();
|
||||
}
|
||||
let extracted = directory.path().join("valid");
|
||||
fs::create_dir(&extracted).unwrap();
|
||||
extract_update_archive(&valid_archive, &extracted).unwrap();
|
||||
assert!(
|
||||
extracted
|
||||
.join("Comicviewer-latest-x86_64/install-or-update.sh")
|
||||
.is_file()
|
||||
);
|
||||
|
||||
let link_archive = directory.path().join("link.tar.gz");
|
||||
{
|
||||
let file = fs::File::create(&link_archive).unwrap();
|
||||
let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
|
||||
let mut archive = tar::Builder::new(encoder);
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_entry_type(tar::EntryType::Symlink);
|
||||
header.set_size(0);
|
||||
header.set_mode(0o777);
|
||||
header.set_link_name("../../escape").unwrap();
|
||||
header.set_cksum();
|
||||
archive
|
||||
.append_data(
|
||||
&mut header,
|
||||
"Comicviewer-latest-x86_64/link",
|
||||
std::io::empty(),
|
||||
)
|
||||
.unwrap();
|
||||
archive.into_inner().unwrap().finish().unwrap();
|
||||
}
|
||||
let rejected = directory.path().join("rejected");
|
||||
fs::create_dir(&rejected).unwrap();
|
||||
assert!(extract_update_archive(&link_archive, &rejected).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn favorites_are_rendered_as_browser_paths_and_files() {
|
||||
let profile = home_profile();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue