├── public
└── .gitkeep
├── src-tauri
├── libs
│ ├── hdiff-sys
│ │ ├── .gitignore
│ │ ├── .gitattributes
│ │ ├── Cargo.toml
│ │ ├── HDiff
│ │ │ └── private_diff
│ │ │ │ ├── libdivsufsort
│ │ │ │ ├── divsufsort.cpp
│ │ │ │ ├── divsufsort64.cpp
│ │ │ │ ├── config.h
│ │ │ │ ├── divsufsort.h
│ │ │ │ ├── divsufsort64.h
│ │ │ │ └── divsufsort_private.h
│ │ │ │ ├── mem_buf.h
│ │ │ │ ├── compress_detect.h
│ │ │ │ ├── bytes_rle.h
│ │ │ │ ├── limit_mem_diff
│ │ │ │ ├── digest_matcher.h
│ │ │ │ ├── covers.h
│ │ │ │ └── bloom_filter.h
│ │ │ │ ├── suffix_string.h
│ │ │ │ ├── pack_uint.h
│ │ │ │ └── qsort_parallel.h
│ │ ├── Cargo.lock
│ │ ├── build.rs
│ │ ├── wrapper.h
│ │ ├── src
│ │ │ └── lib.rs
│ │ └── libParallel
│ │ │ ├── parallel_import.h
│ │ │ ├── parallel_channel.h
│ │ │ └── parallel_channel.cpp
│ └── hpatch-sys
│ │ ├── .gitignore
│ │ ├── wrapper.h
│ │ ├── build.rs
│ │ ├── .gitattributes
│ │ ├── Cargo.toml
│ │ └── HPatch
│ │ └── checksum_plugin.h
├── src
│ ├── module
│ │ └── mod.rs
│ ├── thirdparty
│ │ └── mod.rs
│ ├── ipc
│ │ ├── mod.rs
│ │ └── operation.rs
│ ├── builder
│ │ ├── utils
│ │ │ └── mod.rs
│ │ ├── main.rs
│ │ ├── append.rs
│ │ ├── cli
│ │ │ └── mod.rs
│ │ └── metadata.rs
│ ├── utils
│ │ ├── gui.rs
│ │ ├── mod.rs
│ │ ├── acl.rs
│ │ ├── progressed_read.rs
│ │ ├── hash.rs
│ │ ├── metadata.rs
│ │ ├── dir.rs
│ │ ├── uac.rs
│ │ ├── wincred.rs
│ │ ├── error.rs
│ │ ├── icon.rs
│ │ └── url.rs
│ ├── cli
│ │ ├── arg.rs
│ │ └── mod.rs
│ └── installer
│ │ ├── lnk.rs
│ │ └── registry.rs
├── rust-toolchain.toml
├── build.rs
├── icons
│ └── icon.ico
├── .gitignore
├── capabilities
│ └── default.json
├── .cargo
│ └── config.toml
├── tauri.conf.json
└── Cargo.toml
├── .prettierrc
├── .prettierignore
├── pnpm-workspace.yaml
├── src
├── left.webp
├── IconMinimize.vue
├── consts.ts
├── plugins
│ ├── registry.ts
│ ├── types.ts
│ └── index.ts
├── IconSheild.vue
├── networkInsights.ts
├── IconEdit.vue
├── IconClose.vue
├── CircleSuccess.vue
├── env.d.ts
├── Dialog.vue
├── tauri.ts
├── index.ts
├── Feedback.vue
├── components
│ └── SafeIcon.vue
├── Checkbox.vue
├── Cloud.vue
├── CloudPaid.vue
├── FInput.vue
├── utils
│ ├── friendlyError.ts
│ └── svgSanitizer.ts
├── mirrorc-errors.ts
├── api
│ └── installFile.ts
└── types.ts
├── .vscode
└── extensions.json
├── eslint.config.mjs
├── .gitignore
├── tsconfig.json
├── tests
├── server.mjs
├── offline-install.mjs
├── online-install.mjs
├── offline-update.mjs
├── utils.mjs
└── online-update.mjs
├── rsbuild.config.ts
├── package.json
├── README.md
└── .github
└── workflows
└── build.yml
/public/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/.gitignore:
--------------------------------------------------------------------------------
1 | target/
--------------------------------------------------------------------------------
/src-tauri/libs/hpatch-sys/.gitignore:
--------------------------------------------------------------------------------
1 | target/
--------------------------------------------------------------------------------
/src-tauri/src/module/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod wv2;
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/src/thirdparty/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod mirrorc;
2 |
--------------------------------------------------------------------------------
/src-tauri/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "nightly"
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Lock files
2 | package-lock.json
3 | pnpm-lock.yaml
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | onlyBuiltDependencies:
2 | - '@sentry/cli'
3 | - core-js
4 |
--------------------------------------------------------------------------------
/src/left.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuehaiTeam/kachina-installer/HEAD/src/left.webp
--------------------------------------------------------------------------------
/src-tauri/src/ipc/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod install_file;
2 | pub mod manager;
3 | pub mod operation;
4 |
--------------------------------------------------------------------------------
/src-tauri/libs/hpatch-sys/wrapper.h:
--------------------------------------------------------------------------------
1 | #include "HPatch/patch.h"
2 | #include "HPatch/patch_types.h"
3 |
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuehaiTeam/kachina-installer/HEAD/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/libs/hpatch-sys/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | cc::Build::new().file("HPatch/patch.c").compile("hpatch");
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/libs/hpatch-sys/.gitattributes:
--------------------------------------------------------------------------------
1 | HPatch/** linguist-vendored
2 | binding.rs linguist-vendored
3 | wrapper.h linguist-vendored
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/.gitattributes:
--------------------------------------------------------------------------------
1 | HDiff/** linguist-vendored
2 | libParallel/** linguist-vendored
3 | binding.rs linguist-vendored
4 | wrapper.h linguist-vendored
5 |
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "hdiff-sys"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | [build-dependencies]
8 | cc = "1.0"
--------------------------------------------------------------------------------
/src-tauri/libs/hpatch-sys/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "hpatch-sys"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | [build-dependencies]
8 | cc = "1.0"
9 |
--------------------------------------------------------------------------------
/src/IconMinimize.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/consts.ts:
--------------------------------------------------------------------------------
1 | export const getRuntimeName = (tag: string): string => {
2 | if (tag.startsWith('Microsoft.DotNet')) {
3 | return 'Microsoft .NET Runtime';
4 | }
5 | return tag;
6 | };
7 |
--------------------------------------------------------------------------------
/src/plugins/registry.ts:
--------------------------------------------------------------------------------
1 | import { pluginManager } from './index';
2 | import { GitHubPlugin } from './github';
3 |
4 | export function registerAllPlugins() {
5 | pluginManager.register(new GitHubPlugin());
6 | }
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Generated by Tauri
6 | # will have schema files for capabilities auto-completion
7 | /gen/schemas
8 |
--------------------------------------------------------------------------------
/src-tauri/src/builder/utils/mod.rs:
--------------------------------------------------------------------------------
1 | #[path = "../../utils/hash.rs"]
2 | pub mod hash;
3 | #[path = "../../utils/progressed_read.rs"]
4 | pub mod progressed_read;
5 |
6 | #[path = "../../utils/metadata.rs"]
7 | pub mod metadata;
8 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js';
2 | import globals from 'globals';
3 | import ts from 'typescript-eslint';
4 |
5 | export default [
6 | { languageOptions: { globals: globals.browser } },
7 | js.configs.recommended,
8 | ...ts.configs.recommended,
9 | { ignores: ['dist/'] },
10 | ];
11 |
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/HDiff/private_diff/libdivsufsort/divsufsort.cpp:
--------------------------------------------------------------------------------
1 | #define HAVE_CONFIG_H 1
2 | # include "divsufsort.h"
3 | typedef saidx32_t saidx_t;
4 | typedef saidx_t sastore_t;
5 |
6 | #include "divsufsort_private.h"
7 | #include "divsufsort.c.inc.h"
8 | #include "sssort.c.inc.h"
9 | #include "trsort.c.inc.h"
10 |
11 |
--------------------------------------------------------------------------------
/src/IconSheild.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/src/networkInsights.ts:
--------------------------------------------------------------------------------
1 | import { InsightItem } from './types';
2 |
3 | // 全局统计数据数组
4 | export const networkInsights: InsightItem[] = [];
5 |
6 | // 添加统计数据
7 | export function addNetworkInsight(insight: InsightItem) {
8 | networkInsights.push(insight);
9 | }
10 |
11 | // 清空统计数据
12 | export function clearNetworkInsights() {
13 | networkInsights.length = 0;
14 | }
15 |
--------------------------------------------------------------------------------
/src/IconEdit.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/gui.rs:
--------------------------------------------------------------------------------
1 | const SUBKEY: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
2 | const VALUE: &str = "AppsUseLightTheme";
3 |
4 | pub fn is_dark_mode() -> windows_registry::Result {
5 | let hkcu = windows_registry::CURRENT_USER;
6 | let subkey = hkcu.options().read().open(SUBKEY)?;
7 | let dword: u32 = subkey.get_u32(VALUE)?;
8 | Ok(dword == 0)
9 | }
10 |
--------------------------------------------------------------------------------
/src/IconClose.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | .env
27 |
28 | tests/fixtures
29 |
30 | rustc-ice*
--------------------------------------------------------------------------------
/src/CircleSuccess.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.vue' {
4 | import type { DefineComponent } from 'vue';
5 |
6 | // biome-ignore lint/complexity/noBannedTypes: reason
7 | const component: DefineComponent<{}, {}, any>;
8 | export default component;
9 | }
10 |
11 | // process.env.NODE_ENV is defined by the environment
12 | declare const process: {
13 | env: {
14 | NODE_ENV: 'development' | 'production';
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../gen/schemas/desktop-schema.json",
3 | "identifier": "default",
4 | "description": "Capability for the main window",
5 | "windows": [
6 | "main"
7 | ],
8 | "permissions": [
9 | "core:window:allow-set-title",
10 | "core:window:allow-show",
11 | "core:window:allow-close",
12 | "core:window:allow-minimize",
13 | "core:event:default",
14 | "core:window:allow-set-decorations"
15 | ]
16 | }
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/HDiff/private_diff/libdivsufsort/divsufsort64.cpp:
--------------------------------------------------------------------------------
1 | #define BUILD_DIVSUFSORT64
2 | #define HAVE_CONFIG_H 1
3 | # include "divsufsort64.h"
4 | typedef saidx64_t saidx_t;
5 | typedef saidx_t sastore_t;
6 | # define divsufsort divsufsort64
7 | # define divsufsort_version divsufsort64_version
8 | # define sssort sssort64
9 | # define trsort trsort64
10 |
11 | #include "divsufsort_private.h"
12 | #include "divsufsort.c.inc.h"
13 | #include "sssort.c.inc.h"
14 | #include "trsort.c.inc.h"
15 |
--------------------------------------------------------------------------------
/src-tauri/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [target.'cfg(all(windows, target_env = "msvc"))']
2 | rustflags = [
3 | "-C",
4 | "target-feature=+crt-static",
5 | "-C",
6 | "link-args=/NODEFAULTLIB:ucrt.lib /NODEFAULTLIB:libucrtd.lib /NODEFAULTLIB:ucrtd.lib /NODEFAULTLIB:libcmtd.lib /NODEFAULTLIB:msvcrt.lib /NODEFAULTLIB:msvcrtd.lib /NODEFAULTLIB:libvcruntimed.lib /NODEFAULTLIB:vcruntime.lib /NODEFAULTLIB:vcruntimed.lib /DEFAULTLIB:libucrt.lib /DEFAULTLIB:libvcruntime.lib /DEFAULTLIB:libcmt.lib /DEFAULTLIB:msvcrt.lib /DEFAULTLIB:ucrt.lib /DEFAULTLIB:oldnames.lib /DEFAULTLIB:legacy_stdio_definitions.lib",
7 | ]
8 |
--------------------------------------------------------------------------------
/src/plugins/types.ts:
--------------------------------------------------------------------------------
1 | import type { Dfs2Data, InvokeGetDfsMetadataRes, Embedded } from '../types';
2 |
3 | export interface KachinaInstallSource {
4 | name: string;
5 | matchUrl: (url: string) => boolean;
6 |
7 | // 可选:自定义元数据获取,返回完整的DFS2数据结构
8 | getMetadata?: (url: string) => Promise;
9 |
10 | // 可选:会话管理(插件自己管理sessionId)
11 | createSession?: (url: string, diffchunks: string[]) => Promise;
12 | endSession?: (url: string, insights: any) => Promise;
13 |
14 | // 必需:获取文件块URL
15 | getChunkUrl: (url: string, range: string) => Promise<{url: string, range: string}>;
16 | }
--------------------------------------------------------------------------------
/src/Dialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
29 |
--------------------------------------------------------------------------------
/src/tauri.ts:
--------------------------------------------------------------------------------
1 | import type { invoke as invokeType } from '@tauri-apps/api/core';
2 | import type { listen as listenType } from '@tauri-apps/api/event';
3 | import type { sep as sepType } from '@tauri-apps/api/path';
4 | import type { getCurrentWindow as getCurrentWindowType } from '@tauri-apps/api/window';
5 | const TAURI = (window as any).__TAURI__;
6 | export const invoke = TAURI.core.invoke as typeof invokeType;
7 | export const listen = TAURI.event.listen as typeof listenType;
8 | export const sep = TAURI.path.sep as typeof sepType;
9 | export const getCurrentWindow = TAURI.window
10 | .getCurrentWindow as typeof getCurrentWindowType;
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["DOM", "ES2020"],
4 | "jsx": "preserve",
5 | "target": "ES2020",
6 | "noEmit": true,
7 | "skipLibCheck": true,
8 | "jsxImportSource": "vue",
9 | "useDefineForClassFields": true,
10 |
11 | /* modules */
12 | "module": "ESNext",
13 | "isolatedModules": true,
14 | "resolveJsonModule": true,
15 | "moduleResolution": "Bundler",
16 | "allowImportingTsExtensions": true,
17 |
18 | /* type checking */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true
22 | },
23 | "include": ["src"]
24 | }
25 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import App from './App.vue';
3 | import './index.css';
4 |
5 | createApp(App).mount('#root');
6 |
7 | if (process.env.NODE_ENV !== 'development') {
8 | window.addEventListener('contextmenu', (e) => {
9 | e.preventDefault();
10 | });
11 | document.addEventListener('keydown', function (event) {
12 | // Prevent F5 or Ctrl+R (Windows/Linux) and Command+R (Mac) from refreshing the page
13 | if (
14 | event.key === 'F5' ||
15 | (event.ctrlKey && event.key === 'r') ||
16 | (event.metaKey && event.key === 'r')
17 | ) {
18 | event.preventDefault();
19 | }
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/Feedback.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod acl;
2 | pub mod dir;
3 | pub mod error;
4 | pub mod gui;
5 | pub mod hash;
6 | pub mod icon;
7 | pub mod metadata;
8 | pub mod progressed_read;
9 | pub mod sentry;
10 | pub mod uac;
11 | pub mod url;
12 | pub mod wincred;
13 |
14 | pub fn get_device_id() -> anyhow::Result {
15 | let username = whoami::username();
16 | let key = windows_registry::LOCAL_MACHINE
17 | .options()
18 | .read()
19 | .open(r#"SOFTWARE\Microsoft\Cryptography"#)?;
20 |
21 | let guid: String = key.get_string("MachineGuid")?;
22 | let raw_device_id = format!("{username}{guid}");
23 | Ok(chksum_md5::hash(raw_device_id).to_hex_uppercase())
24 | }
25 |
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "cc"
7 | version = "1.2.9"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
10 | dependencies = [
11 | "shlex",
12 | ]
13 |
14 | [[package]]
15 | name = "hdiff-sys"
16 | version = "0.1.0"
17 | dependencies = [
18 | "cc",
19 | ]
20 |
21 | [[package]]
22 | name = "shlex"
23 | version = "1.3.0"
24 | source = "registry+https://github.com/rust-lang/crates.io-index"
25 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
26 |
--------------------------------------------------------------------------------
/src/components/SafeIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
25 |
26 |
--------------------------------------------------------------------------------
/src/Checkbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
29 |
--------------------------------------------------------------------------------
/src/Cloud.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | cc::Build::new()
3 | .cpp(true)
4 | .cargo_output(true)
5 | .file("HDiff/diff.cpp")
6 | .file("HDiff/match_block.cpp")
7 | .file("HDiff/private_diff/bytes_rle.cpp")
8 | .file("HDiff/private_diff/compress_detect.cpp")
9 | .file("HDiff/private_diff/suffix_string.cpp")
10 | .file("HDiff/private_diff/limit_mem_diff/adler_roll.c")
11 | .file("HDiff/private_diff/limit_mem_diff/digest_matcher.cpp")
12 | .file("HDiff/private_diff/limit_mem_diff/stream_serialize.cpp")
13 | .file("HDiff/private_diff/libdivsufsort/divsufsort.cpp")
14 | .file("HDiff/private_diff/libdivsufsort/divsufsort64.cpp")
15 | .file("libParallel/parallel_channel.cpp")
16 | .file("libParallel/parallel_import.cpp")
17 | .file("../hpatch-sys/HPatch/patch.c")
18 | .compile("hdiff");
19 | }
20 |
--------------------------------------------------------------------------------
/src/CloudPaid.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/acl.rs:
--------------------------------------------------------------------------------
1 | use windows::{
2 | core::w,
3 | Win32::Security::{
4 | Authorization::{ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION},
5 | PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES,
6 | },
7 | };
8 |
9 | pub fn create_security_attributes() -> SECURITY_ATTRIBUTES {
10 | let mut security_descriptor = PSECURITY_DESCRIPTOR::default();
11 | unsafe {
12 | ConvertStringSecurityDescriptorToSecurityDescriptorW(
13 | w!("D:(A;;GA;;;AC)(A;;GA;;;RC)(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;BU)S:(ML;;NW;;;LW)"),
14 | SDDL_REVISION,
15 | &mut security_descriptor,
16 | None,
17 | )
18 | .unwrap();
19 |
20 | SECURITY_ATTRIBUTES {
21 | nLength: size_of::() as u32,
22 | lpSecurityDescriptor: security_descriptor.0,
23 | bInheritHandle: false.into(),
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/progressed_read.rs:
--------------------------------------------------------------------------------
1 | use pin_project::pin_project;
2 | use std::{
3 | pin::Pin,
4 | task::{Context, Poll},
5 | };
6 | use tokio::io::{AsyncRead, ReadBuf};
7 |
8 | #[pin_project]
9 | pub struct ReadWithCallback
10 | where
11 | R: AsyncRead,
12 | F: FnMut(usize),
13 | {
14 | #[pin]
15 | pub reader: R,
16 | pub callback: F,
17 | }
18 |
19 | impl AsyncRead for ReadWithCallback
20 | where
21 | R: AsyncRead,
22 | F: FnMut(usize),
23 | {
24 | fn poll_read(
25 | self: Pin<&mut Self>,
26 | cx: &mut Context<'_>,
27 | buf: &mut ReadBuf<'_>,
28 | ) -> Poll> {
29 | let this = self.project();
30 | let res = this.reader.poll_read(cx, buf);
31 | if let Poll::Ready(Ok(())) = res {
32 | if !buf.filled().is_empty() {
33 | (this.callback)(buf.filled().len());
34 | }
35 | }
36 | res
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.tauri.app/config/2",
3 | "productName": "Kachina Installer",
4 | "version": "0.1.0",
5 | "identifier": "click.kachina",
6 | "build": {
7 | "beforeDevCommand": "rsbuild dev",
8 | "devUrl": "http://localhost:1420",
9 | "beforeBuildCommand": "rsbuild build",
10 | "frontendDist": "../dist"
11 | },
12 | "app": {
13 | "withGlobalTauri": true,
14 | "windows": [],
15 | "security": {
16 | "csp": {
17 | "default-src": "'self' customprotocol: asset:",
18 | "connect-src": "*",
19 | "img-src": "'self' asset: http://asset.localhost blob: data:",
20 | "style-src": "'unsafe-inline' 'self'",
21 | "script-src": "'self' 'unsafe-eval' 'unsafe-inline'"
22 | }
23 | }
24 | },
25 | "bundle": {
26 | "active": false,
27 | "targets": "all",
28 | "copyright": "Built by Kachina Installer",
29 | "icon": ["icons/icon.ico"]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/FInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
18 |
19 |
20 |
21 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/tests/server.mjs:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import path from 'path';
3 |
4 | const PORT = process.env.PORT || 8080;
5 | const FIXTURES_DIR = './fixtures';
6 |
7 | function createServer() {
8 | const app = express();
9 |
10 | // 启用Range请求支持
11 | app.use(express.static(path.resolve(FIXTURES_DIR), {
12 | acceptRanges: true,
13 | lastModified: true,
14 | etag: true
15 | }));
16 |
17 | // 日志中间件
18 | app.use((req, res, next) => {
19 | console.log(`${req.method} ${req.url}`);
20 | next();
21 | });
22 |
23 | return app;
24 | }
25 |
26 | async function startServer() {
27 | const app = createServer();
28 |
29 | return new Promise((resolve) => {
30 | const server = app.listen(PORT, () => {
31 | console.log(chalk.green(`Express server listening on port ${PORT}`));
32 | console.log(chalk.gray(`Serving files from: ${path.resolve(FIXTURES_DIR)}`));
33 | resolve(server);
34 | });
35 |
36 | // 优雅关闭
37 | process.on('SIGINT', () => {
38 | console.log('\\nShutting down server...');
39 | server.close(() => process.exit(0));
40 | });
41 | });
42 | }
43 |
44 | if (import.meta.url === `file://${process.argv[1]}`) {
45 | startServer().catch(console.error);
46 | }
47 |
48 | export { startServer, createServer };
--------------------------------------------------------------------------------
/src-tauri/src/cli/arg.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use clap::Subcommand;
4 |
5 | #[derive(Debug, Clone, clap::Args, serde::Serialize)]
6 | pub struct InstallArgs {
7 | #[clap(short = 'D', help = "Install directory")]
8 | pub target: Option,
9 | #[clap(short = 'I', help = "Non-interactive install")]
10 | pub non_interactive: bool,
11 | #[clap(short = 'S', help = "Silent install")]
12 | pub silent: bool,
13 | #[clap(short = 'O', help = "Force online install")]
14 | pub online: bool,
15 | #[clap(short = 'U', help = "Uninstall")]
16 | pub uninstall: bool,
17 | // override install source
18 | #[clap(long, hide = true)]
19 | pub source: Option,
20 | // dfs extra data
21 | #[clap(long, hide = true)]
22 | pub dfs_extras: Option,
23 | // override mirrorc cdk
24 | #[clap(long, hide = true)]
25 | pub mirrorc_cdk: Option,
26 | }
27 |
28 | #[derive(Debug, Clone, clap::Args)]
29 | pub struct UacArgs {
30 | pub pipe_id: String,
31 | }
32 |
33 | #[derive(Subcommand, Clone, Debug)]
34 | pub enum Command {
35 | #[clap(hide = true)]
36 | Install(InstallArgs),
37 | #[clap(hide = true)]
38 | InstallWebview2,
39 | #[clap(hide = true)]
40 | HeadlessUac(UacArgs),
41 | #[clap(external_subcommand)]
42 | Other(Vec),
43 | }
44 |
--------------------------------------------------------------------------------
/src-tauri/src/builder/main.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 | use cli::Command;
3 |
4 | mod append;
5 | mod cli;
6 | mod extract;
7 | mod gen;
8 | mod local;
9 | mod metadata;
10 | mod pack;
11 | mod replace_bin;
12 | mod utils;
13 |
14 | pub fn main() {
15 | tokio::runtime::Builder::new_multi_thread()
16 | .enable_all()
17 | .build()
18 | .unwrap()
19 | .block_on(async_main());
20 | }
21 |
22 | async fn async_main() {
23 | println!("Kachina Builder v{}", env!("CARGO_PKG_VERSION"));
24 | let now = std::time::Instant::now();
25 | let cli = cli::Cli::parse();
26 | let mut command = cli.command;
27 | if command.is_none() {
28 | panic!("No command provided");
29 | }
30 | let command = command.take().unwrap();
31 | match command {
32 | Command::Pack(args) => pack::pack_cli(args).await,
33 | Command::Gen(args) => gen::gen_cli(args).await,
34 | Command::Append(args) => append::append_cli(args).await,
35 | Command::Extract(args) => extract::extract_cli(args).await,
36 | Command::ReplaceBin(args) => {
37 | if let Err(e) = replace_bin::replace_bin_cli(args).await {
38 | eprintln!("Replace-bin failed: {}", e);
39 | }
40 | }
41 | }
42 | let duration = now.elapsed();
43 | println!("Finished in {duration:?}");
44 | }
45 |
--------------------------------------------------------------------------------
/src/utils/friendlyError.ts:
--------------------------------------------------------------------------------
1 | export const friendlyError = (
2 | error: string | { message: string } | unknown,
3 | ): string => {
4 | const errStr =
5 | typeof error === 'string'
6 | ? error
7 | : error && typeof error === 'object' && 'message' in error
8 | ? (error as { message: string }).message
9 | : JSON.stringify(error);
10 | // 空格,换行符,制表符,右括号,逗号都是url结束
11 | const firstUrlInstr = errStr.match(/https?:\/\/[^\s),]+/);
12 | // 替换url时保留url结束标志字符,避免把右括号等也替换掉
13 | const errStrWithoutUrl = errStr.replace(/https?:\/\/[^\s),]+/g, '[url]');
14 | let friendlyStr = '';
15 | const checkStr = errStrWithoutUrl.toLowerCase();
16 | if (errStr.includes('operation timed out')) {
17 | friendlyStr = '连接下载服务器超时,请检查你的网络连接或更换下载源';
18 | } else if (checkStr.includes('connection refused')) {
19 | friendlyStr = '下载服务器出现问题,请重试或更换下载源';
20 | } else if (checkStr.includes('connection reset')) {
21 | friendlyStr = '连接下载服务器失败,请重试或更换下载源';
22 | } else if (checkStr.includes('too_slow') || checkStr.includes('stalled')) {
23 | friendlyStr = '检测到下载速度异常,请检查你的网络连接或更换下载源';
24 | }
25 |
26 | if (friendlyStr) {
27 | return `${friendlyStr}\n\n原始错误:${errStrWithoutUrl}${firstUrlInstr ? `\n\n下载服务器:${firstUrlInstr[0]}` : ''}`;
28 | } else {
29 | return `${errStrWithoutUrl}${firstUrlInstr ? `\n\n下载服务器:${firstUrlInstr[0]}` : ''}`;
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/hash.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{Context, Result};
2 | use std::{io::Read, path::Path};
3 |
4 | pub async fn run_hash(hash_algorithm: &str, path: &str) -> Result {
5 | if hash_algorithm == "md5" {
6 | let md5 = chksum_md5::async_chksum(Path::new(path))
7 | .await
8 | .context("HASH_COMPLETE_ERR")?;
9 | Ok(md5.to_hex_lowercase())
10 | } else if hash_algorithm == "xxh" {
11 | let path = path.to_string();
12 | let res = tokio::task::spawn_blocking(move || {
13 | use twox_hash::XxHash3_128;
14 | let mut hasher = XxHash3_128::new();
15 | let mut file = std::fs::OpenOptions::new()
16 | .read(true)
17 | .write(false)
18 | .open(&path)
19 | .context("OPEN_TARGET_ERR")?;
20 |
21 | let mut buffer = [0u8; 1024];
22 | loop {
23 | let read = file.read(&mut buffer).context("READ_FILE_ERR")?;
24 | if read == 0 {
25 | break;
26 | }
27 | hasher.write(&buffer[..read]);
28 | }
29 | let hash = hasher.finish_128();
30 | Ok::(format!("{hash:x}"))
31 | })
32 | .await
33 | .context("HASH_THREAD_ERR")?
34 | .context("HASH_COMPLETE_ERR")?;
35 | Ok(res)
36 | } else {
37 | Err(anyhow::anyhow!("NO_HASH_ALGO_ERR"))
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/rsbuild.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@rsbuild/core';
2 | import { pluginVue } from '@rsbuild/plugin-vue';
3 | import { purgeCSSPlugin } from '@fullhuman/postcss-purgecss';
4 |
5 | export default defineConfig({
6 | server: {
7 | port: 1420,
8 | },
9 | source: {
10 | define: {
11 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
12 | },
13 | },
14 | output: {
15 | overrideBrowserslist: ['edge >= 100'],
16 | },
17 | performance: {
18 | chunkSplit: {
19 | strategy: 'single-vendor',
20 | },
21 | },
22 | plugins: [pluginVue()],
23 | tools: {
24 | bundlerChain: (chain) => {
25 | // if (process.env.NODE_ENV !== 'development') {
26 | // chain.plugin('compress').use(CompressionPlugin, [
27 | // {
28 | // test: /\.(js|css|svg)$/,
29 | // filename: '[path][base].gz',
30 | // algorithm: 'gzip',
31 | // threshold: 1024,
32 | // minRatio: 0.8,
33 | // deleteOriginalAssets: true,
34 | // },
35 | // ]);
36 | // }
37 | },
38 | rspack: {
39 | experiments: {
40 | rspackFuture: {
41 | bundlerInfo: { force: false },
42 | },
43 | },
44 | },
45 | // @ts-expect-error -- postcss type not compatible
46 | postcss: {
47 | postcssOptions: {
48 | plugins: [
49 | purgeCSSPlugin({
50 | safelist: [/^(?!h[1-6]).*$/],
51 | variables: true,
52 | }),
53 | ],
54 | },
55 | },
56 | },
57 | });
58 |
--------------------------------------------------------------------------------
/src-tauri/src/installer/lnk.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{anyhow, Context, Result};
2 | use std::path::Path;
3 | use windows::Win32::UI::Shell::{
4 | FOLDERID_CommonPrograms, FOLDERID_Desktop, FOLDERID_Programs, FOLDERID_PublicDesktop,
5 | };
6 |
7 | use crate::utils::{
8 | dir::get_dir,
9 | error::{IntoAnyhow, TAResult},
10 | };
11 |
12 | #[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
13 | pub struct CreateLnkArgs {
14 | pub target: String,
15 | pub lnk: String,
16 | }
17 | pub async fn create_lnk_with_args(args: CreateLnkArgs) -> Result<()> {
18 | create_lnk(args.target, args.lnk).await.into_anyhow()
19 | }
20 |
21 | #[tauri::command]
22 | pub async fn create_lnk(target: String, lnk: String) -> TAResult<()> {
23 | let target = Path::new(&target);
24 | let lnk = Path::new(&lnk);
25 | let lnk_dir = lnk.parent();
26 | if lnk_dir.is_none() {
27 | return Err(anyhow!("Failed to get lnk parent dir")
28 | .context("CREATE_LNK_ERR")
29 | .into());
30 | }
31 | let lnk_dir = lnk_dir.unwrap();
32 | tokio::fs::create_dir_all(lnk_dir)
33 | .await
34 | .context("CREATE_LNK_ERR")?;
35 | let sl = mslnk::ShellLink::new(target).context("CREATE_LNK_ERR")?;
36 | sl.create_lnk(lnk).context("CREATE_LNK_ERR")?;
37 | Ok(())
38 | }
39 |
40 | #[tauri::command]
41 | pub async fn get_dirs(elevated: bool) -> TAResult<(String, String)> {
42 | if elevated {
43 | Ok((
44 | get_dir(&FOLDERID_CommonPrograms)?,
45 | get_dir(&FOLDERID_PublicDesktop)?,
46 | ))
47 | } else {
48 | Ok((get_dir(&FOLDERID_Programs)?, get_dir(&FOLDERID_Desktop)?))
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/metadata.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | #[derive(Serialize, Deserialize, Debug, Clone)]
4 | pub struct Metadata {
5 | pub file_name: String,
6 | pub size: u64,
7 | #[serde(skip_serializing_if = "Option::is_none")]
8 | pub md5: Option,
9 | #[serde(skip_serializing_if = "Option::is_none")]
10 | pub xxh: Option,
11 | }
12 |
13 | #[derive(Serialize, Deserialize, Debug)]
14 | pub struct PatchItem {
15 | pub size: u64,
16 | #[serde(skip_serializing_if = "Option::is_none")]
17 | pub md5: Option,
18 | #[serde(skip_serializing_if = "Option::is_none")]
19 | pub xxh: Option,
20 | }
21 |
22 | #[derive(Serialize, Deserialize, Debug)]
23 | pub struct PatchInfo {
24 | pub file_name: String,
25 | pub size: u64,
26 | pub from: PatchItem,
27 | pub to: PatchItem,
28 | }
29 |
30 | #[derive(Serialize, Deserialize, Debug)]
31 | pub struct InstallerInfo {
32 | pub size: u64,
33 | pub md5: Option,
34 | pub xxh: Option,
35 | }
36 |
37 | #[derive(Serialize, Deserialize, Debug)]
38 | pub struct RepoMetadata {
39 | pub repo_name: String,
40 | pub tag_name: String,
41 | #[serde(skip_serializing_if = "Option::is_none")]
42 | pub assets: Option>,
43 | #[serde(skip_serializing_if = "Option::is_none")]
44 | pub hashed: Option>,
45 | #[serde(skip_serializing_if = "Option::is_none")]
46 | pub patches: Option>,
47 | #[serde(skip_serializing_if = "Option::is_none")]
48 | pub installer: Option,
49 | #[serde(skip_serializing_if = "Option::is_none")]
50 | pub deletes: Option>,
51 | #[serde(skip_serializing_if = "Option::is_none")]
52 | pub packing_info: Option>>,
53 | }
54 |
--------------------------------------------------------------------------------
/src-tauri/src/builder/append.rs:
--------------------------------------------------------------------------------
1 | use tokio::io::AsyncSeekExt;
2 |
3 | use crate::{
4 | cli::AppendArgs,
5 | pack::{write_file, PackFile},
6 | };
7 |
8 | pub async fn append_cli(args: AppendArgs) {
9 | // files len should equals to names len, or names len should be 0
10 | if args.file.len() != args.name.len() && !args.name.is_empty() {
11 | panic!("Files length must equal to names length, or names length must be 0");
12 | }
13 | // open file as append mode
14 | let mut output = tokio::fs::OpenOptions::new()
15 | .append(true)
16 | .open(&args.output)
17 | .await
18 | .expect("Failed to open output file");
19 | // seek to the end of the file
20 | output
21 | .seek(std::io::SeekFrom::End(0))
22 | .await
23 | .expect("Failed to seek to the end of the file");
24 | // loop through input files, get corresponding name or dafault to the file name
25 | for (i, file) in args.file.iter().enumerate() {
26 | let name = if !args.name.is_empty() {
27 | &args.name[i]
28 | } else {
29 | file.file_name().and_then(|s| s.to_str()).unwrap()
30 | };
31 | let input_stream = tokio::fs::File::open(file)
32 | .await
33 | .expect("Failed to open input file");
34 | let input_length = input_stream
35 | .metadata()
36 | .await
37 | .expect("Failed to get input file metadata")
38 | .len();
39 | // write file to output
40 | write_file(
41 | &mut output,
42 | &mut PackFile {
43 | name: name.to_string(),
44 | data: Box::new(input_stream),
45 | size: input_length.try_into().expect("File size too large"),
46 | },
47 | )
48 | .await
49 | .expect("Failed to write file");
50 | println!("Appended file: {name} ({input_length} bytes)");
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/dir.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{Context, Result};
2 | use std::path::Path;
3 | use windows::{
4 | core::{GUID, PWSTR},
5 | Win32::{
6 | Foundation::HANDLE,
7 | UI::Shell::{
8 | FOLDERID_Desktop, FOLDERID_Documents, FOLDERID_Downloads, FOLDERID_LocalAppData,
9 | FOLDERID_LocalAppDataLow, FOLDERID_RoamingAppData, GetUserProfileDirectoryW,
10 | SHGetKnownFolderPath, KF_FLAG_DEFAULT,
11 | },
12 | },
13 | };
14 |
15 | pub fn get_dir(dir: &GUID) -> Result {
16 | let pwstr = unsafe {
17 | SHGetKnownFolderPath(dir, KF_FLAG_DEFAULT, None)
18 | .map(|pwstr| pwstr.to_string().context("INTERNAL_ERROR"))
19 | .context("GET_KNOWNFOLDER_ERR")??
20 | };
21 | Ok(pwstr)
22 | }
23 |
24 | pub fn get_userprofile() -> Result {
25 | let mut buffer = [0u16; 1024];
26 | let pwstr = PWSTR::from_raw(buffer.as_mut_ptr());
27 | let mut size = buffer.len() as u32;
28 | unsafe { GetUserProfileDirectoryW(HANDLE::default(), Some(pwstr), &mut size) }
29 | .context("GET_KNOWNFOLDER_ERR")?;
30 | Ok(unsafe { pwstr.to_string().context("INTERNAL_ERROR")? })
31 | }
32 |
33 | pub fn in_private_folder(path: &Path) -> bool {
34 | let path_ids = vec![
35 | FOLDERID_LocalAppData,
36 | FOLDERID_LocalAppDataLow,
37 | FOLDERID_RoamingAppData,
38 | FOLDERID_Desktop,
39 | FOLDERID_Documents,
40 | FOLDERID_Downloads,
41 | ];
42 | // first check userprofile
43 | let userprofile = get_userprofile();
44 | if let Ok(userprofile) = userprofile {
45 | if path.starts_with(userprofile) {
46 | return true;
47 | }
48 | }
49 | // then check known folders
50 | for id in path_ids {
51 | let known_folder = get_dir(&id);
52 | if let Ok(known_folder) = known_folder {
53 | if path.starts_with(known_folder) {
54 | return true;
55 | }
56 | }
57 | }
58 | false
59 | }
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kachina-installer",
3 | "private": true,
4 | "version": "0.1.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "tauri dev --exit-on-panic",
8 | "build": "cross-env STATIC_VCRUNTIME=false tauri build -- --target x86_64-win7-windows-msvc -Z build-std=std,panic_abort -Z build-std-features=\"optimize_for_size\" && cd src-tauri/target/x86_64-win7-windows-msvc/release && ren kachina-builder.exe kachina-builder-standalone.exe && del kachina-builder.exe && copy /b kachina-builder-standalone.exe+kachina-installer.exe kachina-builder.exe",
9 | "debug": "tauri build --debug && pnpm dev:uac",
10 | "preview": "rsbuild preview",
11 | "tauri": "tauri",
12 | "test:prepare": "cd tests && node prepare.mjs",
13 | "test:offline-install": "cd tests && node offline-install.mjs",
14 | "test:online-install": "cd tests && node online-install.mjs",
15 | "test:offline-update": "cd tests && node offline-update.mjs",
16 | "test:online-update": "cd tests && node online-update.mjs",
17 | "test:all": "npm run test:prepare && npm run test:offline-install && npm run test:online-install && npm run test:offline-update && npm run test:online-update"
18 | },
19 | "dependencies": {
20 | "@sentry/cli": "^2.46.0",
21 | "@tauri-apps/api": "^2.5.0",
22 | "async": "^3.2.6",
23 | "compare-versions": "^6.1.1",
24 | "dompurify": "^3.2.6",
25 | "uuid": "^11.1.0",
26 | "vue": "^3.5.16"
27 | },
28 | "devDependencies": {
29 | "@eslint/js": "^9.29.0",
30 | "@fullhuman/postcss-purgecss": "^7.0.2",
31 | "@rsbuild/core": "^1.5.10",
32 | "@rsbuild/plugin-vue": "^1.1.2",
33 | "@tauri-apps/cli": "^2.5.0",
34 | "@types/async": "^3.2.24",
35 | "@types/uuid": "^10.0.0",
36 | "compression-webpack-plugin": "^11.1.0",
37 | "cross-env": "^7.0.3",
38 | "eslint": "^9.29.0",
39 | "express": "^4.18.2",
40 | "fs-extra": "^11.2.0",
41 | "globals": "^15.15.0",
42 | "prettier": "^3.5.3",
43 | "typescript": "^5.8.3",
44 | "typescript-eslint": "^8.34.0",
45 | "zx": "^8.8.2"
46 | },
47 | "packageManager": "pnpm@10.17.0"
48 | }
49 |
--------------------------------------------------------------------------------
/tests/offline-install.mjs:
--------------------------------------------------------------------------------
1 | import { verifyFiles, cleanupTestDir, getTestDir, printLogFileIfExists, FLAGS } from './utils.mjs';
2 | import 'zx/globals';
3 | import { $, usePwsh } from 'zx';
4 | usePwsh();
5 |
6 | async function test() {
7 | const testDir = getTestDir('offline-install');
8 | const installerPath = './fixtures/test-app-v1.exe';
9 |
10 | console.log(chalk.blue('=== Offline Installation Test ==='));
11 | console.log(`Test directory: ${testDir}`);
12 | console.log(`Installer: ${installerPath}`);
13 |
14 | try {
15 | // 执行离线安装
16 | console.log('Running offline installation...');
17 | let result;
18 | try {
19 | result = await $`${installerPath} ${FLAGS} -D ${testDir}`.timeout('3m').quiet();
20 | } catch (error) {
21 | if (error.message && error.message.includes('timed out')) {
22 | console.error(chalk.red('Offline installation timed out after 3 minutes'));
23 | await printLogFileIfExists();
24 | }
25 | throw error;
26 | }
27 |
28 | if (result.exitCode !== 0) {
29 | throw new Error(`Installation failed with exit code ${result.exitCode}`);
30 | }
31 |
32 | // 验证安装的文件
33 | const expectedFiles = [
34 | { path: 'app.exe', contains: 'APP_V1' },
35 | { path: 'config.json', contains: '"version": "1.0.0"' },
36 | { path: 'readme.txt', contains: 'v1.0.0' },
37 | { path: 'data/assets.dat', size: 10240 },
38 | { path: 'updater.exe' }, // v1更新器
39 | ];
40 |
41 | console.log('Verifying installed files...');
42 | const verification = await verifyFiles(testDir, expectedFiles);
43 |
44 | // 输出结果
45 | if (verification.failed.length === 0) {
46 | console.log(chalk.green('✓ All files installed correctly'));
47 | console.log(chalk.gray(` Verified: ${verification.passed.join(', ')}`));
48 | } else {
49 | console.error(chalk.red('✗ Verification failed:'));
50 | verification.failed.forEach((msg) =>
51 | console.error(chalk.red(` - ${msg}`)),
52 | );
53 | process.exit(1);
54 | }
55 | } catch (error) {
56 | console.error(chalk.red('Test failed:'), error.message);
57 | process.exit(1);
58 | } finally {
59 | await cleanupTestDir(testDir);
60 | }
61 | }
62 |
63 | test();
64 |
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/wrapper.h:
--------------------------------------------------------------------------------
1 | #include "../hpatch-sys/HPatch/patch_types.h"
2 | #include
3 |
4 | typedef hpatch_TStreamOutput hdiff_TStreamOutput;
5 | typedef hpatch_TStreamInput hdiff_TStreamInput;
6 | // compress plugin
7 | typedef struct hdiff_TCompress {
8 | // return type tag; strlen(result)<=hpatch_kMaxPluginTypeLength; (Note:result lifetime)
9 | const char *(*compressType)(void); // ascii cstring,cannot contain '&'
10 | // return the max compressed size, if input dataSize data;
11 | hpatch_StreamPos_t (*maxCompressedSize)(hpatch_StreamPos_t dataSize);
12 | // return support threadNumber
13 | int (*setParallelThreadNumber)(struct hdiff_TCompress *compressPlugin, int threadNum);
14 | // compress data to out_code; return compressed size, if error or not need compress then return 0;
15 | // if out_code->write() return hdiff_stream_kCancelCompress(error) then return 0;
16 | // if memory I/O can use hdiff_compress_mem()
17 | hpatch_StreamPos_t (*compress)(const struct hdiff_TCompress *compressPlugin,
18 | const hpatch_TStreamOutput *out_code,
19 | const hpatch_TStreamInput *in_data);
20 | const char *(*compressTypeForDisplay)(void); // like compressType but just for display,can NULL
21 | } hdiff_TCompress;
22 |
23 | // create a diff data between oldData and newData, the diffData saved as single compressed stream
24 | // kMinSingleMatchScore: default 6, bin: 0--4 text: 4--9
25 | // patchStepMemSize>=hpatch_kStreamCacheSize, default 256k, recommended 64k,2m etc...
26 | // isUseBigCacheMatch: big cache max used O(oldSize) memory, match speed faster, but build big cache slow
27 | void create_single_compressed_diff(const unsigned char *newData, const unsigned char *newData_end,
28 | const unsigned char *oldData, const unsigned char *oldData_end,
29 | const hpatch_TStreamOutput *out_diff, const hdiff_TCompress *compressPlugin,
30 | int kMinSingleMatchScore,
31 | size_t patchStepMemSize,
32 | bool isUseBigCacheMatch,
33 | void *listener, size_t threadNum);
--------------------------------------------------------------------------------
/src-tauri/src/cli/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod arg;
2 | use arg::{Command, InstallArgs};
3 | use clap::Parser;
4 |
5 | use crate::{utils::url::HttpContextExt, REQUEST_CLIENT};
6 |
7 | #[derive(Parser)]
8 | #[command(args_conflicts_with_subcommands = true)]
9 | pub struct Cli {
10 | #[command(subcommand)]
11 | command: Option,
12 | #[clap(flatten)]
13 | pub install: InstallArgs,
14 | }
15 | impl Cli {
16 | pub fn command(&self) -> Command {
17 | self.command
18 | .clone()
19 | .unwrap_or(Command::Install(self.install.clone()))
20 | }
21 | }
22 |
23 | pub async fn install_webview2() {
24 | println!("安装程序缺少必要的运行环境");
25 | println!("当前系统未安装 WebView2 运行时,正在下载并安装...");
26 | // use reqwest to download the installer
27 | let wv2_url = "https://go.microsoft.com/fwlink/p/?LinkId=2124703";
28 | let res = REQUEST_CLIENT
29 | .get(wv2_url)
30 | .send()
31 | .await
32 | .with_http_context("install_webview2", wv2_url)
33 | .expect("Failed to download WebView2 installer");
34 | let wv2_installer_blob = res
35 | .bytes()
36 | .await
37 | .with_http_context("install_webview2", wv2_url)
38 | .expect("Failed to read WebView2 installer data");
39 | let temp_dir = std::env::temp_dir();
40 | let installer_path = temp_dir
41 | .as_path()
42 | .join("kachina.MicrosoftEdgeWebview2Setup.exe");
43 | tokio::fs::write(&installer_path, wv2_installer_blob)
44 | .await
45 | .expect("failed to write installer to temp dir");
46 | // run the installer
47 | let status = tokio::process::Command::new(installer_path.clone())
48 | .arg("/install")
49 | .status()
50 | .await
51 | .expect("failed to run installer");
52 | let _ = tokio::fs::remove_file(installer_path).await;
53 | if status.success() {
54 | println!("WebView2 运行时安装成功");
55 | println!("正在重新启动安装程序...");
56 | // exec self and detatch
57 | let _ = tokio::process::Command::new(std::env::current_exe().unwrap()).spawn();
58 | // delete the installer
59 | } else {
60 | println!("WebView2 运行时安装失败");
61 | println!("按任意键退出...");
62 | let _ = tokio::io::AsyncReadExt::read(&mut tokio::io::stdin(), &mut [0u8]).await;
63 | std::process::exit(0);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src-tauri/libs/hpatch-sys/HPatch/checksum_plugin.h:
--------------------------------------------------------------------------------
1 | //checksum_plugin.h
2 | // checksum plugin type
3 | /*
4 | The MIT License (MIT)
5 | Copyright (c) 2018-2019 HouSisong
6 |
7 | Permission is hereby granted, free of charge, to any person
8 | obtaining a copy of this software and associated documentation
9 | files (the "Software"), to deal in the Software without
10 | restriction, including without limitation the rights to use,
11 | copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the
13 | Software is furnished to do so, subject to the following
14 | conditions:
15 |
16 | The above copyright notice and this permission notice shall be
17 | included in all copies of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 | OTHER DEALINGS IN THE SOFTWARE.
27 | */
28 | #ifndef HPatch_checksum_plugin_h
29 | #define HPatch_checksum_plugin_h
30 | #include "patch_types.h"
31 | #ifdef __cplusplus
32 | extern "C" {
33 | #endif
34 |
35 | typedef void* hpatch_checksumHandle;
36 | typedef struct hpatch_TChecksum{
37 | //return type tag; strlen(result)<=hpatch_kMaxPluginTypeLength; (Note:result lifetime)
38 | const char* (*checksumType)(void); //ascii cstring,cannot contain '&'
39 | hpatch_size_t (*checksumByteSize)(void); //result<=hpatch_kStreamCacheSize
40 | hpatch_checksumHandle (*open)(struct hpatch_TChecksum* plugin);
41 | void (*close)(struct hpatch_TChecksum* plugin,hpatch_checksumHandle handle);
42 | void (*begin)(hpatch_checksumHandle handle);
43 | void (*append)(hpatch_checksumHandle handle,
44 | const unsigned char* part_data,const unsigned char* part_data_end);
45 | void (*end)(hpatch_checksumHandle handle,
46 | unsigned char* checksum,unsigned char* checksum_end);
47 | } hpatch_TChecksum;
48 |
49 | #ifdef __cplusplus
50 | }
51 | #endif
52 | #endif
53 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/uac.rs:
--------------------------------------------------------------------------------
1 | use std::ffi::{c_void, OsStr};
2 | use std::mem::{size_of, zeroed};
3 | use std::ptr::null_mut;
4 | use windows::core::{w, HSTRING, PCWSTR};
5 | use windows::Win32::Foundation::HANDLE;
6 | use windows::Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY};
7 | use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
8 | use windows::Win32::UI::Shell::{
9 | ShellExecuteExW, SEE_MASK_NOASYNC, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW,
10 | };
11 | #[derive(Debug)]
12 | pub struct SendableHandle(pub HANDLE);
13 | unsafe impl Send for SendableHandle {}
14 | unsafe impl Sync for SendableHandle {}
15 |
16 | pub fn check_elevated() -> windows::core::Result {
17 | unsafe {
18 | let h_process = GetCurrentProcess();
19 | let mut h_token = HANDLE(null_mut());
20 | let open_result = OpenProcessToken(h_process, TOKEN_QUERY, &mut h_token);
21 | let mut ret_len: u32 = 0;
22 | let mut token_info: TOKEN_ELEVATION = zeroed();
23 |
24 | if let Err(e) = open_result {
25 | println!("OpenProcessToken {e:?}");
26 | return Err(e);
27 | }
28 |
29 | if let Err(e) = GetTokenInformation(
30 | h_token,
31 | TokenElevation,
32 | Some(std::ptr::addr_of_mut!(token_info).cast::()),
33 | size_of::() as u32,
34 | &mut ret_len,
35 | ) {
36 | println!("GetTokenInformation {e:?}");
37 |
38 | return Err(e);
39 | }
40 |
41 | Ok(token_info.TokenIsElevated != 0)
42 | }
43 | }
44 |
45 | pub fn run_elevated, T: AsRef>(
46 | program_path: S,
47 | args: T,
48 | ) -> std::io::Result {
49 | let file = HSTRING::from(program_path.as_ref());
50 | let par = HSTRING::from(args.as_ref());
51 |
52 | let mut sei = SHELLEXECUTEINFOW {
53 | cbSize: std::mem::size_of::() as u32,
54 | fMask: SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS,
55 | lpVerb: w!("runas"),
56 | lpFile: PCWSTR(file.as_ptr()),
57 | lpParameters: PCWSTR(par.as_ptr()),
58 | nShow: 1,
59 | ..Default::default()
60 | };
61 | unsafe {
62 | ShellExecuteExW(&mut sei)?;
63 | let process = { sei.hProcess };
64 | if process.is_invalid() {
65 | return Err(std::io::Error::last_os_error());
66 | };
67 | Ok(SendableHandle(process))
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/plugins/index.ts:
--------------------------------------------------------------------------------
1 | import type { KachinaInstallSource } from './types';
2 |
3 | export class PluginManager {
4 | private plugins: KachinaInstallSource[] = [];
5 |
6 | register(plugin: KachinaInstallSource): void {
7 | this.plugins.push(plugin);
8 | }
9 |
10 | private parseUrl(url: string): { cleanUrl: string | null; forcedPlugin: string | null } {
11 | // 检查是否包含dfs+或dfs2+,如果有则不匹配插件
12 | if (url.includes('dfs+') || url.includes('dfs2+')) {
13 | return { cleanUrl: null, forcedPlugin: null };
14 | }
15 |
16 | // 找到://前的内容进行分析
17 | const protocolIndex = url.indexOf('://');
18 | if (protocolIndex === -1) return { cleanUrl: url, forcedPlugin: null };
19 |
20 | const beforeProtocol = url.substring(0, protocolIndex);
21 | const afterProtocol = url.substring(protocolIndex);
22 |
23 | // 检查是否有plugin-强制指定格式
24 | const pluginMatch = beforeProtocol.match(/plugin-([^+]+)\+(.*)$/);
25 | if (pluginMatch) {
26 | const [, pluginName, remainingPrefix] = pluginMatch;
27 | // 重新组装URL,移除plugin-xxx+部分
28 | const cleanUrl = remainingPrefix ? `${remainingPrefix}${afterProtocol}` : `https${afterProtocol}`;
29 | return { cleanUrl, forcedPlugin: pluginName };
30 | }
31 |
32 | // 普通处理:找到最后一个+,过滤前面的内容
33 | const lastPlusIndex = beforeProtocol.lastIndexOf('+');
34 | const cleanUrl = lastPlusIndex === -1
35 | ? url
36 | : beforeProtocol.substring(lastPlusIndex + 1) + afterProtocol;
37 |
38 | return { cleanUrl, forcedPlugin: null };
39 | }
40 |
41 | private extractCleanUrl(url: string): string | null {
42 | return this.parseUrl(url).cleanUrl;
43 | }
44 |
45 | findPlugin(url: string): KachinaInstallSource | null {
46 | const { cleanUrl, forcedPlugin } = this.parseUrl(url);
47 | if (!cleanUrl) return null;
48 |
49 | // 如果指定了强制插件,直接按名称查找
50 | if (forcedPlugin) {
51 | const plugin = this.plugins.find(p => p.name === forcedPlugin);
52 | if (!plugin) {
53 | throw new Error(`Plugin "${forcedPlugin}" not found`);
54 | }
55 | return plugin;
56 | }
57 |
58 | // 否则按URL匹配
59 | return this.plugins.find(plugin => plugin.matchUrl(cleanUrl)) || null;
60 | }
61 |
62 | isPluginSource(url: string): boolean {
63 | return this.findPlugin(url) !== null;
64 | }
65 |
66 | getCleanUrl(url: string): string | null {
67 | return this.extractCleanUrl(url);
68 | }
69 | }
70 |
71 | export const pluginManager = new PluginManager();
72 |
73 | // 导出类型供其他模块使用
74 | export type { KachinaInstallSource } from './types';
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![allow(non_upper_case_globals)]
2 | #![allow(non_camel_case_types)]
3 | #![allow(non_snake_case)]
4 |
5 | use std::ffi::c_void;
6 |
7 | include!("../binding.rs");
8 |
9 | trait WriteSeek: std::io::Write + std::io::Seek {}
10 |
11 | impl WriteSeek for T {}
12 |
13 | struct WriteStreamWrapper<'a> {
14 | stream: &'a mut dyn WriteSeek,
15 | }
16 |
17 | extern "C" fn write_seek_callback(
18 | stream: *const hpatch_TStreamOutput,
19 | write_to: u64,
20 | out_data: *const u8,
21 | out_data_end: *const u8,
22 | ) -> i32 {
23 | let write_size = unsafe { out_data_end.offset_from(out_data) };
24 | let stream: &hpatch_TStreamOutput = unsafe { &*stream };
25 | let input_wrapper = unsafe { &mut *(stream.streamImport as *mut WriteStreamWrapper) };
26 | // seek
27 | if let Err(err) = input_wrapper
28 | .stream
29 | .seek(std::io::SeekFrom::Start(write_to))
30 | {
31 | println!("Error in read_seek: {:?}", err);
32 | return 0;
33 | }
34 | // buffer: out_data to out_data_end
35 | let buffer = unsafe { std::slice::from_raw_parts(out_data, write_size as usize) };
36 | // read exact, return 0 if failed
37 | let res = input_wrapper.stream.write_all(buffer);
38 | if let Err(err) = res {
39 | println!("Error in write_seq_callback: {:?}", err);
40 | return 0;
41 | }
42 | write_size as i32
43 | }
44 |
45 | pub fn safe_create_single_patch(
46 | new_data: &[u8],
47 | old_data: &[u8],
48 | mut output: impl std::io::Write + std::io::Seek,
49 | level: u8,
50 | ) -> Result<(), String> {
51 | let new_start_ptr = new_data.as_ptr();
52 | let new_end_ptr = unsafe { new_start_ptr.add(new_data.len()) };
53 | let old_start_ptr = old_data.as_ptr();
54 | let old_end_ptr = unsafe { old_start_ptr.add(old_data.len()) };
55 | let mut output_wrapper = WriteStreamWrapper {
56 | stream: &mut output,
57 | };
58 | let mut stream_output = hpatch_TStreamOutput {
59 | // 1G
60 | streamSize: 1 << 30,
61 | streamImport: &mut output_wrapper as *mut WriteStreamWrapper as *mut c_void,
62 | write: Some(write_seek_callback),
63 | read_writed: None,
64 | };
65 | unsafe {
66 | create_single_compressed_diff(
67 | new_start_ptr,
68 | new_end_ptr,
69 | old_start_ptr,
70 | old_end_ptr,
71 | &mut stream_output,
72 | std::ptr::null_mut(),
73 | level as i32,
74 | 1024 * 256,
75 | true,
76 | std::ptr::null_mut(),
77 | 1,
78 | );
79 | }
80 | Ok(())
81 | }
82 |
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/HDiff/private_diff/libdivsufsort/config.h:
--------------------------------------------------------------------------------
1 | /*
2 | * config.h for libdivsufsort
3 | * Copyright (c) 2003-2008 Yuta Mori All Rights Reserved.
4 | *
5 | * Permission is hereby granted, free of charge, to any person
6 | * obtaining a copy of this software and associated documentation
7 | * files (the "Software"), to deal in the Software without
8 | * restriction, including without limitation the rights to use,
9 | * copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the
11 | * Software is furnished to do so, subject to the following
12 | * conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be
15 | * included in all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | * OTHER DEALINGS IN THE SOFTWARE.
25 | */
26 |
27 | #ifndef _CONFIG_H
28 | #define _CONFIG_H 1
29 |
30 | #ifdef __cplusplus
31 | extern "C" {
32 | #endif /* __cplusplus */
33 |
34 | /** Define to the version of this package. **/
35 | #define PROJECT_VERSION_FULL "2.0.1-14-g5f60d6f"
36 |
37 | /** Define to 1 if you have the header files. **/
38 | #define HAVE_INTTYPES_H 0
39 | #define HAVE_STDDEF_H 1
40 | #if defined (__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */)
41 | # define HAVE_STDINT_H 1
42 | #else
43 | # define HAVE_STDINT_H 0
44 | #endif
45 | #define HAVE_STDLIB_H 1
46 | #define HAVE_STRING_H 1
47 | #define HAVE_STRINGS_H 0
48 | #define HAVE_MEMORY_H 1
49 | #define HAVE_SYS_TYPES_H 0
50 |
51 | /** for WinIO **/
52 | /* #undef HAVE_IO_H */
53 | /* #undef HAVE_FCNTL_H */
54 | /* #undef HAVE__SETMODE */
55 | /* #undef HAVE_SETMODE */
56 | /* #undef HAVE__FILENO */
57 | /* #undef HAVE_FOPEN_S */
58 | /* #undef HAVE__O_BINARY */
59 | #ifndef HAVE__SETMODE
60 | # if HAVE_SETMODE
61 | # define _setmode setmode
62 | # define HAVE__SETMODE 1
63 | # endif
64 | # if HAVE__SETMODE && !HAVE__O_BINARY
65 | # define _O_BINARY 0
66 | # define HAVE__O_BINARY 1
67 | # endif
68 | #endif
69 |
70 | /** for inline **/
71 | #ifndef INLINE
72 | # ifdef _MSC_VER
73 | # define INLINE __inline
74 | # else
75 | # define INLINE inline
76 | # endif
77 | #endif
78 |
79 | /** for VC++ warning **/
80 | #ifdef _MSC_VER
81 | #pragma warning(disable: 4127)
82 | #endif
83 |
84 |
85 | #ifdef __cplusplus
86 | } /* extern "C" */
87 | #endif /* __cplusplus */
88 |
89 | #endif /* _CONFIG_H */
90 |
--------------------------------------------------------------------------------
/src-tauri/src/builder/cli/mod.rs:
--------------------------------------------------------------------------------
1 | #[path = "../../cli/arg.rs"]
2 | pub mod arg;
3 |
4 | use std::path::PathBuf;
5 |
6 | use clap::{Parser, Subcommand};
7 |
8 | #[derive(Debug, Clone, clap::Args)]
9 | pub struct PackArgs {
10 | #[clap(long, short = 'o', default_value = "output.exe")]
11 | pub output: PathBuf,
12 | #[clap(long, short = 'c', default_value = ".config.json")]
13 | pub config: PathBuf,
14 | #[clap(long, short = 't')]
15 | pub image: Option,
16 | #[clap(long, short = 'm')]
17 | pub metadata: Option,
18 | #[clap(long, short = 'd')]
19 | pub data_dir: Option,
20 | #[clap(long)]
21 | pub icon: Option,
22 | }
23 |
24 | #[derive(Debug, Clone, clap::Args)]
25 | pub struct GenArgs {
26 | #[clap(long, short = 'i')]
27 | pub input_dir: PathBuf,
28 | #[clap(long, short = 'm')]
29 | pub output_metadata: PathBuf,
30 | #[clap(long, short = 'o')]
31 | pub output_dir: PathBuf,
32 | #[clap(long, short = 'r')]
33 | pub repo: String,
34 | #[clap(long, short = 't')]
35 | pub tag: String,
36 | #[clap(long, short = 'd')]
37 | pub diff_vers: Option>,
38 | #[clap(long, short = 'x')]
39 | pub diff_ignore: Option>,
40 | #[clap(long, short = 'u')]
41 | pub updater: Option,
42 | #[clap(long, short = 'p')]
43 | pub updater_name: Option,
44 | #[clap(long, short = 'j', default_value = "2")]
45 | pub zstd_concurrency: usize,
46 | }
47 |
48 | #[derive(Debug, Clone, clap::Args)]
49 | pub struct AppendArgs {
50 | #[clap(long, short = 'o', default_value = "output.exe")]
51 | pub output: PathBuf,
52 | #[clap(long, short = 'f')]
53 | pub file: Vec,
54 | #[clap(long, short = 'n')]
55 | pub name: Vec,
56 | }
57 |
58 | #[derive(Debug, Clone, clap::Args)]
59 | pub struct ExtractArgs {
60 | #[clap(long, short = 'i', default_value = "output.exe")]
61 | pub input: PathBuf,
62 |
63 | // 原有参数保持不变
64 | #[clap(long, short = 'f')]
65 | pub file: Vec,
66 | #[clap(long, short = 'n')]
67 | pub name: Vec,
68 |
69 | // 新增参数
70 | #[clap(long)]
71 | pub meta_name: Vec,
72 | #[clap(long)]
73 | pub all: Option,
74 | #[clap(long)]
75 | pub list: bool,
76 | }
77 |
78 | #[derive(Debug, Clone, clap::Args)]
79 | pub struct ReplaceBinArgs {
80 | /// 输入的安装包文件
81 | pub input: PathBuf,
82 | /// 输出的新安装包文件
83 | #[clap(long, short = 'o')]
84 | pub output: PathBuf,
85 | }
86 |
87 | #[derive(Subcommand, Clone, Debug)]
88 | pub enum Command {
89 | Pack(PackArgs),
90 | Append(AppendArgs),
91 | Extract(ExtractArgs),
92 | Gen(GenArgs),
93 | ReplaceBin(ReplaceBinArgs),
94 | }
95 |
96 | #[derive(Parser)]
97 | #[command(args_conflicts_with_subcommands = true, arg_required_else_help = true)]
98 | pub struct Cli {
99 | #[command(subcommand)]
100 | pub command: Option,
101 | }
102 |
--------------------------------------------------------------------------------
/tests/online-install.mjs:
--------------------------------------------------------------------------------
1 | import {
2 | verifyFiles,
3 | cleanupTestDir,
4 | getTestDir,
5 | waitForServer,
6 | printLogFileIfExists,
7 | FLAGS,
8 | } from './utils.mjs';
9 | import { startServer } from './server.mjs';
10 | import 'zx/globals';
11 | import { $, usePwsh } from 'zx';
12 | usePwsh();
13 |
14 | async function test() {
15 | const testDir = getTestDir('online-install');
16 | const installerPath = './fixtures/test-app-v1.exe';
17 |
18 | console.log(chalk.blue('=== Online Installation Test ==='));
19 | console.log(`Test directory: ${testDir}`);
20 |
21 | // 启动HTTP服务器
22 | console.log('Starting HTTP server...');
23 | const server = await startServer();
24 |
25 | try {
26 | // 等待服务器启动
27 | await waitForServer('http://localhost:8080/test-app-v1.exe');
28 |
29 | // 删除日志文件 %temp%/KachinaInstaller.log
30 | const logFile = os.tmpdir() + '/KachinaInstaller.log';
31 | if (await fs.pathExists(logFile)) {
32 | await fs.remove(logFile);
33 | }
34 |
35 | // 执行在线安装
36 | console.log('Running online installation...');
37 | let result;
38 | try {
39 | result =
40 | await $`${installerPath} ${FLAGS} -O -D ${testDir} --source local-v1`.timeout('3m').quiet();
41 | } catch (error) {
42 | if (error.message && error.message.includes('timed out')) {
43 | console.error(chalk.red('Installation process timed out after 3 minutes'));
44 | await printLogFileIfExists();
45 | }
46 | throw error;
47 | }
48 |
49 | if (result.exitCode !== 0) {
50 | throw new Error(`Installation failed with exit code ${result.exitCode}`);
51 | }
52 |
53 | // check if fail in logs
54 | if (await fs.pathExists(logFile)) {
55 | const logs = await fs.readFile(logFile, 'utf-8');
56 | console.log(logs);
57 | // 验证日志文件是否有错误
58 | if (logs.includes('ERROR kachina_installer::installer')) {
59 | throw new Error('Updater log contains errors');
60 | }
61 | }
62 |
63 | // 验证安装的文件
64 | const expectedFiles = [
65 | { path: 'app.exe', contains: 'APP_V1' },
66 | { path: 'config.json', contains: '"version": "1.0.0"' },
67 | { path: 'readme.txt', contains: 'v1.0.0' },
68 | { path: 'data/assets.dat', size: 10240 },
69 | { path: 'updater.exe' },
70 | ];
71 |
72 | console.log('Verifying installed files...');
73 | const verification = await verifyFiles(testDir, expectedFiles);
74 |
75 | if (verification.failed.length === 0) {
76 | console.log(chalk.green('✓ All files installed correctly via HTTP'));
77 | console.log(chalk.gray(` Verified: ${verification.passed.join(', ')}`));
78 | } else {
79 | console.error(chalk.red('✗ Verification failed:'));
80 | verification.failed.forEach((msg) =>
81 | console.error(chalk.red(` - ${msg}`)),
82 | );
83 | process.exit(1);
84 | }
85 | } catch (error) {
86 | console.error(chalk.red('Test failed:'), error.message);
87 | process.exit(1);
88 | } finally {
89 | // 停止服务器
90 | server?.close();
91 | await cleanupTestDir(testDir);
92 | }
93 | }
94 |
95 | test();
96 |
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/HDiff/private_diff/libdivsufsort/divsufsort.h:
--------------------------------------------------------------------------------
1 | /*
2 | * divsufsort.h for libdivsufsort
3 | * Copyright (c) 2003-2008 Yuta Mori All Rights Reserved.
4 | *
5 | * Permission is hereby granted, free of charge, to any person
6 | * obtaining a copy of this software and associated documentation
7 | * files (the "Software"), to deal in the Software without
8 | * restriction, including without limitation the rights to use,
9 | * copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the
11 | * Software is furnished to do so, subject to the following
12 | * conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be
15 | * included in all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | * OTHER DEALINGS IN THE SOFTWARE.
25 | */
26 |
27 | #ifndef _DIVSUFSORT_H
28 | #define _DIVSUFSORT_H 1
29 |
30 | #if defined (__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */)
31 | # include //for uint8_t,int32_t
32 | #else
33 | # if (_MSC_VER >= 1300)
34 | typedef unsigned __int8 uint8_t;
35 | typedef signed __int32 int32_t;
36 | # else
37 | typedef unsigned char uint8_t;
38 | typedef signed int int32_t;
39 | # endif
40 | #endif
41 |
42 | #ifdef __cplusplus
43 | extern "C" {
44 | #endif /* __cplusplus */
45 |
46 | #ifndef PRId32
47 | # define PRId32 "d"
48 | #endif
49 |
50 | #ifndef DIVSUFSORT_API
51 | # ifdef DIVSUFSORT_BUILD_DLL
52 | # define DIVSUFSORT_API
53 | # else
54 | # define DIVSUFSORT_API
55 | # endif
56 | #endif
57 |
58 | /*- Datatypes -*/
59 | #ifndef SAUCHAR_T
60 | #define SAUCHAR_T
61 | typedef uint8_t sauchar_t;
62 | #endif /* SAUCHAR_T */
63 | #ifndef SAINT_T
64 | #define SAINT_T
65 | typedef int32_t saint_t;
66 | #endif /* SAINT_T */
67 | #ifndef SAIDX32_T
68 | #define SAIDX32_T
69 | typedef int32_t saidx32_t;
70 | #endif /* SAIDX32_T */
71 |
72 | /*- Prototypes -*/
73 |
74 | /**
75 | * Constructs the suffix array of a given string.
76 | * @param T[0..n-1] The input string.
77 | * @param SA[0..n-1] The output array of suffixes.
78 | * @param n The length of the given string.
79 | * @return 0 if no error occurred, -1 or -2 otherwise.
80 | */
81 | DIVSUFSORT_API
82 | saint_t
83 | divsufsort(const sauchar_t *T,saidx32_t *SA,saidx32_t n,int threadNum);
84 |
85 | /**
86 | * Returns the version of the divsufsort library.
87 | * @return The version number string.
88 | */
89 | DIVSUFSORT_API
90 | const char *
91 | divsufsort_version(void);
92 |
93 | #ifdef __cplusplus
94 | } /* extern "C" */
95 | #endif /* __cplusplus */
96 |
97 | #endif /* _DIVSUFSORT_H */
98 |
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/HDiff/private_diff/mem_buf.h:
--------------------------------------------------------------------------------
1 | // mem_buf.h
2 | //
3 | /*
4 | The MIT License (MIT)
5 | Copyright (c) 2012-2019 HouSisong
6 |
7 | Permission is hereby granted, free of charge, to any person
8 | obtaining a copy of this software and associated documentation
9 | files (the "Software"), to deal in the Software without
10 | restriction, including without limitation the rights to use,
11 | copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the
13 | Software is furnished to do so, subject to the following
14 | conditions:
15 |
16 | The above copyright notice and this permission notice shall be
17 | included in all copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 | OTHER DEALINGS IN THE SOFTWARE.
27 | */
28 |
29 | #ifndef __mem_buf_h
30 | #define __mem_buf_h
31 | #include //malloc free
32 | #include //size_t
33 | #include //std::runtime_error
34 | #include
35 | namespace hdiff_private{
36 |
37 | struct TAutoMem{
38 | inline explicit TAutoMem(size_t size=0) :_data(0),_data_end(0),_capacity_end(0){ realloc(size); }
39 | inline ~TAutoMem(){ clear(); }
40 | inline unsigned char* data(){ return _data; }
41 | inline const unsigned char* data()const{ return _data; }
42 | inline unsigned char* data_end(){ return _data_end; }
43 | inline const unsigned char* data_end()const{ return _data_end; }
44 | inline size_t size()const{ return (size_t)(_data_end-_data); }
45 | inline size_t capacity()const{ return (size_t)(_capacity_end-_data); }
46 | inline bool empty()const{ return (_data_end==_data); }
47 | inline void clear(){ if (_data) { free(_data); _data=0; _data_end=0; _capacity_end=0; } }
48 | inline void realloc(size_t newSize){
49 | if (newSize<=capacity()){
50 | _data_end=_data+newSize;
51 | }else{
52 | unsigned char* _new_data=(unsigned char*)::realloc(_data,newSize);
53 | if (_new_data==0) throw std::runtime_error("TAutoMem::TAutoMem() realloc() error!");
54 | _data=_new_data;
55 | _data_end=_new_data+newSize;
56 | _capacity_end=_data_end;
57 | }
58 | }
59 | inline void reduceSize(size_t reserveSize){
60 | if (reserveSize<=capacity())
61 | _data_end=_data+reserveSize;
62 | else
63 | throw std::runtime_error("TAutoMem::reduceSize() error!");
64 | }
65 | private:
66 | unsigned char* _data;
67 | unsigned char* _data_end;
68 | unsigned char* _capacity_end;
69 | };
70 |
71 | }//namespace hdiff_private
72 | #endif //__mem_buf_h
73 |
--------------------------------------------------------------------------------
/src-tauri/libs/hdiff-sys/HDiff/private_diff/compress_detect.h:
--------------------------------------------------------------------------------
1 | //compress_detect.h
2 | //粗略估算数据的可压缩性 for diff.
3 | /*
4 | The MIT License (MIT)
5 | Copyright (c) 2012-2017 HouSisong
6 |
7 | Permission is hereby granted, free of charge, to any person
8 | obtaining a copy of this software and associated documentation
9 | files (the "Software"), to deal in the Software without
10 | restriction, including without limitation the rights to use,
11 | copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the
13 | Software is furnished to do so, subject to the following
14 | conditions:
15 |
16 | The above copyright notice and this permission notice shall be
17 | included in all copies of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 | OTHER DEALINGS IN THE SOFTWARE.
27 | */
28 | #ifdef _MSC_VER
29 | #pragma warning( disable : 4706)
30 | #endif
31 |
32 | #ifndef compress_detect_h
33 | #define compress_detect_h
34 | #include //for size_t
35 | #include "../../../hpatch-sys/HPatch/patch_types.h" //for hpatch_uint32_t
36 | #include "mem_buf.h"
37 | namespace hdiff_private{
38 |
39 | template
40 | static unsigned int _getUIntCost(_UInt v){
41 | if ((sizeof(_UInt)<8)||((v>>28)>>28)==0) {
42 | int cost=1;
43 | _UInt t;
44 | if ((t=(v>>28))) { v=t; cost+=4; }
45 | if ((t=(v>>14))) { v=t; cost+=2; }
46 | if ((t=(v>> 7))) { v=t; ++cost; }
47 | return cost;
48 | }else{
49 | return 9;
50 | }
51 | }
52 |
53 | template
54 | inline static unsigned _getIntCost(_Int v){
55 | return _getUIntCost((_UInt)(2*((v>=0)?(_UInt)v:(_UInt)(-v))));
56 | }
57 |
58 | //粗略估算该区域存储成本.
59 | size_t getRegionRleCost(const unsigned char* d,size_t n,const unsigned char* sub=0,
60 | unsigned char* out_nocompress=0,size_t* nocompressSize=0);
61 |
62 | class TCompressDetect{
63 | public:
64 | TCompressDetect();
65 | ~TCompressDetect();
66 | void add_chars(const unsigned char* d,size_t n,const unsigned char* sub=0);
67 | size_t cost(const unsigned char* d,size_t n,const unsigned char* sub=0)const;
68 | private:
69 | struct TCharConvTable{
70 | hpatch_uint32_t sum;
71 | hpatch_uint32_t sum1char[256];
72 | hpatch_uint32_t sum2char[256*256];//用相邻字符转换几率来近似估计数据的可压缩性.
73 | unsigned char cache[1];//实际大小为kCacheSize,超出该距离的旧数据会被清除.
74 | };
75 | TAutoMem m_mem;
76 | TCharConvTable* m_table;
77 | int m_lastChar;
78 | int m_lastPopChar;
79 | hpatch_uint32_t m_cacheBegin;
80 | hpatch_uint32_t m_cacheEnd;
81 | void clear();
82 | void _add_rle(const unsigned char* d,size_t n);
83 | size_t _cost_rle(const unsigned char* d,size_t n)const;
84 | };
85 |
86 | }//namespace hdiff_private
87 |
88 | #endif
89 |
--------------------------------------------------------------------------------
/src/mirrorc-errors.ts:
--------------------------------------------------------------------------------
1 | import { error } from './api/ipc';
2 |
3 | /**
4 | * Mirror酱错误码对应表
5 | */
6 | export interface MirrorcErrorInfo {
7 | code: number;
8 | message: string;
9 | showSourceDialog?: boolean;
10 | }
11 |
12 | export const MIRRORC_ERROR_CODES: Record = {
13 | 1001: {
14 | code: 1001,
15 | message: 'Mirror酱参数错误,请检查打包配置',
16 | },
17 | 7001: {
18 | code: 7001,
19 | message: 'Mirror酱 CDK 已过期',
20 | showSourceDialog: true,
21 | },
22 | 7002: {
23 | code: 7002,
24 | message: 'Mirror酱 CDK 错误,请检查设置的 CDK 是否正确',
25 | showSourceDialog: true,
26 | },
27 | 7003: {
28 | code: 7003,
29 | message: 'Mirror酱 CDK 今日下载次数已达上限,请更换 CDK 或明天再试',
30 | },
31 | 7004: {
32 | code: 7004,
33 | message: 'Mirror酱 CDK 类型和待下载的资源不匹配,请检查设置的 CDK 是否正确',
34 | showSourceDialog: true,
35 | },
36 | 7005: {
37 | code: 7005,
38 | message: 'Mirror酱 CDK 已被封禁,请更换 CDK',
39 | showSourceDialog: true,
40 | },
41 | 8001: {
42 | code: 8001,
43 | message: '从Mirror酱获取更新失败,请检查打包配置',
44 | },
45 | 8002: {
46 | code: 8002,
47 | message: 'Mirror酱参数错误,请检查打包配置',
48 | },
49 | 8003: {
50 | code: 8003,
51 | message: 'Mirror酱参数错误,请检查打包配置',
52 | },
53 | 8004: {
54 | code: 8004,
55 | message: 'Mirror酱参数错误,请检查打包配置',
56 | },
57 | };
58 |
59 | /**
60 | * 获取Mirror酱错误信息
61 | * @param code 错误码
62 | * @returns 错误信息,如果不是已知错误码则返回null
63 | */
64 | export function getMirrorcErrorInfo(code: number): MirrorcErrorInfo | null {
65 | return MIRRORC_ERROR_CODES[code] || null;
66 | }
67 |
68 | /**
69 | * 处理Mirror酱错误并记录日志
70 | * @param mirrorcStatus Mirror酱状态响应
71 | * @param contextType 错误上下文类型(用于日志区分)
72 | * @returns 处理后的错误信息
73 | */
74 | export function processMirrorcError(
75 | mirrorcStatus: { code: number; msg?: string },
76 | contextType: 'install' | 'cdk-validation' = 'install'
77 | ): {
78 | isError: boolean;
79 | errorInfo: MirrorcErrorInfo;
80 | message: string;
81 | showSourceDialog: boolean;
82 | } | null {
83 | if (mirrorcStatus.code === 0) {
84 | return null;
85 | }
86 |
87 | const errorInfo = getMirrorcErrorInfo(mirrorcStatus.code);
88 |
89 | if (errorInfo) {
90 | // 记录已知错误码
91 | error(`Mirror酱${contextType === 'cdk-validation' ? 'CDK验证' : ''}错误 [${mirrorcStatus.code}]: ${errorInfo.message}`);
92 |
93 | return {
94 | isError: true,
95 | errorInfo,
96 | message: errorInfo.message,
97 | showSourceDialog: errorInfo.showSourceDialog || false
98 | };
99 | } else {
100 | // 处理未知错误码
101 | const unknownMessage = contextType === 'cdk-validation'
102 | ? `从Mirror酱获取CDK状态失败: ${mirrorcStatus.msg || '未知错误'},请联系Mirror酱客服`
103 | : `从Mirror酱获取更新失败: ${mirrorcStatus.msg || '未知错误'},请联系Mirror酱客服`;
104 |
105 | // 记录未知错误码
106 | error(`Mirror酱${contextType === 'cdk-validation' ? 'CDK验证' : ''}未知错误 [${mirrorcStatus.code}]: ${mirrorcStatus.msg || '无详细信息'}`);
107 |
108 | return {
109 | isError: true,
110 | errorInfo: {
111 | code: mirrorcStatus.code,
112 | message: unknownMessage
113 | },
114 | message: unknownMessage,
115 | showSourceDialog: false
116 | };
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/utils/svgSanitizer.ts:
--------------------------------------------------------------------------------
1 | import DOMPurify from 'dompurify';
2 |
3 | // 安全的CSS属性白名单(SVG图形属性 + 基础布局样式)
4 | const SAFE_CSS_PROPERTIES = [
5 | // SVG专用属性
6 | 'fill', 'stroke', 'stroke-width', 'stroke-dasharray', 'stroke-dashoffset',
7 | 'opacity', 'fill-opacity', 'stroke-opacity', 'visibility',
8 | 'transform', 'transform-origin', 'clip-path', 'mask',
9 |
10 | // 基础布局和间距
11 | 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
12 | 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
13 | 'width', 'height', 'max-width', 'max-height', 'min-width', 'min-height',
14 |
15 | // 显示和定位(限制范围)
16 | 'display', 'overflow', 'box-sizing',
17 |
18 | // 颜色和文本
19 | 'color', 'background-color', 'border-color',
20 | 'font-size', 'font-weight', 'font-family', 'text-align',
21 |
22 | // 边框
23 | 'border', 'border-width', 'border-style', 'border-radius',
24 | 'border-top', 'border-right', 'border-bottom', 'border-left'
25 | ];
26 |
27 | function sanitizeCssStyle(styleValue: string): string {
28 | if (!styleValue) return '';
29 |
30 | // 移除危险的CSS函数和关键字
31 | const dangerousPatterns = [
32 | /javascript:/gi,
33 | /expression\s*\(/gi,
34 | /url\s*\(/gi,
35 | /import/gi,
36 | /@/gi, // 移除CSS at-rules
37 | /behaviour:/gi,
38 | /-moz-binding:/gi
39 | ];
40 |
41 | for (const pattern of dangerousPatterns) {
42 | if (pattern.test(styleValue)) {
43 | return ''; // 发现危险内容,返回空字符串
44 | }
45 | }
46 |
47 | // 解析CSS属性并过滤
48 | const declarations = styleValue.split(';')
49 | .map(decl => decl.trim())
50 | .filter(decl => decl)
51 | .filter(decl => {
52 | const [property] = decl.split(':').map(part => part.trim());
53 | return SAFE_CSS_PROPERTIES.includes(property.toLowerCase());
54 | });
55 |
56 | return declarations.join('; ');
57 | }
58 |
59 | export function sanitizeSvg(svgContent: string): string | null {
60 | if (
61 | !svgContent?.trim().startsWith('