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