This commit is contained in:
Antkites 2024-07-25 13:25:19 +08:00
parent f3c5bd4a29
commit 2d11b8c569
39 changed files with 2798 additions and 0 deletions

39
.github/workflows/release.yml vendored Normal file
View File

@ -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 }}

203
.gitignore vendored Normal file
View File

@ -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/

38
.goreleaser.yaml Normal file
View File

@ -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

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

6
.idea/JavaSceneConfigState.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SmartInputSourceJavaSceneConfigState">
<option name="customChineseScenes" value="{&quot;capsLockState&quot;:false,&quot;code&quot;:&quot;;Printf(format)&quot;,&quot;enable&quot;:true,&quot;languageType&quot;:&quot;CHINESE&quot;,&quot;name&quot;:&quot;自定义中文切换&quot;,&quot;tip&quot;:&quot;&quot;}" />
</component>
</project>

11
.idea/KillWxapkg.iml generated Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/out" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,5 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
</profile>
</component>

41
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">
<profile-state>
<expanded-state>
<State />
<State>
<id>DOM 问题JavaScript 和 TypeScript</id>
</State>
<State>
<id>Go</id>
</State>
<State>
<id>HTTP 客户端</id>
</State>
<State>
<id>JavaScript 和 TypeScript</id>
</State>
<State>
<id>SQL</id>
</State>
<State>
<id>代码样式问题Go</id>
</State>
<State>
<id>可能的 bugGo</id>
</State>
<State>
<id>常规JavaScript 和 TypeScript</id>
</State>
</expanded-state>
<selected-state>
<State>
<id>用户定义</id>
</State>
</selected-state>
</profile-state>
</entry>
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/KillWxapkg.iml" filepath="$PROJECT_DIR$/.idea/KillWxapkg.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

8
.idea/watcherTasks.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<enabled-global>
<option value="goimports" />
</enabled-global>
</component>
</project>

100
README.md Normal file
View File

