diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..259b00c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,39 @@
+name: Go Release
+
+on:
+ push:
+ # 创建 tag 时
+ tags:
+ - v*
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ # 检出代码
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Install upx
+ run: sudo apt-get install -y upx
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: 'stable'
+
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v6
+ with:
+ # either 'goreleaser' (default) or 'goreleaser-pro'
+ distribution: goreleaser
+ # 'latest', 'nightly', or a semver
+ version: 'latest'
+ args: release --clean
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7ed0965
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,203 @@
+### GoLand+all template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Go template
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+out/*
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+
+### GoLand template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+
+# AWS User-specific
+
+# Generated files
+
+# Sensitive or high-churn files
+
+# Gradle
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+
+# Mongo Explorer plugin
+
+# File-based project format
+
+# IntelliJ
+
+# mpeltonen/sbt-idea plugin
+
+# JIRA plugin
+
+# Cursive Clojure plugin
+
+# SonarLint plugin
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+
+# Editor-based Rest Client
+
+# Android studio 3.1+ serialized cache file
+
+### GoLand+iml template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+
+# AWS User-specific
+
+# Generated files
+
+# Sensitive or high-churn files
+
+# Gradle
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+
+# Mongo Explorer plugin
+
+# File-based project format
+
+# IntelliJ
+
+# mpeltonen/sbt-idea plugin
+
+# JIRA plugin
+
+# Cursive Clojure plugin
+
+# SonarLint plugin
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+
+# Editor-based Rest Client
+
+# Android studio 3.1+ serialized cache file
+
+
+dist/
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
new file mode 100644
index 0000000..4585796
--- /dev/null
+++ b/.goreleaser.yaml
@@ -0,0 +1,38 @@
+# This is an example .goreleaser.yml file with some sensible defaults.
+# Make sure to check the documentation at https://goreleaser.com
+
+# The lines below are called `modelines`. See `:help modeline`
+# Feel free to remove those if you don't want/need to use them.
+# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
+# vim: set ts=2 sw=2 tw=0 fo=cnqoj
+
+version: 2
+
+before:
+ hooks:
+ # You may remove this if you don't use go modules.
+ - go mod tidy
+
+upx:
+ - enabled: true
+ compress: best
+ lzma: true
+
+builds:
+ - env:
+ - CGO_ENABLED=0
+ goos:
+ - windows
+
+archives:
+ - format: binary
+
+changelog:
+ sort: asc
+ filters:
+ exclude:
+ - "^docs:"
+ - "^test:"
+
+checksum:
+ algorithm: md5
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..35410ca
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/JavaSceneConfigState.xml b/.idea/JavaSceneConfigState.xml
new file mode 100644
index 0000000..68a9d6e
--- /dev/null
+++ b/.idea/JavaSceneConfigState.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/KillWxapkg.iml b/.idea/KillWxapkg.iml
new file mode 100644
index 0000000..16dc49f
--- /dev/null
+++ b/.idea/KillWxapkg.iml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..8d66637
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..ec50427
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+ DOM 问题JavaScript 和 TypeScript
+
+
+ Go
+
+
+ HTTP 客户端
+
+
+ JavaScript 和 TypeScript
+
+
+ SQL
+
+
+ 代码样式问题Go
+
+
+ 可能的 bugGo
+
+
+ 常规JavaScript 和 TypeScript
+
+
+
+
+ 用户定义
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..5592cc7
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml
new file mode 100644
index 0000000..cb1a783
--- /dev/null
+++ b/.idea/watcherTasks.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5d22455
--- /dev/null
+++ b/README.md
@@ -0,0 +1,100 @@
+# KillWxapkg
+
+> 存Golang实现,一个用于自动化反编译微信小程序的工具,小程序安全利器,自动解密,解包,可最大程度还原工程目录
+
+[](https://github.com/Ackites/KillWxapkg)
+[]()
+[]()
+[]()
+[]()
+[]()
+[]()
+[]()
+[]()
+[]()
+[]()
+[]()
+------------------
+## 介绍
+
+- [x] 小程序自动解密
+- [x] 小程序自动解包,支持代码美化输出
+ - [x] Json美化
+ - [x] JavaScript美化
+ - [x] Html美化
+- [x] 支持还原源代码工程目录结构
+ - [x] Json配置文件还原
+ - [x] JavaScript代码还原
+ - [ ] Wxml代码还原
+ - [ ] Wxss代码还原
+- [ ] 转换 Uni-app 项目
+- [ ] 敏感数据导出
+
+### 工程结构还原
+
+#### 未还原
+
+
+#### 还原后
+
+
+## 安装
+
+- 下载最新版本的[release](https://github.com/Ackites/KillWxapkg/releases)包
+- 自行编译
+ ```shell
+ # 克隆项目
+ git clone https://github.com/Ackites/KillWxapkg.git
+
+ # 进入项目目录
+ cd ./KillWxapkg
+
+ # 下载依赖
+ go mod tidy
+
+ # 编译
+ go build
+ ```
+
+## 用法
+
+> 使用方法: program -id= -in=<输入文件1,输入文件2> 或 -in=<输入目录> -out=<输出目录>
+> [-restore] [-pretty] [-ext=<文件后缀>]
+
+### 参数说明
+- `-id string`
+ - 微信小程序的AppID
+ - 包已解密,可不指定
+ - 例:-id=wx7627e1630485288d
+- `-in string`
+ - 输入文件路径(多个文件用逗号分隔)或输入目录路径
+ - 自动检测,已解密的包,自动解包,未解密的包,自动解密后解包
+ - 解密后的包会保存到输入目录下以AppID命名的文件夹
+ - 例:-in=app.wxpkg,app1.wxapkg
+ - 例:-in=C:\Users\mi\Desktop\Applet\64
+- `-out string`
+ - 输出目录路径(如果未指定,则默认保存到输入目录下以AppID命名的文件夹)
+- `-restore`
+ - 是否还原源代码工程目录结构,默认不还原
+- `-pretty`
+ - 是否美化输出,默认不美化,美化需较长时间
+- `-ext string`
+ - 处理的文件后缀 (default ".wxapkg")
+ - 例:-ext=.wxapkg
+- `-help`
+ - 显示帮助信息
+
+### 获取微信小程序AppID
+
+
+
+#### 文件夹名即为AppID
+
+
+
+进入文件夹下,即可找到.wxapkg文件
+
+
+## Star History
+
+[](https://star-history.com/#Ackites/KillWxapkg&Date)
\ No newline at end of file
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 0000000..b4b7954
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,59 @@
+package cmd
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "sync"
+
+ . "github.com/Ackites/KillWxapkg/internal/cmd"
+ . "github.com/Ackites/KillWxapkg/internal/config"
+ "github.com/Ackites/KillWxapkg/internal/restore"
+)
+
+func Execute(appID, input, outputDir, fileExt string, restoreDir bool, pretty bool) {
+ if appID == "" || input == "" {
+ fmt.Println("使用方法: program -id= -in=<输入文件1,输入文件2> 或 -in=<输入目录> -out=<输出目录> [-ext=<文件后缀>] [-restore]")
+ flag.PrintDefaults()
+ return
+ }
+
+ // 存储配置
+ configManager := NewSharedConfigManager()
+ configManager.Set("appID", appID)
+ configManager.Set("input", input)
+ configManager.Set("outputDir", outputDir)
+ configManager.Set("fileExt", fileExt)
+ configManager.Set("restoreDir", restoreDir)
+ configManager.Set("pretty", pretty)
+
+ inputFiles := ParseInput(input, fileExt)
+
+ if len(inputFiles) == 0 {
+ log.Println("未找到任何文件")
+ return
+ }
+
+ // 确定输出目录
+ if outputDir == "" {
+ outputDir = DetermineOutputDir(input, appID)
+ }
+
+ var wg sync.WaitGroup
+ for _, inputFile := range inputFiles {
+ wg.Add(1)
+ go func(file string) {
+ defer wg.Done()
+ err := ProcessFile(file, outputDir, appID)
+ if err != nil {
+ log.Printf("处理文件 %s 时出错: %v\n", file, err)
+ } else {
+ log.Printf("成功处理文件: %s\n", file)
+ }
+ }(inputFile)
+ }
+ wg.Wait()
+
+ // 还原工程目录结构
+ restore.ProjectStructure(outputDir, restoreDir)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..b7e2479
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,19 @@
+module github.com/Ackites/KillWxapkg
+
+go 1.22
+
+require (
+ github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c
+ github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5
+ github.com/gorilla/css v1.0.1
+ github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4
+ golang.org/x/crypto v0.25.0
+ golang.org/x/net v0.21.0
+)
+
+require (
+ github.com/dlclark/regexp2 v1.7.0 // indirect
+ github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
+ github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
+ golang.org/x/text v0.16.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..e649aef
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,22 @@
+github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c h1:+Zo5Ca9GH0RoeVZQKzFJcTLoAixx5s5Gq3pTIS+n354=
+github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c/go.mod h1:HJGU9ULdREjOcVGZVPB5s6zYmHi1RxzT71l2wQyLmnE=
+github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
+github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5 h1:ZRqTaoW9WZ2DqeOQGhK9q73eCb47SEs30GV2IRHT9bo=
+github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5/go.mod h1:o31y53rb/qiIAONF7w3FHJZRqqP3fzHUr1HqanthByw=
+github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
+github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
+github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
+github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
+github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
+golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/images/img.png b/images/img.png
new file mode 100644
index 0000000..b306530
Binary files /dev/null and b/images/img.png differ
diff --git a/images/img1.png b/images/img1.png
new file mode 100644
index 0000000..907a323
Binary files /dev/null and b/images/img1.png differ
diff --git a/images/img2.png b/images/img2.png
new file mode 100644
index 0000000..4150c19
Binary files /dev/null and b/images/img2.png differ
diff --git a/images/img3.png b/images/img3.png
new file mode 100644
index 0000000..2386258
Binary files /dev/null and b/images/img3.png differ
diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go
new file mode 100644
index 0000000..8ae6671
--- /dev/null
+++ b/internal/cmd/cmd.go
@@ -0,0 +1,146 @@
+package cmd
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/Ackites/KillWxapkg/internal/decrypt"
+ "github.com/Ackites/KillWxapkg/internal/unpack"
+)
+
+// ParseInput 解析输入文件
+func ParseInput(input, fileExt string) []string {
+ var inputFiles []string
+ if fileInfo, err := os.Stat(input); err == nil && fileInfo.IsDir() {
+ files, err := os.ReadDir(input)
+ if err != nil {
+ log.Fatalf("读取输入目录失败: %v", err)
+ }
+ for _, file := range files {
+ if !file.IsDir() && strings.HasSuffix(file.Name(), fileExt) {
+ inputFiles = append(inputFiles, filepath.Join(input, file.Name()))
+ }
+ }
+ } else {
+ inputFiles = strings.Split(input, ",")
+ }
+
+ // 过滤掉不存在的文件
+ var validFiles []string
+ for _, file := range inputFiles {
+ if _, err := os.Stat(file); err == nil {
+ validFiles = append(validFiles, file)
+ }
+ }
+ return validFiles
+}
+
+// DetermineOutputDir 确定输出目录
+func DetermineOutputDir(input, appID string) string {
+ var baseDir string
+ if fileInfo, err := os.Stat(input); err == nil && fileInfo.IsDir() {
+ baseDir = input
+ } else {
+ baseDir = filepath.Dir(input)
+ }
+ return filepath.Join(baseDir, appID)
+}
+
+// ProcessFile 合并目录
+func ProcessFile(inputFile, outputDir, appID string) error {
+ log.Printf("开始处理文件: %s\n", inputFile)
+
+ // 确定解密后的文件路径
+ decryptedFilePath := filepath.Join(outputDir, filepath.Base(inputFile))
+
+ // 解密
+ decryptedData, err := decrypt.DecryptWxapkg(inputFile, appID)
+ if err != nil {
+ return fmt.Errorf("解密失败: %v", err)
+ }
+
+ // 保存解密后的文件
+ err = os.MkdirAll(outputDir, 0755)
+ if err != nil {
+ return fmt.Errorf("创建输出目录失败: %v", err)
+ }
+
+ err = os.WriteFile(decryptedFilePath, decryptedData, 0755)
+ if err != nil {
+ return fmt.Errorf("保存解密文件失败: %v", err)
+ }
+
+ log.Printf("文件解密并保存到: %s\n", decryptedFilePath)
+
+ // 解包到临时目录
+ tempDir, err := os.MkdirTemp("", "wxapkg")
+ if err != nil {
+ return fmt.Errorf("创建临时目录失败: %v", err)
+ }
+ defer func(path string) {
+ err := os.RemoveAll(path)
+ if err != nil {
+ log.Printf("删除临时目录 %s 失败: %v\n", path, err)
+ }
+ }(tempDir)
+
+ err = unpack.UnpackWxapkg(decryptedData, tempDir)
+ if err != nil {
+ return fmt.Errorf("解包失败: %v", err)
+ }
+
+ // 合并解包后的内容到输出目录
+ err = mergeDirs(tempDir, outputDir)
+ if err != nil {
+ return fmt.Errorf("合并目录失败: %v", err)
+ }
+
+ return nil
+}
+
+// mergeDirs 合并目录
+func mergeDirs(srcDir, dstDir string) error {
+ return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ relPath, err := filepath.Rel(srcDir, path)
+ if err != nil {
+ return err
+ }
+ targetPath := filepath.Join(dstDir, relPath)
+
+ if info.IsDir() {
+ return os.MkdirAll(targetPath, 0755)
+ }
+
+ srcFile, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer func(srcFile *os.File) {
+ err := srcFile.Close()
+ if err != nil {
+ log.Printf("关闭文件 %s 失败: %v\n", srcFile.Name(), err)
+ }
+ }(srcFile)
+
+ dstFile, err := os.Create(targetPath)
+ if err != nil {
+ return err
+ }
+ defer func(dstFile *os.File) {
+ err := dstFile.Close()
+ if err != nil {
+ log.Printf("关闭文件 %s 失败: %v\n", dstFile.Name(), err)
+ }
+ }(dstFile)
+
+ _, err = io.Copy(dstFile, srcFile)
+ return err
+ })
+}
diff --git a/internal/config/delete.go b/internal/config/delete.go
new file mode 100644
index 0000000..0fe0e7f
--- /dev/null
+++ b/internal/config/delete.go
@@ -0,0 +1,73 @@
+package config
+
+import (
+ "context"
+ "log"
+ "os"
+ "sync"
+)
+
+// FileDeletionManager 用于管理需要删除的文件列表
+type FileDeletionManager struct {
+ mu sync.Mutex
+ files map[string]bool
+ cancelFn context.CancelFunc
+ ctx context.Context
+}
+
+var deleteInstance *FileDeletionManager
+var deleteOnce sync.Once
+
+// NewFileDeletionManager 创建或获取一个单例的FileDeletionManager
+func NewFileDeletionManager() *FileDeletionManager {
+ deleteOnce.Do(func() {
+ c := context.Background()
+ ctx, cancel := context.WithCancel(c)
+ deleteInstance = &FileDeletionManager{
+ files: make(map[string]bool),
+ cancelFn: cancel,
+ ctx: ctx,
+ }
+ })
+ return deleteInstance
+}
+
+// AddFile 添加文件路径到删除列表
+func (f *FileDeletionManager) AddFile(filePath string) {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ f.files[filePath] = true
+}
+
+// DeleteFiles 删除所有在列表中的文件
+func (f *FileDeletionManager) DeleteFiles() {
+ f.mu.Lock()
+ files := make([]string, 0, len(f.files))
+ for file := range f.files {
+ files = append(files, file)
+ }
+ f.mu.Unlock()
+
+ for _, file := range files {
+ select {
+ case <-f.ctx.Done():
+ log.Println("文件删除操作已取消")
+ return
+ default:
+ err := os.Remove(file)
+ if err != nil {
+ log.Printf("删除文件 %s 失败: %v\n", file, err)
+ } else {
+ f.mu.Lock()
+ delete(f.files, file) // 删除成功后从列表中移除文件
+ f.mu.Unlock()
+ log.Printf("文件 %s 已成功删除", file)
+ }
+ }
+ }
+}
+
+// Cancel 取消删除操作
+func (f *FileDeletionManager) Cancel() {
+ f.cancelFn()
+}
diff --git a/internal/config/share.go b/internal/config/share.go
new file mode 100644
index 0000000..e654c10
--- /dev/null
+++ b/internal/config/share.go
@@ -0,0 +1,79 @@
+package config
+
+import (
+ "sync"
+)
+
+// SharedConfigManager 用于管理共享配置
+type SharedConfigManager struct {
+ mu sync.RWMutex
+ settings map[string]interface{}
+}
+
+var shareInstance *SharedConfigManager
+var shareOnce sync.Once
+
+// NewSharedConfigManager 创建一个新的SharedConfigManager
+func NewSharedConfigManager() *SharedConfigManager {
+ shareOnce.Do(func() {
+ shareInstance = &SharedConfigManager{
+ settings: make(map[string]interface{}),
+ }
+ })
+ return shareInstance
+}
+
+// Set 设置一个配置项的值
+func (scm *SharedConfigManager) Set(key string, value interface{}) {
+ scm.mu.Lock()
+ defer scm.mu.Unlock()
+ scm.settings[key] = value
+}
+
+// SetBulk 批量设置配置项的值
+func (scm *SharedConfigManager) SetBulk(configs map[string]interface{}) {
+ scm.mu.Lock()
+ defer scm.mu.Unlock()
+ for key, value := range configs {
+ scm.settings[key] = value
+ }
+}
+
+// Get 获取一个配置项的值
+func (scm *SharedConfigManager) Get(key string) (interface{}, bool) {
+ scm.mu.RLock()
+ defer scm.mu.RUnlock()
+ value, exists := scm.settings[key]
+ return value, exists
+}
+
+// GetBulk 批量获取配置项的值
+func (scm *SharedConfigManager) GetBulk(keys []string) map[string]interface{} {
+ scm.mu.RLock()
+ defer scm.mu.RUnlock()
+ results := make(map[string]interface{})
+ for _, key := range keys {
+ if value, exists := scm.settings[key]; exists {
+ results[key] = value
+ }
+ }
+ return results
+}
+
+// Delete 删除一个配置项
+func (scm *SharedConfigManager) Delete(key string) {
+ scm.mu.Lock()
+ defer scm.mu.Unlock()
+ delete(scm.settings, key)
+}
+
+// GetAll 返回所有配置项的副本
+func (scm *SharedConfigManager) GetAll() map[string]interface{} {
+ scm.mu.RLock()
+ defer scm.mu.RUnlock()
+ c := make(map[string]interface{})
+ for key, value := range scm.settings {
+ c[key] = value
+ }
+ return c
+}
diff --git a/internal/decrypt/decrypt.go b/internal/decrypt/decrypt.go
new file mode 100644
index 0000000..c476271
--- /dev/null
+++ b/internal/decrypt/decrypt.go
@@ -0,0 +1,73 @@
+package decrypt
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/sha1"
+ "encoding/binary"
+ "fmt"
+ "os"
+
+ "golang.org/x/crypto/pbkdf2"
+)
+
+const (
+ saltStr = "saltiest"
+ ivStr = "the iv: 16 bytes"
+ fileHeader = "V1MMWX"
+ defaultXorKey = 0x66
+)
+
+func DecryptWxapkg(inputFile, appID string) ([]byte, error) {
+ ciphertext, err := os.ReadFile(inputFile)
+ if err != nil {
+ return nil, fmt.Errorf("读取文件失败: %v", err)
+ }
+
+ // 先检查是否已解密
+ reader := bytes.NewReader(ciphertext)
+ var firstMark byte
+ binary.Read(reader, binary.BigEndian, &firstMark)
+ var info1, indexInfoLength, bodyInfoLength uint32
+ binary.Read(reader, binary.BigEndian, &info1)
+ binary.Read(reader, binary.BigEndian, &indexInfoLength)
+ binary.Read(reader, binary.BigEndian, &bodyInfoLength)
+ var lastMark byte
+ binary.Read(reader, binary.BigEndian, &lastMark)
+ if firstMark == 0xBE && lastMark == 0xED {
+ // 已解密直接返回
+ return ciphertext, nil
+ }
+
+ if string(ciphertext[:len(fileHeader)]) != fileHeader {
+ return nil, fmt.Errorf("无效的文件格式")
+ }
+
+ key := pbkdf2.Key([]byte(appID), []byte(saltStr), 1000, 32, sha1.New)
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, fmt.Errorf("创建AES密码块失败: %v", err)
+ }
+
+ iv := []byte(ivStr)
+ mode := cipher.NewCBCDecrypter(block, iv)
+ originData := make([]byte, 1024)
+ mode.CryptBlocks(originData, ciphertext[6:1024+6])
+
+ afData := make([]byte, len(ciphertext)-1024-6)
+ var xorKey byte
+ if len(appID) >= 2 {
+ xorKey = appID[len(appID)-2]
+ } else {
+ xorKey = defaultXorKey
+ }
+ for i, b := range ciphertext[1024+6:] {
+ afData[i] = b ^ xorKey
+ }
+
+ originData = append(originData[:1023], afData...)
+
+ return originData, nil
+}
diff --git a/internal/restore/javascript.go b/internal/restore/javascript.go
new file mode 100644
index 0000000..5aa286c
--- /dev/null
+++ b/internal/restore/javascript.go
@@ -0,0 +1,32 @@
+package restore
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/Ackites/KillWxapkg/internal/unpack"
+)
+
+// ProcessJavaScriptFiles 分割JavaScript文件
+func ProcessJavaScriptFiles(configFile, outputDir string) error {
+ content, err := os.ReadFile(configFile)
+ if err != nil {
+ return fmt.Errorf("读取配置文件失败: %v", err)
+ }
+
+ var config struct {
+ SubPackages []unpack.SubPackage `json:"subPackages"`
+ }
+ err = json.Unmarshal(content, &config)
+ if err != nil {
+ return fmt.Errorf("解析配置文件失败: %v", err)
+ }
+
+ err = unpack.ProcessJavaScriptFiles(outputDir, config)
+ if err != nil {
+ return fmt.Errorf("处理JavaScript文件失败: %v", err)
+ }
+
+ return nil
+}
diff --git a/internal/restore/restore.go b/internal/restore/restore.go
new file mode 100644
index 0000000..613cab3
--- /dev/null
+++ b/internal/restore/restore.go
@@ -0,0 +1,45 @@
+package restore
+
+import (
+ "log"
+ "path/filepath"
+
+ "github.com/Ackites/KillWxapkg/internal/config"
+ "github.com/Ackites/KillWxapkg/internal/unpack"
+)
+
+// ProjectStructure 是否还原工程目录结构
+func ProjectStructure(outputDir string, restoreDir bool) {
+ if !restoreDir {
+ return
+ }
+
+ // 创建文件删除管理器
+ manager := config.NewFileDeletionManager()
+
+ // 配置文件还原
+ configFile := filepath.Join(outputDir, "app-config.json")
+ err := unpack.ProcessConfigFiles(configFile)
+ if err != nil {
+ log.Printf("还原工程目录结构失败: %v\n", err)
+ } else {
+ manager.AddFile(configFile)
+ }
+
+ // JavaScript文件还原
+ err = ProcessJavaScriptFiles(configFile, outputDir)
+ if err != nil {
+ log.Printf("处理JavaScript文件失败: %v\n", err)
+ }
+
+ // WXSS文件还原
+ //var config unpack.AppConfig
+ //content, err := os.ReadFile(configFile)
+ //if err == nil {
+ // _ = json.Unmarshal(content, &config)
+ //}
+ //ProcessWxssFiles(outputDir, config)
+
+ // 执行删除文件操作
+ manager.DeleteFiles()
+}
diff --git a/internal/restore/wxss.go b/internal/restore/wxss.go
new file mode 100644
index 0000000..d267b74
--- /dev/null
+++ b/internal/restore/wxss.go
@@ -0,0 +1,19 @@
+package restore
+
+import (
+ "path/filepath"
+
+ "github.com/Ackites/KillWxapkg/internal/unpack"
+)
+
+// ProcessWxssFiles 分割 WXSS 文件
+func ProcessWxssFiles(outputDir string, config unpack.AppConfig) {
+ // 处理主包的 WXSS 文件
+ unpack.ProcessXssFiles(outputDir, "")
+
+ // 处理子包的 WXSS 文件
+ for _, subPackage := range config.SubPackages {
+ subPackageDir := filepath.Join(outputDir, subPackage.Root)
+ unpack.ProcessXssFiles(subPackageDir, outputDir)
+ }
+}
diff --git a/internal/unpack/fileMagicNumbers.go b/internal/unpack/fileMagicNumbers.go
new file mode 100644
index 0000000..5b77ccc
--- /dev/null
+++ b/internal/unpack/fileMagicNumbers.go
@@ -0,0 +1,94 @@
+package unpack
+
+// FileMagicNumbers 文件类型及其对应的魔数(文件头标识)
+var FileMagicNumbers = map[string][]byte{
+ // 图像文件
+ ".png": {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'},
+ ".jpg": {0xFF, 0xD8, 0xFF},
+ ".jpeg": {0xFF, 0xD8, 0xFF},
+ ".gif": {'G', 'I', 'F', '8'},
+ ".bmp": {'B', 'M'},
+ ".webp": {'R', 'I', 'F', 'F'},
+ ".tiff": {0x49, 0x49, 0x2A, 0x00},
+ ".cr2": {0x49, 0x49, 0x2A, 0x00, 0x10, 0x00, 0x00, 0x00, 0x43, 0x52},
+ ".ico": {0x00, 0x00, 0x01, 0x00},
+ ".heic": {'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'},
+
+ // 文档文件
+ ".pdf": {'%', 'P', 'D', 'F', '-'},
+ ".doc": {0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1},
+ ".docx": {'P', 'K', 0x03, 0x04},
+ ".xls": {0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1},
+ ".xlsx": {'P', 'K', 0x03, 0x04},
+ ".ppt": {0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1},
+ ".pptx": {'P', 'K', 0x03, 0x04},
+ ".odt": {'P', 'K', 0x03, 0x04},
+ ".ods": {'P', 'K', 0x03, 0x04},
+ ".odp": {'P', 'K', 0x03, 0x04},
+ ".rtf": {'{', '\\', 'r', 't', 'f', '1'},
+ ".epub": {'P', 'K', 0x03, 0x04},
+ ".mobi": {'M', 'O', 'B', 'I'},
+
+ // 压缩文件
+ ".zip": {'P', 'K', 0x03, 0x04},
+ ".rar": {'R', 'a', 'r', '!', 0x1A, 0x07, 0x00},
+ ".7z": {'7', 'z', 0xBC, 0xAF, 0x27, 0x1C},
+ ".gz": {0x1F, 0x8B, 0x08},
+ ".tar": {'u', 's', 't', 'a', 'r'},
+ ".bz2": {'B', 'Z', 'h'},
+ ".xz": {0xFD, '7', 'z', 'X', 'Z', 0x00},
+
+ // 音频文件
+ ".mp3": {0xFF, 0xFB},
+ ".wav": {'R', 'I', 'F', 'F'},
+ ".ogg": {'O', 'g', 'g', 'S'},
+ ".flac": {'f', 'L', 'a', 'C'},
+ ".aac": {0xFF, 0xF1},
+ ".m4a": {'f', 't', 'y', 'p', 'M', '4', 'A', ' '},
+ ".mid": {'M', 'T', 'h', 'd'},
+ ".aiff": {'F', 'O', 'R', 'M'},
+
+ // 视频文件
+ ".mp4": {0x00, 0x00, 0x00, 0x18, 'f', 't', 'y', 'p'},
+ ".avi": {'R', 'I', 'F', 'F'},
+ ".mkv": {0x1A, 0x45, 0xDF, 0xA3},
+ ".flv": {'F', 'L', 'V'},
+ ".mov": {'f', 't', 'y', 'p', 'q', 't', ' ', ' '},
+ ".webm": {0x1A, 0x45, 0xDF, 0xA3},
+ ".mpg": {0x00, 0x00, 0x01, 0xBA},
+ ".wmv": {0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11},
+
+ // 可执行文件和库文件
+ ".exe": {'M', 'Z'},
+ ".elf": {0x7F, 'E', 'L', 'F'},
+ ".so": {0x7F, 'E', 'L', 'F'},
+ ".dll": {'M', 'Z'},
+ ".app": {0xCA, 0xFE, 0xBA, 0xBE}, // Mach-O 文件格式 (macOS)
+
+ // 脚本和标记语言文件
+ ".json": {'{'}, // 假设 JSON 文件以 { 开头
+ ".xml": {'<', '?', 'x', 'm', 'l'},
+ ".html": {'<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E'},
+
+ // 字体文件
+ ".ttf": {0x00, 0x01, 0x00, 0x00, 0x00},
+ ".otf": {'O', 'T', 'T', 'O'},
+ ".woff": {'w', 'O', 'F', 'F'},
+ ".woff2": {'w', 'O', 'F', '2'},
+
+ // 数据库文件
+ ".sqlite": {'S', 'Q', 'L', 'i', 't', 'e', ' ', 'f', 'o', 'r', 'm', 'a', 't', ' ', '3', 0x00},
+ ".db": {0x00, 0x06, 0x15, 0x61, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00},
+
+ // 其他
+ ".swf": {'F', 'W', 'S'},
+ ".class": {0xCA, 0xFE, 0xBA, 0xBE},
+ ".psd": {'8', 'B', 'P', 'S'},
+ ".torrent": {'d', '8', ':', 'a', 'n', 'n', 'o', 'u', 'n', 'c', 'e'},
+ ".blend": {'B', 'L', 'E', 'N', 'D', 'E', 'R'},
+ ".pcap": {0xD4, 0xC3, 0xB2, 0xA1},
+ ".dwg": {0x41, 0x43, 0x31, 0x30},
+ ".iso": {0x43, 0x44, 0x30, 0x30, 0x31},
+ ".vsd": {0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1},
+ ".mdb": {0x00, 0x01, 0x00, 0x00, 0x53, 0x74, 0x61, 0x6E, 0x64, 0x61, 0x72, 0x64, 0x20, 0x4A, 0x65, 0x74},
+}
diff --git a/internal/unpack/formatter/formatter.go b/internal/unpack/formatter/formatter.go
new file mode 100644
index 0000000..5a623a4
--- /dev/null
+++ b/internal/unpack/formatter/formatter.go
@@ -0,0 +1,38 @@
+package formatter
+
+import (
+ "fmt"
+ "strings"
+
+ . "github.com/Ackites/KillWxapkg/internal/config"
+)
+
+// Formatter 是一个文件格式化器接口
+type Formatter interface {
+ Format([]byte) ([]byte, error)
+}
+
+// 注册所有格式化器
+var formatters = map[string]Formatter{}
+
+// RegisterFormatter 注册文件扩展名对应的格式化器
+func RegisterFormatter(ext string, formatter Formatter) {
+ formatters[strings.ToLower(ext)] = formatter
+}
+
+// GetFormatter 返回文件扩展名对应的格式化器
+func GetFormatter(ext string) (Formatter, error) {
+ formatter, exists := formatters[strings.ToLower(ext)]
+ if !exists {
+ return nil, fmt.Errorf("不支持的文件类型: %s", ext)
+ }
+ configManager := NewSharedConfigManager()
+ if pretty, ok := configManager.Get("pretty"); ok {
+ if p, o := pretty.(bool); o {
+ if !p && ext == ".js" {
+ return nil, fmt.Errorf("不进行美化输出")
+ }
+ }
+ }
+ return formatter, nil
+}
diff --git a/internal/unpack/formatter/htmlformatter.go b/internal/unpack/formatter/htmlformatter.go
new file mode 100644
index 0000000..3aee7f8
--- /dev/null
+++ b/internal/unpack/formatter/htmlformatter.go
@@ -0,0 +1,69 @@
+package formatter
+
+import (
+ "bytes"
+ "regexp"
+ "strings"
+
+ "github.com/yosssi/gohtml"
+)
+
+// HTMLFormatter 结构体,用于格式化 HTML 代码
+type HTMLFormatter struct {
+ jsFormatter *JSFormatter
+}
+
+// 正则表达式用于匹配 HTML 中的 `)
+
+// NewHTMLFormatter 创建一个新的 HTMLFormatter 实例
+func NewHTMLFormatter() *HTMLFormatter {
+ return &HTMLFormatter{
+ jsFormatter: NewJSFormatter(),
+ }
+}
+
+// Format 方法用于格式化 HTML 代码
+// input: 原始的 HTML 代码字节切片
+// 返回值: 格式化后的 HTML 代码字节切片和错误信息(如果有)
+func (f *HTMLFormatter) Format(input []byte) ([]byte, error) {
+ // 使用 gohtml 库格式化 HTML 代码
+ data := gohtml.FormatBytes(bytes.TrimSpace(input))
+
+ // 替换