From 8d79265bcecce6f52e6f733ceda64bd201987859 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Mon, 12 Oct 2026 04:18:29 +0800 Subject: [PATCH] CLI: Add lua encode command for inline scripts --- common/lua/encode.go | 29 +++++++++++ common/lua/encode_test.go | 88 +++++++++++++++++++++++++++++++++ main/commands/all/commands.go | 2 + main/commands/all/lua/encode.go | 62 +++++++++++++++++++++++ main/commands/all/lua/lua.go | 13 +++++ 5 files changed, 194 insertions(+) create mode 100644 common/lua/encode.go create mode 100644 common/lua/encode_test.go create mode 100644 main/commands/all/lua/encode.go create mode 100644 main/commands/all/lua/lua.go diff --git a/common/lua/encode.go b/common/lua/encode.go new file mode 100644 index 000000000..a45282274 --- /dev/null +++ b/common/lua/encode.go @@ -0,0 +1,29 @@ +package lua + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + + "github.com/xtls/xray-core/common/errors" +) + +// Encode returns an inline script, optionally compressed with gzip at the highest level. +func Encode(code []byte, zip bool) (string, error) { + if !zip { + return "base64:" + base64.StdEncoding.EncodeToString(code), nil + } + + var data bytes.Buffer + writer, err := gzip.NewWriterLevel(&data, gzip.BestCompression) + if err != nil { + return "", errors.New("failed to compress gzip").Base(err) + } + if _, err := writer.Write(code); err != nil { + return "", errors.New("failed to compress gzip").Base(err) + } + if err := writer.Close(); err != nil { + return "", errors.New("failed to compress gzip").Base(err) + } + return "base64+gzip:" + base64.StdEncoding.EncodeToString(data.Bytes()), nil +} diff --git a/common/lua/encode_test.go b/common/lua/encode_test.go new file mode 100644 index 000000000..52256ee01 --- /dev/null +++ b/common/lua/encode_test.go @@ -0,0 +1,88 @@ +package lua + +import ( + "bytes" + "context" + "encoding/base64" + "strings" + "testing" + + glua "github.com/yuin/gopher-lua" +) + +func TestEncodeRoundTrip(t *testing.T) { + for _, tc := range []struct { + name string + code []byte + }{ + {"empty", nil}, + {"padding", []byte("x")}, + {"unicode", []byte(`value = "你好"`)}, + {"multiline", []byte("-- comment\r\nvalue = 42\n")}, + {"binary", []byte{0, 0xff, 0xfe, 1, 2}}, + {"large", bytes.Repeat([]byte("-- repeated code\n"), 32768)}, + } { + t.Run(tc.name, func(t *testing.T) { + for _, zip := range []bool{false, true} { + prefix := "base64:" + if zip { + prefix = "base64+gzip:" + } + t.Run(prefix, func(t *testing.T) { + script, err := Encode(tc.code, zip) + if err != nil { + t.Fatal(err) + } + encoded, ok := strings.CutPrefix(script, prefix) + if !ok { + t.Fatalf("encoded script does not start with %q", prefix) + } + code, err := decode(encoded, zip) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(code, tc.code) { + t.Fatal("decoded bytes differ from input") + } + }) + } + }) + } +} + +func TestEncodeBestCompression(t *testing.T) { + script, err := Encode([]byte("value = 42"), true) + if err != nil { + t.Fatal(err) + } + data, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(script, "base64+gzip:")) + if err != nil { + t.Fatal(err) + } + // The gzip XFL header byte is 2 for the highest compression level. + if len(data) < 10 || data[8] != 2 { + t.Fatal("gzip header does not indicate highest compression") + } +} + +func TestEncodeCompile(t *testing.T) { + for _, zip := range []bool{false, true} { + script, err := Encode([]byte(`value = "你好"`), zip) + if err != nil { + t.Fatal(err) + } + program, err := Compile(script) + if err != nil { + t.Fatal(err) + } + state, err := program.NewState(context.Background(), nil, nil) + if err != nil { + t.Fatal(err) + } + if got := state.GetGlobal("value"); got != glua.LString("你好") { + state.Close() + t.Fatalf("value = %v, want 你好", got) + } + state.Close() + } +} diff --git a/main/commands/all/commands.go b/main/commands/all/commands.go index fba3a4b8b..3ed3830ee 100644 --- a/main/commands/all/commands.go +++ b/main/commands/all/commands.go @@ -3,6 +3,7 @@ package all import ( "github.com/xtls/xray-core/main/commands/all/api" "github.com/xtls/xray-core/main/commands/all/convert" + "github.com/xtls/xray-core/main/commands/all/lua" "github.com/xtls/xray-core/main/commands/all/tls" "github.com/xtls/xray-core/main/commands/base" ) @@ -12,6 +13,7 @@ func init() { base.RootCommand.Commands, api.CmdAPI, convert.CmdConvert, + lua.CmdLua, tls.CmdTLS, cmdUUID, cmdX25519, diff --git a/main/commands/all/lua/encode.go b/main/commands/all/lua/encode.go new file mode 100644 index 000000000..7ecd8ece2 --- /dev/null +++ b/main/commands/all/lua/encode.go @@ -0,0 +1,62 @@ +package lua + +import ( + "fmt" + "io" + "os" + + xlua "github.com/xtls/xray-core/common/lua" + "github.com/xtls/xray-core/main/commands/base" +) + +var cmdEncode = &base.Command{ + UsageLine: "{{.Exec}} lua encode [-gzip=false] [file | stdin:]", + Short: "Encode a Lua script for inline configuration", + Long: ` +Encode one file or stdin: as an inline Lua script for the script configuration field. +Files are read from the given path without searching configuration directories. +The original bytes are preserved; the script is not executed or validated. +The encoded string is written to stdout on one line, without quotes. + +Arguments: + + -gzip=false + Disable compression and output base64: instead of base64+gzip:. + By default, gzip uses the highest compression level. + +Examples: + + {{.Exec}} lua encode script.lua + {{.Exec}} lua encode -gzip=false script.lua + {{.Exec}} lua encode stdin: +`, +} + +var gzipEnabled = cmdEncode.Flag.Bool("gzip", true, "Compress the script with gzip.") + +func init() { + cmdEncode.Run = executeEncode +} + +func executeEncode(cmd *base.Command, args []string) { + if len(args) != 1 { + cmd.Usage() + } + var code []byte + var err error + if args[0] == "stdin:" { + code, err = io.ReadAll(os.Stdin) + } else { + code, err = os.ReadFile(args[0]) + } + if err != nil { + base.Fatalf("failed to read script: %s", err) + } + script, err := xlua.Encode(code, *gzipEnabled) + if err != nil { + base.Fatalf("failed to encode script: %s", err) + } + if _, err := fmt.Fprintln(os.Stdout, script); err != nil { + base.Fatalf("failed to write encoded script: %s", err) + } +} diff --git a/main/commands/all/lua/lua.go b/main/commands/all/lua/lua.go new file mode 100644 index 000000000..a97d28420 --- /dev/null +++ b/main/commands/all/lua/lua.go @@ -0,0 +1,13 @@ +package lua + +import "github.com/xtls/xray-core/main/commands/base" + +// CmdLua holds all Lua subcommands. +var CmdLua = &base.Command{ + UsageLine: "{{.Exec}} lua", + Short: "Lua tools", + Long: "{{.Exec}} {{.LongName}} provides tools for Lua scripts.", + Commands: []*base.Command{ + cmdEncode, + }, +}