@ -0,0 +1,100 @@
# KillWxapkg
> 存Golang实现一个用于自动化反编译微信小程序的工具小程序安全利器自动解密解包可最大程度还原工程目录
[![stable](https://img.shields.io/badge/stable-stable-green.svg)](https://github.com/Ackites/KillWxapkg)
[![license](https://img.shields.io/github/license/Ackites/KillWxapkg)]()
[![download](https://img.shields.io/github/downloads/Ackites/KillWxapkg/total)]()
[![release](https://img.shields.io/github/v/release/Ackites/KillWxapkg)]()
[![commit](https://img.shields.io/github/last-commit/Ackites/KillWxapkg)]()
[![issues](https://img.shields.io/github/issues/Ackites/KillWxapkg)]()
[![pull](https://img.shields.io/github/issues-pr/Ackites/KillWxapkg)]()
[![fork](https://img.shields.io/github/forks/Ackites/KillWxapkg)]()
[![star](https://img.shields.io/github/stars/Ackites/KillWxapkg)]()
[![go](https://img.shields.io/github/go-mod/go-version/Ackites/KillWxapkg)]()
[![size](https://img.shields.io/github/repo-size/Ackites/KillWxapkg)]()
[![contributors](https://img.shields.io/github/contributors/Ackites/KillWxapkg)]()
------------------
## 介绍
- [x] 小程序自动解密
- [x] 小程序自动解包,支持代码美化输出
- [x] Json美化
- [x] JavaScript美化
- [x] Html美化
- [x] 支持还原源代码工程目录结构
- [x] Json配置文件还原
- [x] JavaScript代码还原
- [ ] Wxml代码还原
- [ ] Wxss代码还原
- [ ] 转换 Uni-app 项目
- [ ] 敏感数据导出
### 工程结构还原
#### 未还原
<img src="./images/img.png" width="50%">
#### 还原后
<img src="./images/img1.png" width="50%">
## 安装
- 下载最新版本的[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=<AppID> -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
<img src="./images/img2.png" width="70%">
#### 文件夹名即为AppID
<img src="./images/img3.png" width="70%">
进入文件夹下,即可找到.wxapkg文件
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=Ackites/KillWxapkg&type=Date)](https://star-history.com/#Ackites/KillWxapkg&Date)

59
cmd/root.go Normal file
View File

@ -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=<AppID> -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)
}

19
go.mod Normal file
View File

@ -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
)

22
go.sum Normal file
View File

@ -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=

BIN
images/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
images/img1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
images/img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
images/img3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

146
internal/cmd/cmd.go Normal file
View File

@ -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
})
}

73
internal/config/delete.go Normal file
View File

@ -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()
}

79
internal/config/share.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

19
internal/restore/wxss.go Normal file
View File

@ -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)
}
}

View File

@ -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},
}

View File

@ -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
}

View File

@ -0,0 +1,69 @@
package formatter
import (
"bytes"
"regexp"
"strings"
"github.com/yosssi/gohtml"
)
// HTMLFormatter 结构体,用于格式化 HTML 代码
type HTMLFormatter struct {
jsFormatter *JSFormatter
}
// 正则表达式用于匹配 HTML 中的 <script> 标签及其内容
var regScriptInHtml = regexp.MustCompile(`(?s) *<script.*?>(.*?)</script>`)
// 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))
// 替换 <script> 标签中的 JavaScript 代码
data = regScriptInHtml.ReplaceAllFunc(data, func(script []byte) []byte {
// 计算 <script> 标签前的空格数量
space := countLeadingSpaces(script)
// 提取 <script> 标签中的 JavaScript 代码
jsCode := regScriptInHtml.FindSubmatch(script)[1]
jsStr := strings.Repeat(" ", space+2) + string(bytes.TrimSpace(jsCode))
// 使用 jsFormatter 格式化 JavaScript 代码
beautifiedCode, err := f.jsFormatter.Format([]byte(jsStr))
if err == nil {
// 替换原始 JavaScript 代码为格式化后的代码,保持缩进一致性
return bytes.Replace(script, jsCode, []byte("\n"+string(beautifiedCode)+"\n"+strings.Repeat(" ", space)), 1)
}
return script
})
return data, nil
}
// countLeadingSpaces 函数用于计算字节切片前导空格的数量
func countLeadingSpaces(data []byte) int {
result := 0
for _, c := range data {
if c == ' ' {
result++
} else {
break
}
}
return result
}
func init() {
RegisterFormatter(".html", NewHTMLFormatter())
}

View File

@ -0,0 +1,35 @@
package formatter
import (
"bytes"
"github.com/ditashi/jsbeautifier-go/jsbeautifier"
)
// JSFormatter 结构体,用于格式化 JavaScript 代码
type JSFormatter struct{}
// NewJSFormatter 创建一个新的 JSFormatter 实例
func NewJSFormatter() *JSFormatter {
return &JSFormatter{}
}
// Format 方法用于格式化 JavaScript 代码
// input: 原始的 JavaScript 代码字节切片
// 返回值: 格式化后的 JavaScript 代码字节切片和错误信息(如果有)
func (f *JSFormatter) Format(input []byte) ([]byte, error) {
// 将输入数据转换为字符串,并去除前后空白
code := string(bytes.TrimSpace(input))
// 使用 jsbeautifier 库格式化 JavaScript 代码
beautifiedCode, err := jsbeautifier.Beautify(&code, jsbeautifier.DefaultOptions())
if err != nil {
return input, err
}
return []byte(beautifiedCode), nil
}
func init() {
RegisterFormatter(".js", NewJSFormatter())
}

View File

@ -0,0 +1,26 @@
package formatter
import (
"bytes"
"encoding/json"
)
// JSONFormatter 格式化 JSON 文件
type JSONFormatter struct{}
func NewJSONFormatter() *JSONFormatter {
return &JSONFormatter{}
}
func (f *JSONFormatter) Format(input []byte) ([]byte, error) {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, input, "", " ")
if err != nil {
return nil, err
}
return prettyJSON.Bytes(), nil
}
func init() {
RegisterFormatter(".json", NewJSONFormatter())
}

419
internal/unpack/uconfig.go Normal file
View File

@ -0,0 +1,419 @@
package unpack
import (
"bytes"
"crypto/md5"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/dop251/goja"
)
// PageConfig 存储页面配置
type PageConfig struct {
Window map[string]interface{} `json:"window,omitempty"`
}
// AppConfig 存储应用配置
type AppConfig struct {
Pages []string `json:"pages"`
Window map[string]interface{} `json:"window,omitempty"`
TabBar map[string]interface{} `json:"tabBar,omitempty"`
NetworkTimeout map[string]interface{} `json:"networkTimeout,omitempty"`
SubPackages []SubPackage `json:"subPackages,omitempty"`
NavigateToMiniProgramAppIdList []string `json:"navigateToMiniProgramAppIdList,omitempty"`
Workers string `json:"workers,omitempty"`
Debug bool `json:"debug,omitempty"`
}
// SubPackage 存储子包配置
type SubPackage struct {
Root string `json:"root"`
Pages []string `json:"pages"`
}
// getWorkerPath 解析 workers.js 并返回公共路径
func getWorkerPath(name string) string {
// 读取 workers.js 文件内容
code, err := os.ReadFile(name)
if err != nil {
panic(err)
}
// 使用 goja 创建 JavaScript VM
vm := goja.New()
_, err = vm.RunString(string(code))
if err != nil {
return ""
}
// 初始化公共路径
commPath := ""
err = vm.Set("define", func(call goja.FunctionCall) goja.Value {
name := call.Argument(0).String()
name = filepath.Dir(name) + "/"
if commPath == "" {
commPath = name
} else {
commPath = commonDir(commPath, name)
}
return goja.Undefined()
})
if err != nil {
return ""
}
_, err = vm.RunString(string(code))
if err != nil {
return ""
}
// 去掉最后一个字符
if len(commPath) > 0 {
commPath = commPath[:len(commPath)-1]
}
log.Printf("Worker path: \"" + commPath + "\"")
return commPath
}
// commonDir 找到两个路径的公共目录
func commonDir(path1, path2 string) string {
i := 0
for i < len(path1) && i < len(path2) && path1[i] == path2[i] {
i++
}
return path1[:i]
}
// changeExt 更改文件扩展名
func changeExt(filename, newExt string) string {
ext := filepath.Ext(filename)
return filename[:len(filename)-len(ext)] + newExt
}
// save 保存内容到文件
func save(filename string, content []byte) error {
// 判断目录是否存在
dir := filepath.Dir(filename)
if _, err := os.Stat(dir); os.IsNotExist(err) {
err := os.MkdirAll(dir, 0755)
if err != nil {
return fmt.Errorf("unable to create directory %s: %v", dir, err)
}
}
err := os.WriteFile(filename, content, 0755)
if err != nil {
return fmt.Errorf("unable to save file %s: %v", filename, err)
}
return nil
}
// ProcessConfigFiles 解析和处理配置文件
func ProcessConfigFiles(configFile string) error {
dir := filepath.Dir(configFile)
content, err := os.ReadFile(configFile)
if err != nil {
return err
}
// 定义结构体以解析 JSON 内容
var e struct {
Pages []string `json:"pages"`
EntryPagePath string `json:"entryPagePath"`
Global map[string]interface{} `json:"global"`
TabBar map[string]interface{} `json:"tabBar"`
NetworkTimeout map[string]interface{} `json:"networkTimeout"`
SubPackages []SubPackage `json:"subPackages"`
NavigateToMiniProgramAppIdList []string `json:"navigateToMiniProgramAppIdList"`
ExtAppid string `json:"extAppid"`
Ext map[string]interface{} `json:"ext"`
Debug bool `json:"debug"`
Page map[string]PageConfig `json:"page"`
}
err = json.Unmarshal(content, &e)
if err != nil {
return err
}
// 处理页面路径,将 entryPagePath 放在首位
k := e.Pages
entryIndex := indexOf(k, changeExt(e.EntryPagePath, ""))
k = append(k[:entryIndex], k[entryIndex+1:]...)
k = append([]string{changeExt(e.EntryPagePath, "")}, k...)
// 构建应用配置
app := AppConfig{
Pages: k,
Window: e.Global["window"].(map[string]interface{}),
TabBar: e.TabBar,
NetworkTimeout: e.NetworkTimeout,
}
// 处理子包
if len(e.SubPackages) > 0 {
var subPackages []SubPackage
for _, subPackage := range e.SubPackages {
root := subPackage.Root
if !strings.HasSuffix(root, "/") {
root += "/"
}
root = strings.TrimPrefix(root, "/")
var newPages []string
for i := 0; i < len(app.Pages); {
page := app.Pages[i]
if strings.HasPrefix(page, root) {
newPage := strings.TrimPrefix(page, root)
newPages = append(newPages, newPage)
app.Pages = append(app.Pages[:i], app.Pages[i+1:]...)
} else {
i++
}
}
subPackage.Root = root
subPackage.Pages = newPages
subPackages = append(subPackages, subPackage)
}
app.SubPackages = subPackages
fmt.Printf("=======================================================\n这个小程序采用了分包\n子包个数为: %d\n=======================================================\n", len(app.SubPackages))
}
// 处理 navigateToMiniProgramAppIdList
if len(e.NavigateToMiniProgramAppIdList) > 0 {
app.NavigateToMiniProgramAppIdList = e.NavigateToMiniProgramAppIdList
}
// 处理 workers.js
if fileExists(filepath.Join(dir, "workers.js")) {
app.Workers = getWorkerPath(filepath.Join(dir, "workers.js"))
}
// 处理 extAppid
if len(e.ExtAppid) > 0 {
extContent, _ := json.MarshalIndent(map[string]interface{}{
"extEnable": true,
"extAppid": e.ExtAppid,
"ext": e.Ext,
}, "", " ")
err := save(filepath.Join(dir, "ext.json"), extContent)
if err != nil {
return err
}
}
// 处理调试模式
if e.Debug {
app.Debug = e.Debug
}
// 处理页面中的组件路径
cur := "./file"
for a, page := range e.Page {
if page.Window != nil && page.Window["usingComponents"] != nil {
for _, componentPath := range page.Window["usingComponents"].(map[string]interface{}) {
componentPath := componentPath.(string) + ".html"
file := componentPath
if filepath.IsAbs(componentPath) {
file = componentPath[1:]
} else {
file = toDir(filepath.Join(filepath.Dir(a), componentPath), cur)
}
if _, ok := e.Page[file]; !ok {
e.Page[file] = PageConfig{}
}
if e.Page[file].Window == nil {
e.Page[file] = PageConfig{Window: map[string]interface{}{"component": true}}
}
}
}
}
// 处理 app-service.js 文件, 主包及子包
if fileExists(filepath.Join(dir, "app-service.js")) {
serviceContent, _ := os.ReadFile(filepath.Join(dir, "app-service.js"))
matches := findMatches(`__wxAppCode__\['[^']+\.json'\]\s*=\s*({[^;]*});`, string(serviceContent))
if len(matches) > 0 {
attachInfo := make(map[string]interface{})
vm := goja.New()
err = vm.Set("__wxAppCode__", attachInfo)
if err != nil {
return err
}
_, err = vm.RunString(strings.Join(matches, ""))
if err != nil {
return err
}
for name, info := range attachInfo {
e.Page[changeExt(name, ".html")] = PageConfig{Window: info.(map[string]interface{})}
}
}
// 子包配置 app-service.js
for _, subPackage := range app.SubPackages {
root := subPackage.Root
subServiceFile := filepath.Join(dir, root, "app-service.js")
if !fileExists(subServiceFile) {
continue
}
serviceContent, _ = os.ReadFile(subServiceFile)
matches = findMatches(`__wxAppCode__\['[^']+\.json'\]\s*=\s*({[^;]*});`, string(serviceContent))
if len(matches) > 0 {
attachInfo := make(map[string]interface{})
vm := goja.New()
err := vm.Set("__wxAppCode__", attachInfo)
if err != nil {
return err
}
_, err = vm.RunString(strings.Join(matches, ""))
if err != nil {
return err
}
for name, info := range attachInfo {
e.Page[changeExt(name, ".html")] = PageConfig{Window: info.(map[string]interface{})}
}
}
}
}
// 保存页面 JSON 文件
for a := range e.Page {
aFile := changeExt(a, ".json")
fileName := filepath.Join(dir, aFile)
if aFile != "app.json" {
windowContent, _ := json.MarshalIndent(e.Page[a].Window, "", " ")
err = save(fileName, windowContent)
if err != nil {
return err
}
}
}
// 处理子包中的文件
if len(app.SubPackages) > 0 {
for _, subPackage := range app.SubPackages {
for _, item := range subPackage.Pages {
a := subPackage.Root + item + ".xx"
err := save(filepath.Join(dir, changeExt(a, ".js")), []byte("// "+changeExt(a, ".js")+"\nPage({data: {}})"))
if err != nil {
return err
}
err = save(filepath.Join(dir, changeExt(a, ".wxml")), []byte("<!--"+changeExt(a, ".wxml")+"--><text>"+changeExt(a, ".wxml")+"</text>"))
if err != nil {
return err
}
err = save(filepath.Join(dir, changeExt(a, ".wxss")), []byte("/* "+changeExt(a, ".wxss")+" */"))
if err != nil {
return err
}
}
}
}
// 处理 TabBar 图标路径
if app.TabBar != nil && app.TabBar["list"] != nil {
var digests [][2]interface{}
for _, file := range scanDirByExt(dir, "") {
data, _ := os.ReadFile(file)
digests = append(digests, [2]interface{}{md5.Sum(data), file})
}
for _, e := range app.TabBar["list"].([]interface{}) {
pagePath := e.(map[string]interface{})["pagePath"].(string)
e.(map[string]interface{})["pagePath"] = changeExt(pagePath, "")
if iconData, ok := e.(map[string]interface{})["iconData"].(string); ok {
hash := md5.Sum([]byte(iconData))
for _, digest := range digests {
digestByte, _ := digest[0].([16]byte)
if bytes.Equal(hash[:], digestByte[:]) {
delete(e.(map[string]interface{}), "iconData")
e.(map[string]interface{})["iconPath"] = fixDir(digest[1].(string), dir)
break
}
}
}
if selectedIconData, ok := e.(map[string]interface{})["selectedIconData"].(string); ok {
hash := md5.Sum([]byte(selectedIconData))
for _, digest := range digests {
digestByte, _ := digest[0].([16]byte)
if bytes.Equal(hash[:], digestByte[:]) {
delete(e.(map[string]interface{}), "selectedIconData")
e.(map[string]interface{})["selectedIconPath"] = fixDir(digest[1].(string), dir)
break
}
}
}
}
}
// 保存应用配置到 app.json
appContent, _ := json.MarshalIndent(app, "", " ")
err = save(filepath.Join(dir, "app.json"), appContent)
if err != nil {
return err
}
log.Printf("Config file processed: %s\n", configFile)
return nil
}
// indexOf 返回字符串切片中项的索引
func indexOf(slice []string, item string) int {
for i, v := range slice {
if v == item {
return i
}
}
return -1
}
// fileExists 检查文件是否存在
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// toDir 将文件路径转换为相对路径
func toDir(file, base string) string {
relative, err := filepath.Rel(base, file)
if err != nil {
return file
}
return relative
}
// findMatches 查找所有匹配模式的字符串
func findMatches(pattern, text string) []string {
re := regexp.MustCompile(pattern)
return re.FindAllString(text, -1)
}
// scanDirByExt 扫描目录中的文件并返回指定扩展名的文件列表
func scanDirByExt(dir, ext string) []string {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && strings.HasSuffix(info.Name(), ext) {
files = append(files, path)
}
return nil
})
if err != nil {
return nil
}
return files
}
// fixDir 修复文件路径为相对路径
func fixDir(file, base string) string {
rel, err := filepath.Rel(base, file)
if err != nil {
return file
}
return filepath.ToSlash(rel)
}

237
internal/unpack/ujs.go Normal file
View File

@ -0,0 +1,237 @@
package unpack
import (
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
. "github.com/Ackites/KillWxapkg/internal/config"
"github.com/dop251/goja"
)
// removeWrapper 移除函数包装器
func removeWrapper(jsCode string) (string, error) {
vm := goja.New()
script := `
(function(code) {
let match = code.match(/^function\s*\(.*?\)\s*\{([\s\S]*)\}$/);
if (match && match[1]) {
// 每一行缩进减少一个空格
match[1] = match[1].trim();
code = match[1].replace(/^\s{4}/gm, '');
}
return code;
})(code);
`
// 设置 JavaScript 变量
err := vm.Set("code", jsCode)
if err != nil {
return "", err
}
value, err := vm.RunString(script)
if err != nil {
return "", fmt.Errorf("JavaScript execution error: %w", err)
}
return value.String(), nil
}
// removeInvalidLineCode 删除无效行代码
func removeInvalidLineCode(code string) string {
invalidRe := regexp.MustCompile(`\s+[a-z] = VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL\.handleException\([a-z]\);`)
return invalidRe.ReplaceAllString(code, "")
}
// SplitJs 解析和分割 JavaScript 文件
func SplitJs(filePath string, mainDir string, wg *sync.WaitGroup, errChan chan error) {
defer wg.Done()
isSubPkg := mainDir != ""
dir := filepath.Dir(filePath)
if isSubPkg {
dir = mainDir
}
code, err := os.ReadFile(filePath)
if err != nil {
errChan <- fmt.Errorf("failed to read file: %w", err)
return
}
vm := goja.New()
// 定义 console 对象
console := vm.NewObject()
_ = console.Set("log", func(call goja.FunctionCall) goja.Value {
// 使用 call.Arguments 获取传递给 console.log 的参数
args := call.Arguments
for _, arg := range args {
fmt.Println(arg.String())
}
return goja.Undefined()
})
_ = console.Set("error", func(call goja.FunctionCall) goja.Value {
args := call.Arguments
for _, arg := range args {
fmt.Println("ERROR:", arg.String())
}
return goja.Undefined()
})
_ = vm.Set("console", console)
// 提供 __g 变量的默认实现
err = vm.Set("__g", make(map[string]interface{}))
if err != nil {
errChan <- err
return
}
err = vm.Set("__vd_version_info__", map[string]interface{}{
"version": "1.0.0",
"build": "default",
})
if err != nil {
errChan <- err
return
}
wxAppCode := make(map[string]func())
// 设置 __wxAppCode__
err = vm.Set("__wxAppCode__", wxAppCode)
if err != nil {
log.Printf("Error setting __wxAppCode__: %v\n", err)
return
}
// 设置 define 函数和 require 函数的行为
err = vm.Set("define", func(call goja.FunctionCall) goja.Value {
moduleName := call.Argument(0).String()
funcBody := call.Argument(1).String()
cleanedCode, err := removeWrapper(funcBody)
if err != nil {
log.Printf("Error removing wrapper: %v\n", err)
cleanedCode = funcBody
}
bcode := cleanedCode
// 检查是否包含 "use strict" 并处理
if strings.HasPrefix(cleanedCode, `"use strict";`) || strings.HasPrefix(cleanedCode, `'use strict';`) {
cleanedCode = cleanedCode[13:]
} else if (strings.HasPrefix(cleanedCode, `(function(){"use strict";`) || strings.HasPrefix(cleanedCode, `(function(){'use strict';`)) &&
strings.HasSuffix(cleanedCode, `})();`) {
cleanedCode = cleanedCode[25 : len(cleanedCode)-5]
}
// 删除无效行代码
res := removeInvalidLineCode(cleanedCode)
if res == "" {
log.Printf("Fail to delete 'use strict' in \"%s\".", moduleName)
res = removeInvalidLineCode(bcode)
}
err = saveToFile(filepath.Join(dir, moduleName), []byte(res))
if err != nil {
log.Printf("Error saving file: %v\n", err)
}
return goja.Undefined()
})
if err != nil {
errChan <- err
return
}
err = vm.Set("require", func(call goja.FunctionCall) goja.Value {
// 返回一个空对象,表示对 require 的任何调用都将返回这个空对象
result := vm.NewObject()
return result
})
if err != nil {
errChan <- err
return
}
if isSubPkg {
codeStr := string(code)
code = []byte(codeStr[strings.Index(codeStr, "define("):])
}
_, err = vm.RunString(string(code))
if err != nil {
errChan <- fmt.Errorf("failed to run JavaScript: %w", err)
return
}
manager := NewFileDeletionManager()
manager.AddFile(filePath)
log.Printf("Splitting \"%s\" done.", filePath)
}
// saveToFile 保存文件内容
func saveToFile(filePath string, content []byte) error {
err := os.MkdirAll(filepath.Dir(filePath), 0755)
if err != nil {
return fmt.Errorf("failed to create directories: %w", err)
}
err = os.WriteFile(filePath, content, 0755)
if err != nil {
log.Printf("Save file error: %v\n", err)
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
// ProcessJavaScriptFiles 处理所有 JavaScript 文件
func ProcessJavaScriptFiles(dir string, config struct {
SubPackages []SubPackage `json:"subPackages"`
}) error {
var wg sync.WaitGroup
errChan := make(chan error, 10) // 缓冲区大小可以根据需要调整
// 处理主包
appServicePath := filepath.Join(dir, "app-service.js")
workersPath := filepath.Join(dir, "workers.js")
if _, err := os.Stat(appServicePath); err == nil {
wg.Add(1)
go SplitJs(appServicePath, "", &wg, errChan)
}
if _, err := os.Stat(workersPath); err == nil {
wg.Add(1)
go SplitJs(workersPath, "", &wg, errChan)
}
// 遍历所有子包
for _, subPackage := range config.SubPackages {
subDir := filepath.Join(dir, subPackage.Root)
if _, err := os.Stat(subDir); err != nil {
continue
}
appServicePath = filepath.Join(subDir, "app-service.js")
workersPath = filepath.Join(subDir, "workers.js")
if _, err := os.Stat(appServicePath); err == nil {
wg.Add(1)
go SplitJs(appServicePath, dir, &wg, errChan)
}
if _, err := os.Stat(workersPath); err == nil {
wg.Add(1)
go SplitJs(workersPath, dir, &wg, errChan)
}
}
// 等待所有goroutine完成
go func() {
wg.Wait()
close(errChan)
}()
// 处理错误
for err := range errChan {
if err != nil {
return err
}
}
return nil
}

221
internal/unpack/unpack.go Normal file
View File

@ -0,0 +1,221 @@
package unpack
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"os"
"path/filepath"
"sync"
formatter2 "github.com/Ackites/KillWxapkg/internal/unpack/formatter"
)
type WxapkgFile struct {
NameLen uint32
Name string
Offset uint32
Size uint32
}
// UnpackWxapkg 解包 wxapkg 文件并将内容保存到指定目录
func UnpackWxapkg(data []byte, outputDir string) error {
reader := bytes.NewReader(data)
// 读取文件头
var firstMark byte
if err := binary.Read(reader, binary.BigEndian, &firstMark); err != nil {
return fmt.Errorf("读取首标记失败: %v", err)
}
if firstMark != 0xBE {
return fmt.Errorf("无效的wxapkg文件: 首标记不正确")
}
var info1, indexInfoLength, bodyInfoLength uint32
if err := binary.Read(reader, binary.BigEndian, &info1); err != nil {
return fmt.Errorf("读取info1失败: %v", err)
}
if err := binary.Read(reader, binary.BigEndian, &indexInfoLength); err != nil {
return fmt.Errorf("读取索引段长度失败: %v", err)
}
if err := binary.Read(reader, binary.BigEndian, &bodyInfoLength); err != nil {
return fmt.Errorf("读取数据段长度失败: %v", err)
}
// 验证长度的合理性
totalLength := uint64(indexInfoLength) + uint64(bodyInfoLength)
if totalLength > uint64(len(data)) {
return fmt.Errorf("文件长度不足, 文件损坏: 索引段(%d) + 数据段(%d) > 文件总长度(%d)", indexInfoLength, bodyInfoLength, len(data))
}
totalLength = uint64(len(data))
var lastMark byte
if err := binary.Read(reader, binary.BigEndian, &lastMark); err != nil {
return fmt.Errorf("读取尾标记失败: %v", err)
}
if lastMark != 0xED {
return fmt.Errorf("无效的wxapkg文件: 尾标记不正确")
}
var fileCount uint32
if err := binary.Read(reader, binary.BigEndian, &fileCount); err != nil {
return fmt.Errorf("读取文件数量失败: %v", err)
}
// 计算索引段的预期结束位置
expectedIndexEnd := uint64(reader.Size()) - uint64(bodyInfoLength)
// 读取索引
fileList := make([]WxapkgFile, fileCount)
for i := range fileList {
if err := binary.Read(reader, binary.BigEndian, &fileList[i].NameLen); err != nil {
return fmt.Errorf("读取文件名长度失败: %v", err)
}
if fileList[i].NameLen == 0 || fileList[i].NameLen > 1024 {
return fmt.Errorf("文件名长度 %d 不合理", fileList[i].NameLen)
}
nameBytes := make([]byte, fileList[i].NameLen)
if _, err := io.ReadAtLeast(reader, nameBytes, int(fileList[i].NameLen)); err != nil {
return fmt.Errorf("读取文件名失败: %v", err)
}
fileList[i].Name = string(nameBytes)
if err := binary.Read(reader, binary.BigEndian, &fileList[i].Offset); err != nil {
return fmt.Errorf("读取文件偏移量失败: %v", err)
}
if err := binary.Read(reader, binary.BigEndian, &fileList[i].Size); err != nil {
return fmt.Errorf("读取文件大小失败: %v", err)
}
// 验证文件偏移量和大小
fileEnd := uint64(fileList[i].Offset) + uint64(fileList[i].Size)
if fileEnd > totalLength {
return fmt.Errorf("文件 %s 的结束位置 (%d) 超出了文件总长度 (%d)", fileList[i].Name, fileEnd, totalLength)
}
// 验证我们是否仍在索引段内
currentPos := uint64(reader.Size()) - uint64(reader.Len())
if currentPos > expectedIndexEnd {
return fmt.Errorf("索引读取超出预期范围: 当前位置 %d, 预期索引结束位置 %d", currentPos, expectedIndexEnd)
}
}
// 验证是否正确读完了整个索引段
currentPos := uint64(reader.Size()) - uint64(reader.Len())
if currentPos != expectedIndexEnd {
return fmt.Errorf("索引段长度不符: 读取到位置 %d, 预期结束位置 %d", currentPos, expectedIndexEnd)
}
// 控制并发数
const workerCount = 10
var wg sync.WaitGroup
fileChan := make(chan WxapkgFile, workerCount)
errChan := make(chan error, workerCount)
// 使用 sync.Pool 来复用缓冲区,减少内存分配和 GC 开销
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
for i := 0; i < workerCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for file := range fileChan {
if err := processFile(outputDir, file, reader, &bufferPool); err != nil {
errChan <- fmt.Errorf("保存文件 %s 失败: %w", file.Name, err)
}
}
}()
}
for _, file := range fileList {
fileChan <- file
}
close(fileChan)
// 等待所有 goroutine 完成
wg.Wait()
close(errChan)
// 检查是否有错误
if len(errChan) > 0 {
return <-errChan
}
const configJSON = `{
"description": "See https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"setting": {
"urlCheck": false
}
}`
// 保存 project.config.json
configFile := filepath.Join(outputDir, "project.private.config.json")
if err := os.WriteFile(configFile, []byte(configJSON), 0755); err != nil {
return fmt.Errorf("保存文件 %s 失败: %w", configFile, err)
}
return nil
}
// processFile 处理单个文件的读取、格式化和保存
func processFile(outputDir string, file WxapkgFile, reader io.ReaderAt, bufferPool *sync.Pool) error {
fullPath := filepath.Join(outputDir, file.Name)
dir := filepath.Dir(fullPath)
// 创建目录
if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) {
return fmt.Errorf("创建目录失败: %w", err)
}
// 创建文件
f, err := os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
return fmt.Errorf("创建文件失败: %w", err)
}
defer func(f *os.File) {
err := f.Close()
if err != nil {
fmt.Printf("关闭文件 %s 失败: %v\n", file.Name, err)
}
}(f)
// 使用 io.NewSectionReader 创建一个只读取指定部分的 Reader
sectionReader := io.NewSectionReader(reader, int64(file.Offset), int64(file.Size))
// 从 bufferPool 获取缓冲区
buf := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buf)
buf.Reset()
// 读取文件内容
if _, err := io.Copy(buf, sectionReader); err != nil {
return fmt.Errorf("读取文件内容失败: %w", err)
}
content := buf.Bytes()
// 获取文件格式化器
ext := filepath.Ext(file.Name)
formatter, err := formatter2.GetFormatter(ext)
if err == nil {
content, err = formatter.Format(content)
if err != nil {
return fmt.Errorf("格式化文件失败: %w", err)
}
}
// 写入文件内容
if _, err := f.Write(content); err != nil {
return fmt.Errorf("写入文件失败: %w", err)
}
return nil
}

1
internal/unpack/uxml.go Normal file
View File

@ -0,0 +1 @@
package unpack

381
internal/unpack/uxss.go Normal file
View File

@ -0,0 +1,381 @@
package unpack
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/Ackites/KillWxapkg/utils"
"github.com/dop251/goja"
"golang.org/x/net/html"
)
// GwxCfg 结构体定义
type GwxCfg struct{}
// GWX 空方法
func (g *GwxCfg) GWX() {}
// CSS 重建函数
func cssRebuild(pureData map[string]interface{}, importCnt map[string]int, actualPure map[string]string, blockCss []string, cssFile string, result map[string]string, commonStyle map[string]interface{}, onlyTest *bool) func(data interface{}) {
// 统计导入的 CSS 文件
var statistic func(data interface{})
statistic = func(data interface{}) {
addStat := func(id string) {
if _, exists := importCnt[id]; !exists {
importCnt[id] = 1
statistic(pureData[id])
} else {
importCnt[id]++
}
}
switch v := data.(type) {
case float64:
addStat(fmt.Sprintf("%v", v))
case string:
addStat(v)
case []interface{}:
for _, content := range v {
if contentArr, ok := content.([]interface{}); ok && int(contentArr[0].(float64)) == 2 {
addStat(fmt.Sprintf("%v", contentArr[1]))
}
}
}
}
// 生成 CSS 样式
var makeup func(data interface{}) string
makeup = func(data interface{}) string {
log.Printf("Processing data: %v\n", data)
isPure := false
switch data.(type) {
case string:
isPure = true
}
if *onlyTest {
statistic(data)
if !isPure {
dataArr := data.([]interface{})
if len(dataArr) == 1 && int(dataArr[0].([]interface{})[0].(float64)) == 2 {
data = dataArr[0].([]interface{})[1]
} else {
return ""
}
}
if actualPure[data.(string)] == "" && !contains(blockCss, changeExt(toDir2(cssFile, ""), "")) {
actualPure[data.(string)] = cssFile
}
return ""
}
var res strings.Builder
attach := ""
if isPure && actualPure[data.(string)] != cssFile {
if actualPure[data.(string)] != "" {
return fmt.Sprintf("@import \"%s.wxss\";\n", toDir2(actualPure[data.(string)], cssFile))
}
res.WriteString(fmt.Sprintf("/*! Import by _C[%s], whose real path we cannot found. */", data.(string)))
attach = "/*! Import end */"
}
exactData := data
if isPure {
exactData = pureData[data.(string)]
}
if styleData, ok := commonStyle[data.(string)]; ok {
if styleArray, ok := styleData.([]interface{}); ok {
var fileStyle strings.Builder
for _, content := range styleArray {
if contentStr, ok := content.(string); ok && contentStr != "1" {
fileStyle.WriteString(contentStr)
} else if contentArr, ok := content.([]interface{}); ok && len(contentArr) != 1 {
fileStyle.WriteString(fmt.Sprintf("%vrpx", contentArr[1]))
}
}
return fileStyle.String()
}
} else {
if dataArr, ok := exactData.([]interface{}); ok {
for _, content := range dataArr {
if contentArr, ok := content.([]interface{}); ok {
switch int(contentArr[0].(float64)) {
case 0: // rpx
res.WriteString(fmt.Sprintf("%vrpx", contentArr[1]))
case 1: // add suffix, ignore it for restoring correct!
case 2: // import
res.WriteString(makeup(contentArr[1]))
}
} else {
res.WriteString(content.(string))
}
}
}
}
return res.String() + attach
}
// 返回处理函数
return func(data interface{}) {
log.Printf("Processing CSS file: %s\n", cssFile)
if result[cssFile] == "" {
result[cssFile] = ""
}
result[cssFile] += makeup(data)
}
}
// 运行 JavaScript 代码
func runVM(name string, code string, pureData map[string]interface{}, importCnt map[string]int, actualPure map[string]string, blockCss []string, result map[string]string, commonStyle map[string]interface{}, onlyTest *bool) {
vm := goja.New()
wxAppCode := make(map[string]func())
// 添加 console.log
err := vm.Set("console", map[string]interface{}{
"log": func(msg string) {
log.Printf(msg)
},
})
if err != nil {
log.Printf("Error setting console log: %v\n", err)
return
}
// 设置 setCssToHead 函数
err = vm.Set("setCssToHead", cssRebuild(pureData, importCnt, actualPure, blockCss, name, result, commonStyle, onlyTest))
if err != nil {
log.Printf("Error setting setCssToHead: %v\n", err)
return
}
// 设置 __wxAppCode__
err = vm.Set("__wxAppCode__", wxAppCode)
if err != nil {
log.Printf("Error setting __wxAppCode__: %v\n", err)
return
}
// 运行 JavaScript 代码
_, err = vm.RunString(code)
if err != nil {
log.Printf("Error running JavaScript code: %v\n", err)
return
}
// 调用 wxAppCode 中的函数
log.Printf("Running wxAppCode...\n")
for _, fn := range wxAppCode {
fn()
}
}
// ProcessXssFiles 处理 WXSS 文件
func ProcessXssFiles(dir string, mainDir string) {
saveDir := dir
isSubPkg := mainDir != ""
if isSubPkg {
saveDir = mainDir
}
var runList = make(map[string]string)
var pureData = make(map[string]interface{})
var result = make(map[string]string)
var actualPure = make(map[string]string)
var importCnt = make(map[string]int)
var blockCss []string // 自定义阻止导入的 CSS 文件(无扩展名)
var commonStyle map[string]interface{}
var onlyTest = true
// 预运行,读取所有相关文件
preRun := func(dir, frameFile, mainCode string, files []string, cb func()) {
runList[filepath.Join(dir, "./app.wxss")] = mainCode
for _, name := range files {
if name != frameFile {
code, err := os.ReadFile(name)
if err != nil {
log.Printf("Error reading file: %v\n", err)
continue
}
codeStr := string(code)
codeStr = strings.Replace(codeStr, "display:-webkit-box;display:-webkit-flex;", "", -1)
codeStr = codeStr[:strings.Index(codeStr, "\n")] // 确保只取第一行
if strings.Contains(codeStr, "setCssToHead(") {
runList[name] = codeStr[strings.Index(codeStr, "setCssToHead("):]
}
}
}
cb()
}
// 一次性运行所有 JavaScript 代码
runOnce := func() {
for name, code := range runList {
runVM(name, code, pureData, importCnt, actualPure, blockCss, result, commonStyle, &onlyTest)
}
}
// 扫描目录中的所有 HTML 文件
scanDirByExtTo(dir, ".html", func(files []string) {
var frameFile string
if _, err := os.Stat(filepath.Join(dir, "page-frame.html")); err == nil {
frameFile = filepath.Join(dir, "page-frame.html")
} else if _, err := os.Stat(filepath.Join(dir, "app-wxss.js")); err == nil {
frameFile = filepath.Join(dir, "app-wxss.js")
} else if _, err := os.Stat(filepath.Join(dir, "page-frame.js")); err == nil {
frameFile = filepath.Join(dir, "page-frame.js")
} else {
log.Printf("未找到类似 page-frame 的文件")
return
}
code, err := os.ReadFile(frameFile)
if err != nil {
log.Printf("Error reading file: %v\n", err)
return
}
codeStr := string(code)
codeStr = strings.Replace(codeStr, "display:-webkit-box;display:-webkit-flex;", "", -1)
scriptCode := codeStr
if strings.HasSuffix(frameFile, ".html") {
doc, err := html.Parse(strings.NewReader(codeStr))
if err != nil {
log.Printf("Error parsing HTML: %v\n", err)
return
}
var scriptBuilder strings.Builder
var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "script" {
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.TextNode {
scriptBuilder.WriteString(c.Data)
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(doc)
scriptCode = scriptBuilder.String()
}
window := map[string]interface{}{
"screen": map[string]interface{}{
"width": 720,
"height": 1028,
"orientation": map[string]interface{}{
"type": "vertical",
},
},
}
navigator := map[string]interface{}{
"userAgent": "iPhone",
}
scriptCode = scriptCode[strings.LastIndex(scriptCode, "window.__wcc_version__"):]
mainCode := fmt.Sprintf(`window=%s;navigator=%s;var __mainPageFrameReady__=window.__mainPageFrameReady__ || function() {};var __WXML_GLOBAL__={entrys:{},defines:{},modules:{},ops:[],wxs_nf_init:undefined,total_ops:0};var __vd_version_info__=__vd_version_info__ || {};%s`,
toJSON(window), toJSON(navigator), scriptCode)
// 处理 commonStyles
if idx := strings.Index(codeStr, "__COMMON_STYLESHEETS__ || {}"); idx != -1 {
start := idx + 28
end := strings.Index(codeStr[start:], "var setCssToHead = function(file, _xcInvalid, info)")
if end != -1 {
commonStyles := codeStr[start : start+end]
vm := goja.New()
_, err := vm.RunString(fmt.Sprintf(";var __COMMON_STYLESHEETS__ = __COMMON_STYLESHEETS__ || {};%s;__COMMON_STYLESHEETS__;", commonStyles))
if err == nil {
err = vm.ExportTo(vm.Get("__COMMON_STYLESHEETS__"), &commonStyle)
if err != nil {
log.Printf("Error exporting common styles: %v\n", err)
}
}
}
}
mainCode = strings.Replace(mainCode, "var setCssToHead = function", "var setCssToHead2 = function", 1)
codeStr = codeStr[strings.LastIndex(codeStr, "var setCssToHead = function(file, _xcInvalid"):]
codeStr = strings.Replace(codeStr, "__COMMON_STYLESHEETS__", "[]", 1)
if idx := strings.Index(codeStr, "_C = "); idx != -1 {
codeStr = codeStr[strings.LastIndex(codeStr, "var _C = "):]
} else {
codeStr = codeStr[strings.LastIndex(codeStr, "var _C= "):]
}
codeStr = codeStr[:strings.Index(codeStr, "\n")]
vm := goja.New()
_, err = vm.RunString(codeStr + "\n_C")
if err != nil {
log.Printf("Error running JavaScript code: %v\n", err)
return
}
err = vm.ExportTo(vm.Get("_C"), &pureData)
if err != nil {
log.Printf("Error exporting pure data: %v\n", err)
return
}
preRun(dir, frameFile, mainCode, files, func() {
runOnce()
onlyTest = false
runOnce()
for name, content := range result {
name = filepath.Join(saveDir, changeExt(name, ".wxss"))
err := os.WriteFile(name, []byte(utils.TransformCSS(content)), 0755)
log.Printf("Save wxss %s done.\n", name)
if err != nil {
log.Printf("Error saving file: %v\n", err)
}
}
})
})
}
// toJSON 将对象转换为 JSON 字符串
func toJSON(obj interface{}) string {
data, _ := json.Marshal(obj)
return string(data)
}
// scanDirByExtTo 扫描目录中的文件并返回指定扩展名的文件列表
func scanDirByExtTo(dir, ext string, cb func([]string)) {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && strings.HasSuffix(info.Name(), ext) {
files = append(files, path)
}
return nil
})
if err != nil {
return
}
cb(files)
}
// contains 检查切片是否包含特定元素
func contains(slice []string, item string) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
// toDir2 生成目录路径
func toDir2(filePath, frameName string) string {
dir := filepath.Dir(filePath)
return filepath.Join(dir, frameName)
}

52
main.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"flag"
"fmt"
"github.com/Ackites/KillWxapkg/cmd"
)
var (
appID string
input string
outputDir string
fileExt string
restoreDir bool
pretty bool
)
func init() {
flag.StringVar(&appID, "id", "", "微信小程序的AppID")
flag.StringVar(&input, "in", "", "输入文件路径(多个文件用逗号分隔)或输入目录路径")
flag.StringVar(&outputDir, "out", "", "输出目录路径如果未指定则默认保存到输入目录下以AppID命名的文件夹")
flag.StringVar(&fileExt, "ext", ".wxapkg", "处理的文件后缀")
flag.BoolVar(&restoreDir, "restore", false, "是否还原工程目录结构")
flag.BoolVar(&pretty, "pretty", false, "是否美化输出")
}
func main() {
// 解析命令行参数
flag.Parse()
if appID == "" || input == "" {
fmt.Println("使用方法: program -id=<AppID> -in=<输入文件1,输入文件2> 或 -in=<输入目录> -out=<输出目录> [-ext=<文件后缀>] [-restore] [-pretty]")
flag.PrintDefaults()
return
}
banner := `
_ __ _ _ _ __ __ _
| | / /(_) | | \ \ / / | |
| |/ / _| | | \ \ / / __ ____ _ ___| | ____ _
| \ | | | | \ \/ / / / / / _ / __| |/ / ' \
| |\ \| | | | \ / / /_/ / (_| \__ \ <| | | |
\_| \_/_|_|_| \/ \__,_|\__,_|___/_|\_\_| |_|
Wxapkg Decompiler Tool v1.0.0
`
fmt.Println(banner)
// 执行命令
cmd.Execute(appID, input, outputDir, fileExt, restoreDir, pretty)
}

View File

@ -0,0 +1,17 @@
package utils
import "fmt"
// HumanReadableSize 转换为人类可读的文件大小
func HumanReadableSize(size uint64) string {
if size < 1024 {
return fmt.Sprintf("%d B", size)
}
if size < 1024*1024 {
return fmt.Sprintf("%.2f KB", float64(size)/1024)
}
if size < 1024*1024*1024 {
return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024))
}
return fmt.Sprintf("%.2f GB", float64(size)/(1024*1024*1024))
}

