v1.0.0
This commit is contained in:
parent
f3c5bd4a29
commit
2d11b8c569
39
.github/workflows/release.yml
vendored
Normal file
39
.github/workflows/release.yml
vendored
Normal 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
203
.gitignore
vendored
Normal 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
38
.goreleaser.yaml
Normal 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
8
.idea/.gitignore
generated
vendored
Normal 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
6
.idea/JavaSceneConfigState.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SmartInputSourceJavaSceneConfigState">
|
||||||
|
<option name="customChineseScenes" value="{"capsLockState":false,"code":";Printf(format)","enable":true,"languageType":"CHINESE","name":"自定义中文切换","tip":""}" />
|
||||||
|
</component>
|
||||||
|
</project>
|
11
.idea/KillWxapkg.iml
generated
Normal file
11
.idea/KillWxapkg.iml
generated
Normal 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>
|
5
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
5
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
41
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
8
.idea/watcherTasks.xml
generated
Normal 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
100
README.md
Normal file
@ -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 项目
|
||||||
|
- [ ] 敏感数据导出
|
||||||
|
|
||||||
|
### 工程结构还原
|
||||||
|
|
||||||
|
#### 未还原
|
||||||
|
<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
|
||||||
|
|
||||||
|
[](https://star-history.com/#Ackites/KillWxapkg&Date)
|
59
cmd/root.go
Normal file
59
cmd/root.go
Normal 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
19
go.mod
Normal 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
22
go.sum
Normal 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
BIN
images/img.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
BIN
images/img1.png
Normal file
BIN
images/img1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
images/img2.png
Normal file
BIN
images/img2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
images/img3.png
Normal file
BIN
images/img3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
146
internal/cmd/cmd.go
Normal file
146
internal/cmd/cmd.go
Normal 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
73
internal/config/delete.go
Normal 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
79
internal/config/share.go
Normal 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
|
||||||
|
}
|
73
internal/decrypt/decrypt.go
Normal file
73
internal/decrypt/decrypt.go
Normal 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
|
||||||
|
}
|
32
internal/restore/javascript.go
Normal file
32
internal/restore/javascript.go
Normal 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
|
||||||
|
}
|
45
internal/restore/restore.go
Normal file
45
internal/restore/restore.go
Normal 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
19
internal/restore/wxss.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
94
internal/unpack/fileMagicNumbers.go
Normal file
94
internal/unpack/fileMagicNumbers.go
Normal 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},
|
||||||
|
}
|
38
internal/unpack/formatter/formatter.go
Normal file
38
internal/unpack/formatter/formatter.go
Normal 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
|
||||||
|
}
|
69
internal/unpack/formatter/htmlformatter.go
Normal file
69
internal/unpack/formatter/htmlformatter.go
Normal 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())
|
||||||
|
}
|
35
internal/unpack/formatter/jsformatter.go
Normal file
35
internal/unpack/formatter/jsformatter.go
Normal 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())
|
||||||
|
}
|
26
internal/unpack/formatter/jsonformatter.go
Normal file
26
internal/unpack/formatter/jsonformatter.go
Normal 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
419
internal/unpack/uconfig.go
Normal 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
237
internal/unpack/ujs.go
Normal 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
221
internal/unpack/unpack.go
Normal 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
1
internal/unpack/uxml.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package unpack
|
381
internal/unpack/uxss.go
Normal file
381
internal/unpack/uxss.go
Normal 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
52
main.go
Normal 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)
|
||||||
|
}
|
17
utils/humanReadableSize.go
Normal file
17
utils/humanReadableSize.go
Normal 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
168
utils/transformCSS.go
Normal 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()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user