168
utils/transformCSS.go Normal file
View File

@ -0,0 +1,168 @@
package utils
import (
"bytes"
"fmt"
"strings"
"github.com/gorilla/css/scanner"
)
// 定义需要移除的供应商前缀
var removeTypes = []string{"webkit", "moz", "ms", "o"}
// Declaration 结构体用于存储 CSS 声明
type Declaration struct {
Property string
Value string
}
// TransformCSS 函数用于转换 CSS
func TransformCSS(style string) string {
s := scanner.New(style)
var sb strings.Builder
var currentSelector string
var currentDeclarations []Declaration
for {
token := s.Next()
if token.Type == scanner.TokenEOF {
if currentSelector != "" {
writeRuleset(&sb, currentSelector, currentDeclarations)
}
break
}
switch token.Type {
case scanner.TokenS:
// 忽略空白
continue
case scanner.TokenComment:
sb.WriteString(fmt.Sprintf("\n%s\n", token.Value))
case scanner.TokenIdent:
// 处理选择器或属性名
if currentSelector == "" {
// 选择器
currentSelector = strings.TrimSpace(token.Value)
if strings.HasPrefix(currentSelector, "wx-") {
currentSelector = currentSelector[3:]
} else if currentSelector == "body" {
currentSelector = "page"
}
} else {
// 属性名
currentDeclarations = append(currentDeclarations, readDeclaration(s, token.Value))
}
case scanner.TokenChar:
if token.Value == "{" {
// 忽略大括号
continue
} else if token.Value == "}" {
// 遇到右大括号,写入规则集
writeRuleset(&sb, currentSelector, currentDeclarations)
currentSelector = ""
currentDeclarations = nil
}
default:
panic("unhandled default case")
}
}
return beautifyCSS(sb.String())
}
// readDeclaration 函数读取一个声明
func readDeclaration(s *scanner.Scanner, property string) Declaration {
var value bytes.Buffer
foundColon := false
// 1: 遇到冒号,跳过冒号
// 2: 遇到冒号,不跳过冒号
count := 1
for {
token := s.Next()
if token.Type == scanner.TokenEOF || token.Value == "}" || token.Value == ";" {
break
}
if token.Value == ":" && count == 1 {
foundColon = true
count++
continue
}
if foundColon {
value.WriteString(token.Value)
}
}
prop := strings.TrimSpace(property)
val := strings.TrimSpace(value.String())
if shouldRemoveProperty(prop, val) {
return Declaration{}
}
return Declaration{
Property: prop,
Value: val,
}
}
// shouldRemoveProperty 函数判断是否应移除属性
func shouldRemoveProperty(prop, value string) bool {
// 移除包含 progid:DXImageTransform 的值
if strings.HasPrefix(value, "progid:DXImageTransform") {
return true
}
// 移除指定前缀的属性
for _, prefix := range removeTypes {
if strings.HasPrefix(prop, "-"+prefix+"-") {
return true
}
if strings.HasPrefix(value, "-"+prefix+"-") {
return true
}
}
return false
}
// writeRuleset 函数用于写入处理后的规则集
func writeRuleset(sb *strings.Builder, selector string, declarations []Declaration) {
sb.WriteString(selector + " {\n")
for _, decl := range declarations {
// 过滤空的声明
if decl.Property != "" && decl.Value != "" {
sb.WriteString(fmt.Sprintf(" %s: %s;\n", decl.Property, decl.Value))
}
}
sb.WriteString("}\n\n")
}
// beautifyCSS 函数用于美化 CSS
func beautifyCSS(css string) string {
var beautified strings.Builder
indent := 0
lines := strings.Split(css, "\n")
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if trimmedLine == "" {
continue
}
if strings.HasPrefix(trimmedLine, "}") {
indent--
}
beautified.WriteString(strings.Repeat(" ", indent))
beautified.WriteString(trimmedLine)
beautified.WriteString("\n")
if strings.HasSuffix(trimmedLine, "{") {
indent++
}
}
return beautified.String()
}