mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-07 17:08:13 -10:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1420ebd447 | ||
|
|
96a531f9d4 | ||
|
|
f3bdb5cadc | ||
|
|
fd3df6a3f3 | ||
|
|
3c6b60bc3e | ||
|
|
d42373e545 | ||
|
|
76e4f430d0 | ||
|
|
ab8d431c39 | ||
|
|
e4ad404822 | ||
|
|
9e6a771a1a | ||
|
|
69ebbcb728 | ||
|
|
5c65da3567 | ||
|
|
1c67a11c29 | ||
|
|
755bed951b | ||
|
|
7ba4e5bec0 | ||
|
|
5d8d49461c | ||
|
|
cadc717963 | ||
|
|
ce4a7c7dcc | ||
|
|
ab8ff6a06a | ||
|
|
3b93554df2 | ||
|
|
b688b7b121 | ||
|
|
3ce603a3c7 | ||
|
|
3d9df17d43 | ||
|
|
d53c646af0 | ||
|
|
601c1eea64 | ||
|
|
12ee96451a | ||
|
|
cc304ba85a | ||
|
|
a743a72500 | ||
|
|
6c75b08648 |
No files matched your search
@@ -0,0 +1,9 @@
|
||||
fs/*.zip
|
||||
fs/extensions/darwin/amd64/
|
||||
fs/extensions/darwin/arm64/
|
||||
fs/extensions/linux/amd64/
|
||||
fs/extensions/windows/amd64/
|
||||
fs/extensions/darwin/386/
|
||||
fs/extensions/darwin/386/
|
||||
fs/extensions/linux/386/
|
||||
fs/extensions/windows/386/
|
||||
@@ -19,15 +19,29 @@ package assets
|
||||
*/
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
ver "github.com/bishopfox/sliver/client/version"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed fs/extensions/*
|
||||
assetsFs embed.FS
|
||||
)
|
||||
|
||||
const (
|
||||
// SliverClientDirName - Directory storing all of the client configs/logs
|
||||
SliverClientDirName = ".sliver-client"
|
||||
// SliverExtensionsDirName - Directory storing the client side extensions
|
||||
SliverExtensionsDirName = "extensions"
|
||||
versionFileName = "version"
|
||||
)
|
||||
|
||||
// GetRootAppDir - Get the Sliver app dir ~/.sliver-client/
|
||||
@@ -42,3 +56,89 @@ func GetRootAppDir() string {
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// GetExtensionsDir - Get the Sliver extension directory: ~/.sliver-client/extensions
|
||||
func GetExtensionsDir() string {
|
||||
user, _ := user.Current()
|
||||
dir := path.Join(user.HomeDir, SliverClientDirName, SliverExtensionsDirName)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func assetVersion() string {
|
||||
appDir := GetRootAppDir()
|
||||
data, err := ioutil.ReadFile(path.Join(appDir, versionFileName))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
func saveAssetVersion(appDir string) {
|
||||
versionFilePath := path.Join(appDir, versionFileName)
|
||||
fVer, _ := os.Create(versionFilePath)
|
||||
defer fVer.Close()
|
||||
fVer.Write([]byte(ver.GitCommit))
|
||||
}
|
||||
|
||||
// Setup - Extract or create local assets
|
||||
func Setup(force bool, echo bool) {
|
||||
appDir := GetRootAppDir()
|
||||
localVer := assetVersion()
|
||||
if force || localVer == "" || localVer != ver.GitCommit {
|
||||
if echo {
|
||||
fmt.Printf("Unpacking assets ...\n")
|
||||
}
|
||||
err := setupCoffLoaderExt(appDir)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
saveAssetVersion(appDir)
|
||||
}
|
||||
}
|
||||
|
||||
func setupCoffLoaderExt(appDir string) error {
|
||||
extDir := GetExtensionsDir()
|
||||
win32ExtDir := path.Join("windows", "386")
|
||||
win64ExtDir := path.Join("windows", "amd64")
|
||||
coffLoader32 := path.Join("fs", SliverExtensionsDirName, win32ExtDir, "COFFLoader.x86.dll")
|
||||
coffLoader64 := path.Join("fs", SliverExtensionsDirName, win64ExtDir, "COFFLoader.x64.dll")
|
||||
manifestPath := path.Join("fs", SliverExtensionsDirName, "manifest.json")
|
||||
loader64, err := assetsFs.ReadFile(coffLoader64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
loader32, err := assetsFs.ReadFile(coffLoader32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest, err := assetsFs.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localWin32ExtDir := path.Join(extDir, win32ExtDir)
|
||||
err = os.MkdirAll(localWin32ExtDir, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localWin64ExtDir := path.Join(extDir, win64ExtDir)
|
||||
err = os.MkdirAll(localWin64ExtDir, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(path.Join(localWin32ExtDir, "COFFLoader.x86.dll"), loader32, 0744)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(path.Join(extDir, "manifest.json"), manifest, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path.Join(localWin64ExtDir, "COFFLoader.x64.dll"), loader64, 0744)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"name": "coff-loader",
|
||||
"help": "Load and execute Beacon Object Files (BOFs) in memory.",
|
||||
"entrypoint": "LoadAndRun",
|
||||
"extFiles": [
|
||||
{
|
||||
"os": "windows",
|
||||
"files": {
|
||||
"x64": "COFFLoader.x64.dll",
|
||||
"x86": "COFFLoader.x86.dll"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
+3
-3
@@ -82,7 +82,7 @@ var rootCmd = &cobra.Command{
|
||||
func StartClientConsole() error {
|
||||
configs := assets.GetConfigs()
|
||||
if len(configs) == 0 {
|
||||
fmt.Printf("No config files found at %s or -import\n", assets.GetConfigDir())
|
||||
fmt.Printf("No config files found at %s (see --help)\n", assets.GetConfigDir())
|
||||
return nil
|
||||
}
|
||||
config := selectConfig()
|
||||
@@ -93,11 +93,11 @@ func StartClientConsole() error {
|
||||
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
|
||||
rpc, ln, err := transport.MTLSConnect(config)
|
||||
if err != nil {
|
||||
fmt.Printf("Connection to server failed %v", err)
|
||||
fmt.Printf("Connection to server failed %s", err)
|
||||
return nil
|
||||
}
|
||||
defer ln.Close()
|
||||
return console.Start(rpc, command.BindCommands, func(*console.SliverConsoleClient) {}, false)
|
||||
return console.Start(rpc, command.BindCommands, func(con *console.SliverConsoleClient) {}, false)
|
||||
}
|
||||
|
||||
// Execute - Execute root command
|
||||
|
||||
+119
-6
@@ -36,6 +36,7 @@ package command
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/backdoor"
|
||||
"github.com/bishopfox/sliver/client/command/dllhijack"
|
||||
"github.com/bishopfox/sliver/client/command/environment"
|
||||
@@ -44,9 +45,11 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/filesystem"
|
||||
"github.com/bishopfox/sliver/client/command/generate"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/command/hosts"
|
||||
"github.com/bishopfox/sliver/client/command/info"
|
||||
"github.com/bishopfox/sliver/client/command/jobs"
|
||||
"github.com/bishopfox/sliver/client/command/loot"
|
||||
"github.com/bishopfox/sliver/client/command/macros"
|
||||
"github.com/bishopfox/sliver/client/command/monitor"
|
||||
"github.com/bishopfox/sliver/client/command/network"
|
||||
"github.com/bishopfox/sliver/client/command/operators"
|
||||
@@ -82,6 +85,14 @@ func BindCommands(con *console.SliverConsoleClient) {
|
||||
con.PrintInfof("Loaded %d reaction(s) from disk\n", n)
|
||||
}
|
||||
|
||||
// Attempt to load extensions from ~/.sliver-client/extensions/
|
||||
exts, err := extensions.ParseExtensions(assets.GetExtensionsDir())
|
||||
// Absorb error in case there's no extensions manifest
|
||||
if err == nil {
|
||||
for _, ext := range exts {
|
||||
extensions.RegisterExtensionCommand(ext, con)
|
||||
}
|
||||
}
|
||||
con.App.SetPrintHelp(help.HelpCmd(con)) // Responsible for display long-form help templates, etc.
|
||||
|
||||
// [ Update ] --------------------------------------------------------------
|
||||
@@ -1177,6 +1188,8 @@ func BindCommands(con *console.SliverConsoleClient) {
|
||||
LongHelp: help.GetHelpFor([]string{consts.UploadStr}),
|
||||
Flags: func(f *grumble.Flags) {
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
|
||||
f.Bool("i", "ioc", false, "track uploaded file as an ioc")
|
||||
},
|
||||
Args: func(a *grumble.Args) {
|
||||
a.String("local-path", "local path to the file to upload")
|
||||
@@ -1525,20 +1538,20 @@ func BindCommands(con *console.SliverConsoleClient) {
|
||||
},
|
||||
})
|
||||
|
||||
// [ Extensions ] ---------------------------------------------
|
||||
// [ Macros ] ---------------------------------------------
|
||||
|
||||
con.App.AddCommand(&grumble.Command{
|
||||
Name: consts.LoadExtensionStr,
|
||||
Help: "Load a sliver extension",
|
||||
LongHelp: help.GetHelpFor([]string{consts.LoadExtensionStr}),
|
||||
Name: consts.LoadMacroStr,
|
||||
Help: "Load a sliver macro",
|
||||
LongHelp: help.GetHelpFor([]string{consts.LoadMacroStr}),
|
||||
Run: func(ctx *grumble.Context) error {
|
||||
con.Println()
|
||||
extensions.LoadExtensionCmd(ctx, con)
|
||||
macros.LoadMacroCmd(ctx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
Args: func(a *grumble.Args) {
|
||||
a.String("dir-path", "path to the extension directory")
|
||||
a.String("dir-path", "path to the macro directory")
|
||||
},
|
||||
HelpGroup: consts.GenericHelpGroup,
|
||||
})
|
||||
@@ -2112,6 +2125,70 @@ func BindCommands(con *console.SliverConsoleClient) {
|
||||
})
|
||||
con.App.AddCommand(lootCmd)
|
||||
|
||||
// [ Hosts ] --------------------------------------------------------------
|
||||
hostsCmd := &grumble.Command{
|
||||
Name: consts.HostsStr,
|
||||
Help: "Manage the database of hosts",
|
||||
LongHelp: help.GetHelpFor([]string{consts.HostsStr}),
|
||||
Flags: func(f *grumble.Flags) {
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
Run: func(ctx *grumble.Context) error {
|
||||
con.Println()
|
||||
hosts.HostsCmd(ctx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
HelpGroup: consts.GenericHelpGroup,
|
||||
}
|
||||
hostsCmd.AddCommand(&grumble.Command{
|
||||
Name: consts.RmStr,
|
||||
Help: "Remove a host from the database",
|
||||
LongHelp: help.GetHelpFor([]string{consts.HostsStr, consts.RmStr}),
|
||||
Flags: func(f *grumble.Flags) {
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
Run: func(ctx *grumble.Context) error {
|
||||
con.Println()
|
||||
hosts.HostsRmCmd(ctx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
HelpGroup: consts.GenericHelpGroup,
|
||||
})
|
||||
iocCmd := &grumble.Command{
|
||||
Name: consts.IOCStr,
|
||||
Help: "Manage tracked IOCs on a given host",
|
||||
LongHelp: help.GetHelpFor([]string{consts.HostsStr, consts.IOCStr}),
|
||||
Flags: func(f *grumble.Flags) {
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
Run: func(ctx *grumble.Context) error {
|
||||
con.Println()
|
||||
hosts.HostsIOCCmd(ctx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
HelpGroup: consts.GenericHelpGroup,
|
||||
}
|
||||
iocCmd.AddCommand(&grumble.Command{
|
||||
Name: consts.RmStr,
|
||||
Help: "Delete IOCs from the database",
|
||||
LongHelp: help.GetHelpFor([]string{consts.HostsStr, consts.IOCStr, consts.RmStr}),
|
||||
Flags: func(f *grumble.Flags) {
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
Run: func(ctx *grumble.Context) error {
|
||||
con.Println()
|
||||
hosts.HostsIOCRmCmd(ctx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
HelpGroup: consts.GenericHelpGroup,
|
||||
})
|
||||
hostsCmd.AddCommand(iocCmd)
|
||||
con.App.AddCommand(hostsCmd)
|
||||
|
||||
// [ Reactions ] -----------------------------------------------------------------
|
||||
|
||||
reactionCmd := &grumble.Command{
|
||||
@@ -2225,4 +2302,40 @@ func BindCommands(con *console.SliverConsoleClient) {
|
||||
},
|
||||
}
|
||||
con.App.AddCommand(getprivsCmd)
|
||||
|
||||
// [ Extensions ] -----------------------------------------------------------------
|
||||
extensionCmd := &grumble.Command{
|
||||
Name: consts.ExtensionStr,
|
||||
Help: "Manage sliver extensions",
|
||||
LongHelp: help.GetHelpFor([]string{consts.ExtensionStr}),
|
||||
HelpGroup: consts.SliverHelpGroup,
|
||||
Run: func(ctx *grumble.Context) error {
|
||||
con.Println()
|
||||
extensions.ListCmd(ctx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
Flags: func(f *grumble.Flags) {
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
}
|
||||
extensionCmd.AddCommand(&grumble.Command{
|
||||
Name: consts.LoadStr,
|
||||
Help: "Register a new extension",
|
||||
LongHelp: help.GetHelpFor([]string{consts.ExtensionStr, consts.LoadStr}),
|
||||
HelpGroup: consts.SliverHelpGroup,
|
||||
Run: func(ctx *grumble.Context) error {
|
||||
con.Println()
|
||||
extensions.LoadCmd(ctx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
Args: func(a *grumble.Args) {
|
||||
a.String("dir-path", "path to the extension directory")
|
||||
},
|
||||
Flags: func(f *grumble.Flags) {
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
})
|
||||
con.App.AddCommand(extensionCmd)
|
||||
}
|
||||
@@ -1,4 +1,62 @@
|
||||
Extensions
|
||||
===========
|
||||
====
|
||||
|
||||
Wraps external DLLs/tool in Sliver console commands.
|
||||
Allows to load and execute 3rd party extensions.
|
||||
|
||||
Extensions must be loaded from a directory with the following architecture:
|
||||
|
||||
```shell
|
||||
/path/to/extension/folder/
|
||||
├── manifest.json
|
||||
└── windows
|
||||
├── 386
|
||||
│ └── extension.x86.dll
|
||||
└── amd64
|
||||
└── extension.x64.dll
|
||||
```
|
||||
|
||||
The extension folder structure must follow the `GOOS/GOARCH/` scheme.
|
||||
|
||||
Here's an example manifest:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "foo",
|
||||
"help": "Help for foo command",
|
||||
"entrypoint": "RunFoo",
|
||||
"init" :"NimMain",
|
||||
"dependsOn": "bar",
|
||||
"files": [
|
||||
{
|
||||
"os": "windows",
|
||||
"files":{
|
||||
"x86": "extension.x86.o",
|
||||
"x64": "extension.x64.o",
|
||||
}
|
||||
}
|
||||
],
|
||||
"arguments": [
|
||||
{"name": "pid", "type": "int", "desc": "pid", "optional": false},
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The structure is the following one:
|
||||
|
||||
- `name`: name of the extension, which will also be the name of the command in the sliver client
|
||||
- `help`: the documentation for the new command
|
||||
- `entrypoint`: the name of the exported function to call
|
||||
- `files`: a list of object pointing to the extensions files to load for each architectures and operating systems
|
||||
- `init`: the initialization function name (if relevant, can be omitted)
|
||||
- `arguments`: an optional list of objects (for DLLs), but mandatory for BOFs
|
||||
- `dependsOn`: the name of an extension required by the current extension (won't load if the dependency is not loaded)
|
||||
|
||||
The `type` of an argument can be one of the following:
|
||||
|
||||
- `string`: regular ASCII string
|
||||
- `wstring`: string that will be UTF16 encoded
|
||||
- `int`: will be parsed as a 32 bit unsigned integer
|
||||
- `short`: will be parsed as a 16 bit unsigned integer
|
||||
- `file`: a string to a file path on the client side which content will be passed to the BOF
|
||||
+429
-241
@@ -1,26 +1,11 @@
|
||||
package extensions
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@@ -28,80 +13,59 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 60
|
||||
|
||||
windowsDefaultHostProc = `c:\windows\system32\notepad.exe`
|
||||
linuxDefaultHostProc = "/bin/bash"
|
||||
macosDefaultHostProc = "/Applications/Safari.app/Contents/MacOS/SafariForWebKitDevelopment"
|
||||
)
|
||||
|
||||
var commandMap map[string]extension
|
||||
var defaultHostProc = map[string]string{
|
||||
"windows": windowsDefaultHostProc,
|
||||
"linux": windowsDefaultHostProc,
|
||||
"darwin": macosDefaultHostProc,
|
||||
}
|
||||
var commandMap map[string]extensionCommand
|
||||
|
||||
type extensionCommand struct {
|
||||
Name string `json:"name"`
|
||||
Help string `json:"help"`
|
||||
Files []extensionFiles `json:"extFiles"`
|
||||
Arguments []extensionArgument `json:"arguments"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
DependsOn string `json:"dependsOn"`
|
||||
Init string `json:"init"`
|
||||
Path string
|
||||
}
|
||||
type binFiles struct {
|
||||
Ext64Path string `json:"x64"`
|
||||
Ext32Path string `json:"x86"`
|
||||
}
|
||||
|
||||
type extFile struct {
|
||||
type extensionFiles struct {
|
||||
OS string `json:"os"`
|
||||
Files binFiles `json:"files"`
|
||||
}
|
||||
|
||||
type extensionCommand struct {
|
||||
Name string `json:"name"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
Help string `json:"help"`
|
||||
LongHelp string `json:"longHelp"`
|
||||
AllowArgs bool `json:"allowArgs"`
|
||||
DefaultArgs string `json:"defaultArgs"`
|
||||
ExtensionFiles []extFile `json:"extFiles"`
|
||||
IsReflective bool `json:"isReflective"`
|
||||
IsAssembly bool `json:"IsAssembly"`
|
||||
type extensionArgument struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Desc string `json:"desc"`
|
||||
Optional bool `json:"optional"`
|
||||
}
|
||||
|
||||
func (ec *extensionCommand) getDefaultProcess(targetOS string) (proc string, err error) {
|
||||
proc, ok := defaultHostProc[targetOS]
|
||||
if !ok {
|
||||
err = fmt.Errorf("no default process for %s target, please specify one", targetOS)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type extension struct {
|
||||
Name string `json:"extensionName"`
|
||||
Commands []extensionCommand `json:"extensionCommands"`
|
||||
Path string
|
||||
}
|
||||
|
||||
func (e *extension) getFileForTarget(cmdName string, targetOS string, targetArch string) (filePath string, err error) {
|
||||
for _, c := range e.Commands {
|
||||
if cmdName == c.Name {
|
||||
for _, ef := range c.ExtensionFiles {
|
||||
if targetOS == ef.OS {
|
||||
switch targetArch {
|
||||
case "x86":
|
||||
filePath = fmt.Sprintf("%s/%s", e.Path, ef.Files.Ext32Path)
|
||||
case "x64":
|
||||
filePath = fmt.Sprintf("%s/%s", e.Path, ef.Files.Ext64Path)
|
||||
default:
|
||||
filePath = fmt.Sprintf("%s/%s", e.Path, ef.Files.Ext64Path)
|
||||
}
|
||||
}
|
||||
func (e *extensionCommand) getFileForTarget(cmdName string, targetOS string, targetArch string) (filePath string, err error) {
|
||||
for _, ef := range e.Files {
|
||||
if targetOS == ef.OS {
|
||||
switch targetArch {
|
||||
case "x86":
|
||||
filePath = filepath.Join(e.Path, targetOS, targetArch, ef.Files.Ext32Path)
|
||||
case "x64":
|
||||
filePath = filepath.Join(e.Path, targetOS, targetArch, ef.Files.Ext64Path)
|
||||
default:
|
||||
filePath = filepath.Join(e.Path, targetOS, targetArch, ef.Files.Ext64Path)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if filePath == "" {
|
||||
@@ -110,132 +74,239 @@ func (e *extension) getFileForTarget(cmdName string, targetOS string, targetArch
|
||||
return
|
||||
}
|
||||
|
||||
func (e *extension) getCommandFromName(name string) (extCmd *extensionCommand, err error) {
|
||||
for _, x := range e.Commands {
|
||||
if x.Name == name {
|
||||
extCmd = &x
|
||||
return
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("no extension command found for name %s", name)
|
||||
return
|
||||
}
|
||||
|
||||
// LoadExtensionCmd - Locally load an extension into the Sliver shell.
|
||||
func LoadExtensionCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
func LoadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
dirPath := ctx.Args.String("dir-path")
|
||||
if dirPath == "" {
|
||||
con.PrintErrorf("Please provide an extension path\n")
|
||||
extCmds, err := ParseExtensions(dirPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve extension manifest
|
||||
manifestPath := fmt.Sprintf("%s/%s", dirPath, "manifest.json")
|
||||
for _, extCmd := range extCmds {
|
||||
// do not add if the command already exists
|
||||
if cmdExists(extCmd.Name, con.App) {
|
||||
con.PrintErrorf("%s command already exists\n", extCmd.Name)
|
||||
continue
|
||||
}
|
||||
RegisterExtensionCommand(extCmd, con)
|
||||
con.PrintInfof("Added %s command: %s\n", extCmd.Name, extCmd.Help)
|
||||
}
|
||||
}
|
||||
|
||||
func ParseExtensions(extPath string) ([]*extensionCommand, error) {
|
||||
manifestPath := filepath.Join(extPath, "manifest.json")
|
||||
jsonBytes, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
// parse it
|
||||
ext := &extension{}
|
||||
err = json.Unmarshal(jsonBytes, ext)
|
||||
extensionsCmds := make([]*extensionCommand, 0)
|
||||
err = json.Unmarshal(jsonBytes, &extensionsCmds)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error loading extension: %v", err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
ext.Path = dirPath
|
||||
// for each extension command, add a new app command
|
||||
for _, extCmd := range ext.Commands {
|
||||
// do not add if the command already exists
|
||||
if cmdExists(extCmd.Name, ctx.App) {
|
||||
con.PrintErrorf("%s command already exists\n", extCmd.Name)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Adding %s command: %s\n", extCmd.Name, extCmd.Help)
|
||||
|
||||
// Have to use a global map here, as passing the extCmd
|
||||
// either by value or by ref fucks things up
|
||||
commandMap[extCmd.Name] = *ext
|
||||
helpMsg := fmt.Sprintf("[%s] %s", ext.Name, extCmd.Help)
|
||||
extensionCmd := &grumble.Command{
|
||||
Name: extCmd.Name,
|
||||
Help: helpMsg,
|
||||
LongHelp: help.FormatHelpTmpl(extCmd.LongHelp),
|
||||
Run: func(extCtx *grumble.Context) error {
|
||||
con.Println()
|
||||
runExtensionCommand(extCtx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
Flags: func(f *grumble.Flags) {
|
||||
if extCmd.IsAssembly {
|
||||
f.String("m", "method", "", "Optional method (a method is required for a .NET DLL)")
|
||||
f.String("c", "class", "", "Optional class name (required for .NET DLL)")
|
||||
f.String("d", "app-domain", "", "AppDomain name to create for .NET assembly. Generated randomly if not set.")
|
||||
f.String("a", "arch", "x84", "Assembly target architecture: x86, x64, x84 (x86+x64)")
|
||||
}
|
||||
f.String("p", "process", "", "Path to process to host the shared object")
|
||||
f.Bool("s", "save", false, "Save output to disk")
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
Args: func(a *grumble.Args) {
|
||||
a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
},
|
||||
HelpGroup: consts.ExtensionHelpGroup,
|
||||
}
|
||||
con.App.AddCommand(extensionCmd)
|
||||
for _, extCmd := range extensionsCmds {
|
||||
extCmd.Path = extPath
|
||||
commandMap[extCmd.Name] = *extCmd
|
||||
}
|
||||
con.PrintInfof("%s extension has been loaded\n", ext.Name)
|
||||
return extensionsCmds, nil
|
||||
}
|
||||
|
||||
func runExtensionCommand(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
func RegisterExtensionCommand(extCmd *extensionCommand, con *console.SliverConsoleClient) {
|
||||
commandMap[extCmd.Name] = *extCmd
|
||||
helpMsg := extCmd.Help
|
||||
extensionCmd := &grumble.Command{
|
||||
Name: extCmd.Name,
|
||||
Help: helpMsg,
|
||||
Run: func(extCtx *grumble.Context) error {
|
||||
con.Println()
|
||||
runExtensionCmd(extCtx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
Flags: func(f *grumble.Flags) {
|
||||
f.Bool("s", "save", false, "Save output to disk")
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
Args: func(a *grumble.Args) {
|
||||
if len(extCmd.Arguments) > 0 {
|
||||
// BOF specific
|
||||
for _, arg := range extCmd.Arguments {
|
||||
var (
|
||||
argFunc func(string, string, ...grumble.ArgOption)
|
||||
defaultValue grumble.ArgOption
|
||||
)
|
||||
switch arg.Type {
|
||||
case "int", "short":
|
||||
argFunc = a.Int
|
||||
defaultValue = grumble.Default(0)
|
||||
case "string", "wstring", "file":
|
||||
argFunc = a.String
|
||||
defaultValue = grumble.Default("")
|
||||
}
|
||||
if arg.Optional {
|
||||
argFunc(arg.Name, arg.Desc, defaultValue)
|
||||
} else {
|
||||
argFunc(arg.Name, arg.Desc)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
}
|
||||
},
|
||||
HelpGroup: consts.ExtensionHelpGroup,
|
||||
}
|
||||
con.App.AddCommand(extensionCmd)
|
||||
}
|
||||
|
||||
func ListCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
extList, err := con.Rpc.ListExtensions(context.Background(), &sliverpb.ListExtensionsReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if extList.Response != nil && extList.Response.Err != "" {
|
||||
con.PrintErrorf("Error: %s", extList.Response.Err)
|
||||
return
|
||||
}
|
||||
if len(extList.Names) > 0 {
|
||||
con.PrintInfof("Loaded extensions:\n")
|
||||
for _, ext := range extList.Names {
|
||||
con.Printf("- %s\n", ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadExtension(ctx *grumble.Context, session *clientpb.Session, con *console.SliverConsoleClient, ext *extensionCommand) error {
|
||||
var extensionList []string
|
||||
binPath, err := ext.getFileForTarget(ctx.Command.Name, session.OS, session.Arch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Try to find the extension in the loaded extensions
|
||||
if len(session.Extensions) == 0 {
|
||||
extList, err := con.Rpc.ListExtensions(context.Background(), &sliverpb.ListExtensionsReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
if extList.Response != nil && extList.Response.Err != "" {
|
||||
return errors.New(extList.Response.Err)
|
||||
}
|
||||
extensionList = extList.Names
|
||||
} else {
|
||||
extensionList = session.Extensions
|
||||
}
|
||||
depLoaded := false
|
||||
for _, extName := range extensionList {
|
||||
if !depLoaded && extName == ext.DependsOn {
|
||||
depLoaded = true
|
||||
}
|
||||
if ext.Name == extName {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Extension not found, let's load it
|
||||
if filepath.Ext(binPath) == ".o" {
|
||||
// BOFs are not loaded by the DLL loader, but we make sure the loader itself is loaded
|
||||
// Auto load the coff loader if we have it
|
||||
if !depLoaded {
|
||||
if errLoad := loadDep(session, con, ctx, ext.DependsOn); errLoad != nil {
|
||||
return errLoad
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
binData, err := ioutil.ReadFile(binPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if errRegister := registerExtension(con, ext, binData, session, ctx); errRegister != nil {
|
||||
return errRegister
|
||||
}
|
||||
// Update session info
|
||||
if filepath.Ext(binPath) != ".o" {
|
||||
// Don't update session for BOFs, we don't cache them
|
||||
// on the implant side (yet)
|
||||
// update the Session info
|
||||
_, err = con.Rpc.UpdateSession(context.Background(), &clientpb.UpdateSession{
|
||||
Extensions: extensionList,
|
||||
SessionID: session.ID,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerExtension(con *console.SliverConsoleClient, ext *extensionCommand, binData []byte, session *clientpb.Session, ctx *grumble.Context) error {
|
||||
registerResp, err := con.Rpc.RegisterExtension(context.Background(), &sliverpb.RegisterExtensionReq{
|
||||
Name: ext.Name,
|
||||
Data: binData,
|
||||
OS: session.OS,
|
||||
Init: ext.Init,
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if registerResp.Response != nil && registerResp.Response.Err != "" {
|
||||
return errors.New(registerResp.Response.Err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadDep(session *clientpb.Session, con *console.SliverConsoleClient, ctx *grumble.Context, depName string) error {
|
||||
depExt, f := commandMap[depName]
|
||||
if f {
|
||||
depBinPath, err := depExt.getFileForTarget(depExt.Name, session.OS, session.Arch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
depBinData, err := ioutil.ReadFile(depBinPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return registerExtension(con, &depExt, depBinData, session, ctx)
|
||||
}
|
||||
return fmt.Errorf("missing dependency %s", depName)
|
||||
}
|
||||
|
||||
func runExtensionCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
var (
|
||||
callExtension *sliverpb.CallExtension
|
||||
err error
|
||||
extensionArgs []byte
|
||||
extName string
|
||||
entryPoint string
|
||||
)
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ext, ok := commandMap[ctx.Command.Name]
|
||||
if !ok {
|
||||
con.PrintErrorf("No extension command found for `%s` command\n", ctx.Command.Name)
|
||||
return
|
||||
}
|
||||
|
||||
binPath, err := ext.getFileForTarget(ctx.Command.Name, session.GetOS(), session.GetArch())
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
if err = loadExtension(ctx, session, con, &ext); err != nil {
|
||||
con.PrintErrorf("Error: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := ext.getCommandFromName(ctx.Command.Name)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
args := ctx.Args.StringList("arguments")
|
||||
var extArgs string
|
||||
if len(c.DefaultArgs) != 0 && len(args) == 0 {
|
||||
extArgs = c.DefaultArgs
|
||||
} else {
|
||||
extArgs = strings.Join(args, " ")
|
||||
}
|
||||
entryPoint := c.Entrypoint
|
||||
processName := ctx.Flags.String("process")
|
||||
if processName == "" {
|
||||
processName, err = c.getDefaultProcess(session.GetOS())
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
isDLL := false
|
||||
if filepath.Ext(binPath) == ".dll" {
|
||||
isDLL = true
|
||||
}
|
||||
binData, err := ioutil.ReadFile(binPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
var outFilePath *os.File
|
||||
if ctx.Flags.Bool("save") {
|
||||
outFile := path.Base(fmt.Sprintf("%s_%s*.log", ctx.Command.Name, session.GetHostname()))
|
||||
@@ -245,86 +316,142 @@ func runExtensionCommand(ctx *grumble.Context, con *console.SliverConsoleClient)
|
||||
return
|
||||
}
|
||||
}
|
||||
if c.IsAssembly {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, extArgs)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
executeAssemblyResp, err := con.Rpc.ExecuteAssembly(context.Background(), &sliverpb.ExecuteAssemblyReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
IsDLL: isDLL,
|
||||
Process: processName,
|
||||
Arguments: extArgs,
|
||||
Assembly: binData,
|
||||
Arch: ctx.Flags.String("arch"),
|
||||
Method: ctx.Flags.String("method"),
|
||||
ClassName: ctx.Flags.String("class"),
|
||||
AppDomain: ctx.Flags.String("app-domain"),
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
binPath, err := ext.getFileForTarget(ctx.Command.Name, session.OS, session.Arch)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
isBOF := filepath.Ext(binPath) == ".o"
|
||||
|
||||
// BOFs (Beacon Object Files) are a specific kind of extensions
|
||||
// than require another extension (a COFF loader) to be present.
|
||||
// BOFs also have strongly typed arguments that need to be parsed in the proper way.
|
||||
// This block will pack both the BOF data and its arguments into a single buffer that
|
||||
// the loader will extract and load.
|
||||
if isBOF {
|
||||
// Beacon Object File -- requires a COFF loader
|
||||
extensionArgs, err = getBOFArgs(ctx, binPath, ext)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
con.PrintErrorf("Error: %s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Output:\n%s", string(executeAssemblyResp.GetOutput()))
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write(executeAssemblyResp.GetOutput())
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
} else if c.IsReflective {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, extArgs)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
spawnDllResp, err := con.Rpc.SpawnDll(context.Background(), &sliverpb.InvokeSpawnDllReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Args: strings.Trim(extArgs, " "),
|
||||
Data: binData,
|
||||
ProcessName: processName,
|
||||
EntryPoint: c.Entrypoint,
|
||||
Kill: true,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.PrintInfof("Output:\n%s", spawnDllResp.GetResult())
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write([]byte(spawnDllResp.GetResult()))
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
extName = ext.DependsOn
|
||||
entryPoint = commandMap[extName].Entrypoint // should exist at this point
|
||||
} else {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, extArgs)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
sideloadResp, err := con.Rpc.Sideload(context.Background(), &sliverpb.SideloadReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Args: extArgs,
|
||||
Data: binData,
|
||||
EntryPoint: entryPoint,
|
||||
ProcessName: processName,
|
||||
Kill: true,
|
||||
IsDLL: isDLL,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.PrintInfof("Output:\n%s", sideloadResp.GetResult())
|
||||
// Regular DLL
|
||||
extArgs := strings.Join(ctx.Args.StringList("arguments"), " ")
|
||||
extensionArgs = []byte(extArgs)
|
||||
extName = ext.Name
|
||||
entryPoint = ext.Entrypoint
|
||||
}
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s ...", ctx.Command.Name)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
callExtension, err = con.Rpc.CallExtension(context.Background(), &sliverpb.CallExtensionReq{
|
||||
Name: extName,
|
||||
Export: entryPoint,
|
||||
Args: extensionArgs,
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
if callExtension.Response != nil && callExtension.Response.Err != "" {
|
||||
con.PrintErrorf("Error: %s\n", callExtension.Response.Err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Successfully executed %s\n", extName)
|
||||
if len(callExtension.Output) > 0 {
|
||||
con.PrintInfof("Got output:\n%s", string(callExtension.Output))
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write([]byte(sideloadResp.GetResult()))
|
||||
outFilePath.Write(callExtension.Output)
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getBOFArgs(ctx *grumble.Context, binPath string, ext extensionCommand) ([]byte, error) {
|
||||
var extensionArgs []byte
|
||||
binData, err := ioutil.ReadFile(binPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
argsBuffer := BOFArgsBuffer{
|
||||
Buffer: new(bytes.Buffer),
|
||||
}
|
||||
// Parse BOF arguments from grumble
|
||||
for _, arg := range ext.Arguments {
|
||||
switch arg.Type {
|
||||
case "int":
|
||||
val := ctx.Args.Int(arg.Name)
|
||||
err = argsBuffer.AddInt(uint32(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "short":
|
||||
val := ctx.Args.Int(arg.Name)
|
||||
err = argsBuffer.AddShort(uint16(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "string":
|
||||
val := ctx.Args.String(arg.Name)
|
||||
err = argsBuffer.AddString(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "wstring":
|
||||
val := ctx.Args.String(arg.Name)
|
||||
err = argsBuffer.AddWString(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Adding support for filepaths so we can
|
||||
// send binary data like shellcodes to BOFs
|
||||
case "file":
|
||||
val := ctx.Args.String(arg.Name)
|
||||
data, err := ioutil.ReadFile(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = argsBuffer.AddData(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
parsedArgs, err := argsBuffer.GetBuffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Now build the extension's argument buffer
|
||||
extensionArgsBuffer := BOFArgsBuffer{
|
||||
Buffer: new(bytes.Buffer),
|
||||
}
|
||||
err = extensionArgsBuffer.AddString(ext.Entrypoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = extensionArgsBuffer.AddData(binData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = extensionArgsBuffer.AddData(parsedArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extensionArgs, err = extensionArgsBuffer.GetBuffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extensionArgs, nil
|
||||
|
||||
}
|
||||
|
||||
func cmdExists(name string, app *grumble.App) bool {
|
||||
for _, c := range app.Commands().All() {
|
||||
if name == c.Name {
|
||||
@@ -335,5 +462,66 @@ func cmdExists(name string, app *grumble.App) bool {
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandMap = make(map[string]extension)
|
||||
commandMap = make(map[string]extensionCommand)
|
||||
}
|
||||
|
||||
// BOF Specific code
|
||||
|
||||
type BOFArgsBuffer struct {
|
||||
Buffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *BOFArgsBuffer) AddData(d []byte) error {
|
||||
dataLen := uint32(len(d))
|
||||
err := binary.Write(b.Buffer, binary.LittleEndian, &dataLen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return binary.Write(b.Buffer, binary.LittleEndian, &d)
|
||||
}
|
||||
|
||||
func (b *BOFArgsBuffer) AddShort(d uint16) error {
|
||||
return binary.Write(b.Buffer, binary.LittleEndian, &d)
|
||||
}
|
||||
|
||||
func (b *BOFArgsBuffer) AddInt(d uint32) error {
|
||||
return binary.Write(b.Buffer, binary.LittleEndian, &d)
|
||||
}
|
||||
|
||||
func (b *BOFArgsBuffer) AddString(d string) error {
|
||||
stringLen := uint32(len(d)) + 1
|
||||
err := binary.Write(b.Buffer, binary.LittleEndian, &stringLen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dBytes := append([]byte(d), 0x00)
|
||||
return binary.Write(b.Buffer, binary.LittleEndian, dBytes)
|
||||
}
|
||||
|
||||
func (b *BOFArgsBuffer) AddWString(d string) error {
|
||||
encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
|
||||
strBytes := append([]byte(d), 0x00)
|
||||
utf16Data, err := encoder.Bytes(strBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stringLen := uint32(len(utf16Data))
|
||||
err = binary.Write(b.Buffer, binary.LittleEndian, &stringLen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return binary.Write(b.Buffer, binary.LittleEndian, utf16Data)
|
||||
}
|
||||
|
||||
func (b *BOFArgsBuffer) GetBuffer() ([]byte, error) {
|
||||
outBuffer := new(bytes.Buffer)
|
||||
err := binary.Write(outBuffer, binary.LittleEndian, uint32(b.Buffer.Len()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = binary.Write(outBuffer, binary.LittleEndian, b.Buffer.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return outBuffer.Bytes(), nil
|
||||
}
|
||||
@@ -41,6 +41,7 @@ func UploadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
localPath := ctx.Args.String("local-path")
|
||||
remotePath := ctx.Args.String("remote-path")
|
||||
isIOC := ctx.Flags.Bool("ioc")
|
||||
|
||||
if localPath == "" {
|
||||
con.PrintErrorf("Missing parameter, see `help upload`\n")
|
||||
@@ -70,6 +71,7 @@ func UploadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
Path: dst,
|
||||
Data: uploadGzip,
|
||||
Encoder: "gzip",
|
||||
IsIOC: isIOC,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
@@ -50,7 +50,7 @@ func printHelp(con *console.SliverConsoleClient) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if key == consts.SliverHelpGroup || key == consts.SliverWinHelpGroup || key == consts.ExtensionHelpGroup {
|
||||
if key == consts.SliverHelpGroup || key == consts.SliverWinHelpGroup || key == consts.MacroHelpGroup || key == consts.ExtensionHelpGroup {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ var (
|
||||
consts.MigrateStr: migrateHelp,
|
||||
consts.SideloadStr: sideloadHelp,
|
||||
consts.TerminateStr: terminateHelp,
|
||||
consts.LoadExtensionStr: loadExtensionHelp,
|
||||
consts.LoadMacroStr: loadMacrosHelp,
|
||||
consts.PsExecStr: psExecHelp,
|
||||
consts.BackdoorStr: backdoorHelp,
|
||||
consts.SpawnDllStr: spawnDllHelp,
|
||||
@@ -89,6 +89,7 @@ var (
|
||||
consts.WgSocksStr: wgSocksHelp,
|
||||
consts.SSHStr: sshHelp,
|
||||
consts.DLLHijackStr: dllHijackHelp,
|
||||
consts.GetPrivsStr: getPrivsHelp,
|
||||
|
||||
// Loot
|
||||
consts.LootStr: lootHelp,
|
||||
@@ -345,21 +346,21 @@ Parameters to the Linux and MacOS shared module are passed using the [[.Bold]]LD
|
||||
screenshotHelp = `[[.Bold]]Command:[[.Normal]] screenshot
|
||||
[[.Bold]]About:[[.Normal]] Take a screenshot from the remote implant.
|
||||
`
|
||||
loadExtensionHelp = `[[.Bold]]Command:[[.Normal]] load-extension <directory path>
|
||||
[[.Bold]]About:[[.Normal]] Load a Sliver extension to add new commands.
|
||||
Extensions are using the [[.Bold]]sideload[[.Normal]] or [[.Bold]]spawndll[[.Normal]] commands under the hood, depending on the use case.
|
||||
For Linux and Mac OS, the [[.Bold]]sideload[[.Normal]] command will be used. On Windows, it will depend the extension file is a reflective DLL or not.
|
||||
Load an extension:
|
||||
loadMacrosHelp = `[[.Bold]]Command:[[.Normal]] load-macro <directory path>
|
||||
[[.Bold]]About:[[.Normal]] Load a Sliver macro to add new commands.
|
||||
Macros are using the [[.Bold]]sideload[[.Normal]] or [[.Bold]]spawndll[[.Normal]] commands under the hood, depending on the use case.
|
||||
For Linux and Mac OS, the [[.Bold]]sideload[[.Normal]] command will be used. On Windows, it will depend the macro file is a reflective DLL or not.
|
||||
Load an macro:
|
||||
load /tmp/chrome-dump
|
||||
Sliver extensions have the following structure (example for the [[.Bold]]chrome-dump[[.Normal]] extension):
|
||||
Sliver macros have the following structure (example for the [[.Bold]]chrome-dump[[.Normal]] macro):
|
||||
chrome-dump
|
||||
├── chrome-dump.dll
|
||||
├── chrome-dump.so
|
||||
└── manifest.json
|
||||
It is a directory containing any number of files, with a mandatory [[.Bold]]manifest.json[[.Normal]], that has the following structure:
|
||||
{
|
||||
"extensionName":"chrome-dump", // name of the extension, can be anything
|
||||
"extensionCommands":[
|
||||
"macroName":"chrome-dump", // name of the macro, can be anything
|
||||
"macroCommands":[
|
||||
{
|
||||
"name":"chrome-dump", // name of the command available in the sliver client (no space)
|
||||
"entrypoint":"ChromeDump", // entrypoint of the shared library to execute
|
||||
@@ -371,7 +372,7 @@ It is a directory containing any number of files, with a mandatory [[.Bold]]mani
|
||||
"os":"windows", // Target OS for the following files. Values can be "windows", "linux" or "darwin"
|
||||
"files":{
|
||||
"x64":"chrome-dump.dll",
|
||||
"x86":"chrome-dump.x86.dll" // only x86 and x64 arch are supported, path is relative to the extension directory
|
||||
"x86":"chrome-dump.x86.dll" // only x86 and x64 arch are supported, path is relative to the macro directory
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -558,7 +559,10 @@ loot fetch
|
||||
`
|
||||
|
||||
reactionHelp = fmt.Sprintf(`[[.Bold]]Command:[[.Normal]] reaction
|
||||
[[.Bold]]About:[[.Normal]] Automate commands in reaction to event(s).
|
||||
[[.Bold]]About:[[.Normal]] Automate commands in reaction to event(s). The built-in
|
||||
reactions do not support variables or logic, they simply allow you to run verbatim
|
||||
commands when an event occurs. To implement complex event-based logic we recommend
|
||||
using SliverPy (Python) or sliver-script (TypeScript/JavaScript).
|
||||
|
||||
[[.Bold]]Reactable Events:[[.Normal]]
|
||||
% 20s Triggered when a new session is opened to a target
|
||||
@@ -579,7 +583,11 @@ loot fetch
|
||||
)
|
||||
|
||||
reactionSetHelp = fmt.Sprintf(`[[.Bold]]Command:[[.Normal]] reaction set
|
||||
[[.Bold]]About:[[.Normal]] Set automated commands in reaction to event(s).
|
||||
[[.Bold]]About:[[.Normal]] Set automated commands in reaction to event(s).
|
||||
|
||||
The built-in reactions do not support variables or logic, they simply allow you to
|
||||
run verbatim commands when an event occurs. To implement complex event-based logic
|
||||
we recommend using SliverPy (Python) or sliver-script (TypeScript/JavaScript).
|
||||
|
||||
[[.Bold]]Examples:[[.Normal]]
|
||||
# The command uses interactive menus to build a reaction. Simply run:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Hosts
|
||||
======
|
||||
|
||||
Command to access and manage host information.
|
||||
@@ -0,0 +1,46 @@
|
||||
package hosts
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 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 (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// HostsIOCRmCmd - Remove a host from the database
|
||||
func HostsIOCRmCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
host, err := SelectHost(con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
ioc, err := SelectHostIOC(host, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
_, err = con.Rpc.HostIOCRm(context.Background(), ioc)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Removed host from database\n")
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package hosts
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
// HostsIOCCmd - Remove a host from the database
|
||||
func HostsIOCCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
host, err := SelectHost(con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if 0 < len(host.IOCs) {
|
||||
con.Printf("%s\n", hostIOCsTable(host, con))
|
||||
} else {
|
||||
con.Println()
|
||||
con.PrintInfof("No IOCs tracked on host\n")
|
||||
}
|
||||
}
|
||||
|
||||
func hostIOCsTable(host *clientpb.Host, con *console.SliverConsoleClient) string {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(table.StyleBold)
|
||||
tw.AppendHeader(table.Row{"File Path", "SHA-256"})
|
||||
for _, ioc := range host.IOCs {
|
||||
tw.AppendRow(table.Row{
|
||||
ioc.Path,
|
||||
ioc.FileHash,
|
||||
})
|
||||
}
|
||||
return tw.Render()
|
||||
}
|
||||
|
||||
func SelectHostIOC(host *clientpb.Host, con *console.SliverConsoleClient) (*clientpb.IOC, error) {
|
||||
|
||||
// Sort the keys because maps have a randomized order, these keys must be ordered for the selection
|
||||
// to work properly since we rely on the index of the user's selection to find the session in the map
|
||||
var keys []string
|
||||
var iocMap = make(map[string]*clientpb.IOC)
|
||||
for _, ioc := range host.IOCs {
|
||||
keys = append(keys, ioc.ID)
|
||||
iocMap[ioc.ID] = ioc
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
outputBuf := bytes.NewBufferString("")
|
||||
table := tabwriter.NewWriter(outputBuf, 0, 2, 2, ' ', 0)
|
||||
|
||||
// Column Headers
|
||||
for _, key := range keys {
|
||||
ioc := iocMap[key]
|
||||
fmt.Fprintf(table, "%s\t%s\t\n",
|
||||
ioc.Path,
|
||||
ioc.FileHash,
|
||||
)
|
||||
}
|
||||
table.Flush()
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select an IOC:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
survey.AskOne(prompt, &selected)
|
||||
if selected == "" {
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
// Go from the selected option -> index -> host
|
||||
for index, option := range options {
|
||||
if option == selected {
|
||||
return iocMap[keys[index]], nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package hosts
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 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 (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// HostsRmCmd - Remove a host from the database
|
||||
func HostsRmCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
host, err := SelectHost(con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
_, err = con.Rpc.HostRm(context.Background(), host)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Removed host from database\n")
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package hosts
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/desertbit/grumble"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoHosts - No hosts in database
|
||||
ErrNoHosts = errors.New("no hosts")
|
||||
// ErrNoSelection - No selection made
|
||||
ErrNoSelection = errors.New("no selection")
|
||||
)
|
||||
|
||||
// HostsCmd - Main hosts command
|
||||
func HostsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
allHosts, err := con.Rpc.Hosts(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s", err)
|
||||
return
|
||||
}
|
||||
if 0 < len(allHosts.Hosts) {
|
||||
con.Printf("%s\n", hostsTable(allHosts.Hosts, con))
|
||||
} else {
|
||||
con.PrintInfof("No hosts\n")
|
||||
}
|
||||
}
|
||||
|
||||
func hostsTable(hosts []*clientpb.Host, con *console.SliverConsoleClient) string {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(table.StyleLight)
|
||||
tw.AppendHeader(table.Row{
|
||||
"Hostname",
|
||||
"Operating System",
|
||||
"Sessions",
|
||||
"IOCs",
|
||||
"Extension Data",
|
||||
"ID (Short)",
|
||||
})
|
||||
for _, host := range hosts {
|
||||
var shortID string
|
||||
if len(host.HostUUID) < 8 {
|
||||
shortID = host.HostUUID[:len(host.HostUUID)]
|
||||
} else {
|
||||
shortID = host.HostUUID[:8]
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
host.Hostname,
|
||||
host.OSVersion,
|
||||
hostSessionNumbers(host.HostUUID, con),
|
||||
len(host.IOCs),
|
||||
len(host.ExtensionData),
|
||||
shortID,
|
||||
})
|
||||
}
|
||||
return tw.Render()
|
||||
}
|
||||
|
||||
func hostSessionNumbers(hostUUID string, con *console.SliverConsoleClient) string {
|
||||
hostSessions := SessionsForHost(hostUUID, con)
|
||||
if 0 == len(hostSessions) {
|
||||
return "None"
|
||||
}
|
||||
sessionNumbers := []string{}
|
||||
for _, hostSession := range hostSessions {
|
||||
sessionNumbers = append(sessionNumbers, fmt.Sprintf("%d", hostSession.ID))
|
||||
}
|
||||
return strings.Join(sessionNumbers, ", ")
|
||||
}
|
||||
|
||||
func SessionsForHost(hostUUID string, con *console.SliverConsoleClient) []*clientpb.Session {
|
||||
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return []*clientpb.Session{}
|
||||
}
|
||||
hostSessions := []*clientpb.Session{}
|
||||
for _, session := range sessions.Sessions {
|
||||
if session.UUID == hostUUID {
|
||||
hostSessions = append(hostSessions, session)
|
||||
}
|
||||
}
|
||||
return hostSessions
|
||||
}
|
||||
|
||||
// SelectHost - Interactively select a host from the database
|
||||
func SelectHost(con *console.SliverConsoleClient) (*clientpb.Host, error) {
|
||||
allHosts, err := con.Rpc.Hosts(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Sort the keys because maps have a randomized order, these keys must be ordered for the selection
|
||||
// to work properly since we rely on the index of the user's selection to find the session in the map
|
||||
var keys []string
|
||||
var hostMap = make(map[string]*clientpb.Host)
|
||||
for _, host := range allHosts.Hosts {
|
||||
keys = append(keys, host.HostUUID)
|
||||
hostMap[host.HostUUID] = host
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
outputBuf := bytes.NewBufferString("")
|
||||
table := tabwriter.NewWriter(outputBuf, 0, 2, 2, ' ', 0)
|
||||
|
||||
// Column Headers
|
||||
for _, key := range keys {
|
||||
host := hostMap[key]
|
||||
fmt.Fprintf(table, "%s\t%s\t\n",
|
||||
host.Hostname,
|
||||
host.OSVersion,
|
||||
)
|
||||
}
|
||||
table.Flush()
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a host:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
survey.AskOne(prompt, &selected)
|
||||
if selected == "" {
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
// Go from the selected option -> index -> host
|
||||
for index, option := range options {
|
||||
if option == selected {
|
||||
return hostMap[keys[index]], nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
Extensions
|
||||
===========
|
||||
|
||||
Wraps external DLLs/tool in Sliver console commands.
|
||||
@@ -0,0 +1,335 @@
|
||||
package macros
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 60
|
||||
|
||||
windowsDefaultHostProc = `c:\windows\system32\notepad.exe`
|
||||
linuxDefaultHostProc = "/bin/bash"
|
||||
macosDefaultHostProc = "/Applications/Safari.app/Contents/MacOS/SafariForWebKitDevelopment"
|
||||
)
|
||||
|
||||
var commandMap map[string]macro
|
||||
var defaultHostProc = map[string]string{
|
||||
"windows": windowsDefaultHostProc,
|
||||
"linux": windowsDefaultHostProc,
|
||||
"darwin": macosDefaultHostProc,
|
||||
}
|
||||
|
||||
type binFiles struct {
|
||||
Ext64Path string `json:"x64"`
|
||||
Ext32Path string `json:"x86"`
|
||||
}
|
||||
|
||||
type macroFile struct {
|
||||
OS string `json:"os"`
|
||||
Files binFiles `json:"files"`
|
||||
}
|
||||
|
||||
type macroCommand struct {
|
||||
Name string `json:"name"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
Help string `json:"help"`
|
||||
LongHelp string `json:"longHelp"`
|
||||
AllowArgs bool `json:"allowArgs"`
|
||||
DefaultArgs string `json:"defaultArgs"`
|
||||
MacroFiles []macroFile `json:"extFiles"`
|
||||
IsReflective bool `json:"isReflective"`
|
||||
IsAssembly bool `json:"IsAssembly"`
|
||||
}
|
||||
|
||||
func (ec *macroCommand) getDefaultProcess(targetOS string) (proc string, err error) {
|
||||
proc, ok := defaultHostProc[targetOS]
|
||||
if !ok {
|
||||
err = fmt.Errorf("no default process for %s target, please specify one", targetOS)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type macro struct {
|
||||
Name string `json:"macroName"`
|
||||
Commands []macroCommand `json:"macroCommands"`
|
||||
Path string
|
||||
}
|
||||
|
||||
func (e *macro) getFileForTarget(cmdName string, targetOS string, targetArch string) (filePath string, err error) {
|
||||
for _, c := range e.Commands {
|
||||
if cmdName == c.Name {
|
||||
for _, ef := range c.MacroFiles {
|
||||
if targetOS == ef.OS {
|
||||
switch targetArch {
|
||||
case "x86":
|
||||
filePath = filepath.Join(e.Path, ef.Files.Ext32Path)
|
||||
case "x64":
|
||||
filePath = filepath.Join(e.Path, ef.Files.Ext64Path)
|
||||
default:
|
||||
filePath = filepath.Join(e.Path, ef.Files.Ext64Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if filePath == "" {
|
||||
err = fmt.Errorf("no macro file found for %s/%s", targetOS, targetArch)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (e *macro) getCommandFromName(name string) (extCmd *macroCommand, err error) {
|
||||
for _, x := range e.Commands {
|
||||
if x.Name == name {
|
||||
extCmd = &x
|
||||
return
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("no macro command found for name %s", name)
|
||||
return
|
||||
}
|
||||
|
||||
// LoadMacroCmd - Locally load a macro into the Sliver shell.
|
||||
func LoadMacroCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
dirPath := ctx.Args.String("dir-path")
|
||||
|
||||
// retrieve macro manifest
|
||||
manifestPath := fmt.Sprintf("%s/%s", dirPath, "manifest.json")
|
||||
jsonBytes, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
}
|
||||
// parse it
|
||||
ext := ¯o{}
|
||||
err = json.Unmarshal(jsonBytes, ext)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error loading macro: %v", err)
|
||||
return
|
||||
}
|
||||
ext.Path = dirPath
|
||||
// for each macro command, add a new app command
|
||||
for _, extCmd := range ext.Commands {
|
||||
// do not add if the command already exists
|
||||
if cmdExists(extCmd.Name, ctx.App) {
|
||||
con.PrintErrorf("%s command already exists\n", extCmd.Name)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Adding %s command: %s\n", extCmd.Name, extCmd.Help)
|
||||
|
||||
// Have to use a global map here, as passing the extCmd
|
||||
// either by value or by ref fucks things up
|
||||
commandMap[extCmd.Name] = *ext
|
||||
helpMsg := fmt.Sprintf("[%s] %s", ext.Name, extCmd.Help)
|
||||
macroCmd := &grumble.Command{
|
||||
Name: extCmd.Name,
|
||||
Help: helpMsg,
|
||||
LongHelp: help.FormatHelpTmpl(extCmd.LongHelp),
|
||||
Run: func(extCtx *grumble.Context) error {
|
||||
con.Println()
|
||||
runMacroCommand(extCtx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
Flags: func(f *grumble.Flags) {
|
||||
if extCmd.IsAssembly {
|
||||
f.String("m", "method", "", "Optional method (a method is required for a .NET DLL)")
|
||||
f.String("c", "class", "", "Optional class name (required for .NET DLL)")
|
||||
f.String("d", "app-domain", "", "AppDomain name to create for .NET assembly. Generated randomly if not set.")
|
||||
f.String("a", "arch", "x84", "Assembly target architecture: x86, x64, x84 (x86+x64)")
|
||||
}
|
||||
f.String("p", "process", "", "Path to process to host the shared object")
|
||||
f.Bool("s", "save", false, "Save output to disk")
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
Args: func(a *grumble.Args) {
|
||||
a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
},
|
||||
HelpGroup: consts.MacroHelpGroup,
|
||||
}
|
||||
con.App.AddCommand(macroCmd)
|
||||
}
|
||||
con.PrintInfof("%s macro has been loaded\n", ext.Name)
|
||||
}
|
||||
|
||||
func runMacroCommand(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
ext, ok := commandMap[ctx.Command.Name]
|
||||
if !ok {
|
||||
con.PrintErrorf("No macro command found for `%s` command\n", ctx.Command.Name)
|
||||
return
|
||||
}
|
||||
|
||||
binPath, err := ext.getFileForTarget(ctx.Command.Name, session.GetOS(), session.GetArch())
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := ext.getCommandFromName(ctx.Command.Name)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
args := ctx.Args.StringList("arguments")
|
||||
var extArgs string
|
||||
if len(c.DefaultArgs) != 0 && len(args) == 0 {
|
||||
extArgs = c.DefaultArgs
|
||||
} else {
|
||||
extArgs = strings.Join(args, " ")
|
||||
}
|
||||
entryPoint := c.Entrypoint
|
||||
processName := ctx.Flags.String("process")
|
||||
if processName == "" {
|
||||
processName, err = c.getDefaultProcess(session.GetOS())
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
isDLL := false
|
||||
if filepath.Ext(binPath) == ".dll" {
|
||||
isDLL = true
|
||||
}
|
||||
binData, err := ioutil.ReadFile(binPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
var outFilePath *os.File
|
||||
if ctx.Flags.Bool("save") {
|
||||
outFile := path.Base(fmt.Sprintf("%s_%s*.log", ctx.Command.Name, session.GetHostname()))
|
||||
outFilePath, err = ioutil.TempFile("", outFile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if c.IsAssembly {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, extArgs)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
executeAssemblyResp, err := con.Rpc.ExecuteAssembly(context.Background(), &sliverpb.ExecuteAssemblyReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
IsDLL: isDLL,
|
||||
Process: processName,
|
||||
Arguments: extArgs,
|
||||
Assembly: binData,
|
||||
Arch: ctx.Flags.String("arch"),
|
||||
Method: ctx.Flags.String("method"),
|
||||
ClassName: ctx.Flags.String("class"),
|
||||
AppDomain: ctx.Flags.String("app-domain"),
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Output:\n%s", string(executeAssemblyResp.GetOutput()))
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write(executeAssemblyResp.GetOutput())
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
} else if c.IsReflective {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, extArgs)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
spawnDllResp, err := con.Rpc.SpawnDll(context.Background(), &sliverpb.InvokeSpawnDllReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Args: strings.Trim(extArgs, " "),
|
||||
Data: binData,
|
||||
ProcessName: processName,
|
||||
EntryPoint: c.Entrypoint,
|
||||
Kill: true,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.PrintInfof("Output:\n%s", spawnDllResp.GetResult())
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write([]byte(spawnDllResp.GetResult()))
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
} else {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, extArgs)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
sideloadResp, err := con.Rpc.Sideload(context.Background(), &sliverpb.SideloadReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Args: extArgs,
|
||||
Data: binData,
|
||||
EntryPoint: entryPoint,
|
||||
ProcessName: processName,
|
||||
Kill: true,
|
||||
IsDLL: isDLL,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.PrintInfof("Output:\n%s", sideloadResp.GetResult())
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write([]byte(sideloadResp.GetResult()))
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cmdExists(name string, app *grumble.App) bool {
|
||||
for _, c := range app.Commands().All() {
|
||||
if name == c.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandMap = make(map[string]macro)
|
||||
}
|
||||
@@ -95,6 +95,8 @@ type BindCmds func(console *SliverConsoleClient)
|
||||
// Start - Console entrypoint
|
||||
func Start(rpc rpcpb.SliverRPCClient, bindCmds BindCmds, extraCmds BindCmds, isServer bool) error {
|
||||
|
||||
assets.Setup(false, false)
|
||||
|
||||
con := &SliverConsoleClient{
|
||||
App: grumble.New(&grumble.Config{
|
||||
Name: "Sliver",
|
||||
@@ -199,7 +201,7 @@ func (con *SliverConsoleClient) EventLoop() {
|
||||
if activeSession != nil && activeSession.ID == session.ID {
|
||||
con.ActiveSession.Set(nil)
|
||||
con.App.SetPrompt(con.GetPrompt())
|
||||
con.Printf(Warn + " Active session disconnected\n")
|
||||
con.Printf(Warn + "Active session disconnected\n")
|
||||
}
|
||||
con.Println()
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ const (
|
||||
UnsetStr = "unset"
|
||||
SaveStr = "save"
|
||||
ReloadStr = "reload"
|
||||
LoadStr = "load"
|
||||
|
||||
LootStr = "loot"
|
||||
LootLocalStr = "local"
|
||||
@@ -174,7 +175,8 @@ const (
|
||||
MigrateStr = "migrate"
|
||||
SideloadStr = "sideload"
|
||||
SpawnDllStr = "spawndll"
|
||||
LoadExtensionStr = "load-extension"
|
||||
ExtensionStr = "extension"
|
||||
LoadMacroStr = "load-macro"
|
||||
StageListenerStr = "stage-listener"
|
||||
|
||||
WebsitesStr = "websites"
|
||||
@@ -205,6 +207,9 @@ const (
|
||||
|
||||
ReactionStr = "reaction"
|
||||
|
||||
HostsStr = "hosts"
|
||||
IOCStr = "ioc"
|
||||
|
||||
LicensesStr = "licenses"
|
||||
|
||||
GetPrivsStr = "getprivs"
|
||||
@@ -216,5 +221,6 @@ const (
|
||||
SliverHelpGroup = "Sliver:"
|
||||
SliverWinHelpGroup = "Sliver - Windows:"
|
||||
MultiplayerHelpGroup = "Multiplayer:"
|
||||
MacroHelpGroup = "Sliver - 3rd Party macros:"
|
||||
ExtensionHelpGroup = "Sliver - 3rd Party extensions:"
|
||||
)
|
||||
@@ -21,9 +21,11 @@
|
||||
|
||||
GO_VER="1.16.6"
|
||||
GARBLE_VER="1.16.7"
|
||||
COFF_LOADER_VER="1.0.4"
|
||||
|
||||
GO_ARCH_1="amd64"
|
||||
GO_ARCH_2="arm64"
|
||||
GO_ARCH_3="386"
|
||||
BLOAT_FILES="AUTHORS CONTRIBUTORS PATENTS VERSION favicon.ico robots.txt CONTRIBUTING.md LICENSE README.md ./doc ./test ./api ./misc"
|
||||
|
||||
if ! [ -x "$(command -v curl)" ]; then
|
||||
@@ -47,6 +49,7 @@ fi
|
||||
|
||||
REPO_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
OUTPUT_DIR=$REPO_DIR/server/assets/fs
|
||||
CLIENT_OUTPUT_DIR=$REPO_DIR/client/assets/fs
|
||||
WORK_DIR=`mktemp -d`
|
||||
|
||||
echo "-----------------------------------------------------------------"
|
||||
@@ -130,6 +133,10 @@ cp -vv windows-go.zip $OUTPUT_DIR/windows/$GO_ARCH_1/go.zip
|
||||
rm -rf ./go
|
||||
rm -f windows-go.zip go$GO_VER.windows-$GO_ARCH_1.zip
|
||||
|
||||
# --- Client ---
|
||||
mkdir -p $CLIENT_OUTPUT_DIR/extensions/windows/$GO_ARCH_1
|
||||
mkdir -p $CLIENT_OUTPUT_DIR/extensions/windows/$GO_ARCH_3
|
||||
|
||||
echo "-----------------------------------------------------------------"
|
||||
echo " Garble"
|
||||
echo "-----------------------------------------------------------------"
|
||||
@@ -138,6 +145,12 @@ curl -L --output $OUTPUT_DIR/windows/$GO_ARCH_1/garble.exe https://github.com/mo
|
||||
curl -L --output $OUTPUT_DIR/darwin/$GO_ARCH_1/garble https://github.com/moloch--/garble/releases/download/v$GARBLE_VER/garble_macos-$GO_ARCH_1
|
||||
curl -L --output $OUTPUT_DIR/darwin/$GO_ARCH_2/garble https://github.com/moloch--/garble/releases/download/v$GARBLE_VER/garble_macos-$GO_ARCH_2
|
||||
|
||||
echo "-----------------------------------------------------------------"
|
||||
echo " COFF Loader"
|
||||
echo "-----------------------------------------------------------------"
|
||||
curl -L --output $CLIENT_OUTPUT_DIR/extensions/windows/$GO_ARCH_1/COFFLoader.x64.dll https://github.com/lesnuages/COFFLoader/releases/download/v$COFF_LOADER_VER/COFFLoader.x64.dll
|
||||
curl -L --output $CLIENT_OUTPUT_DIR/extensions/windows/$GO_ARCH_3/COFFLoader.x86.dll https://github.com/lesnuages/COFFLoader/releases/download/v$COFF_LOADER_VER/COFFLoader.x86.dll
|
||||
|
||||
# end
|
||||
echo -e "clean up: $WORK_DIR"
|
||||
rm -rf $WORK_DIR
|
||||
|
||||
@@ -24,6 +24,7 @@ require (
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jedib0t/go-pretty/v6 v6.2.4
|
||||
github.com/kbinani/screenshot v0.0.0-20191211154542-3a185f1ce18f
|
||||
github.com/lesnuages/go-socks5 v0.0.0-20210409090601-adbe23bd0194
|
||||
github.com/lesnuages/go-winio v0.4.19
|
||||
@@ -40,6 +41,7 @@ require (
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22
|
||||
golang.org/x/text v0.3.6
|
||||
golang.zx2c4.com/wireguard v0.0.20200121
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b
|
||||
google.golang.org/genproto v0.0.0-20210406143921-e86de6bf7a46 // indirect
|
||||
|
||||
@@ -113,6 +113,7 @@ github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E=
|
||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 h1:Y5Q2mEwfzjMt5+3u70Gtw93ZOu2UuPeeeTBDntF7FoY=
|
||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
@@ -261,6 +262,8 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.2/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jedib0t/go-pretty/v6 v6.2.4 h1:wdaj2KHD2W+mz8JgJ/Q6L/T5dB7kyqEFI16eLq7GEmk=
|
||||
github.com/jedib0t/go-pretty/v6 v6.2.4/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
|
||||
@@ -318,8 +321,9 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
|
||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
@@ -356,6 +360,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
@@ -510,6 +515,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package extension
|
||||
|
||||
import "errors"
|
||||
|
||||
var extensions map[string]Extension
|
||||
|
||||
type Extension interface {
|
||||
Load() error
|
||||
Call(exportName string, arguments []byte, callback func([]byte)) error
|
||||
GetID() string
|
||||
GetArch() string
|
||||
}
|
||||
|
||||
func Add(e Extension) {
|
||||
extensions[e.GetID()] = e
|
||||
}
|
||||
|
||||
func List() []string {
|
||||
var extList []string
|
||||
for id := range extensions {
|
||||
extList = append(extList, id)
|
||||
}
|
||||
return extList
|
||||
}
|
||||
|
||||
func Run(extID string, funcName string, arguments []byte, callback func([]byte)) error {
|
||||
if ext, found := extensions[extID]; found {
|
||||
return ext.Call(funcName, arguments, callback)
|
||||
}
|
||||
return errors.New("{{if .Config.Debug}} extension not found{{end}}")
|
||||
}
|
||||
|
||||
func init() {
|
||||
extensions = make(map[string]Extension)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
// {{if .Config.Debug}}
|
||||
"log"
|
||||
// {{end}}
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun/wintun/memmod"
|
||||
)
|
||||
|
||||
const (
|
||||
Success = 0
|
||||
Failure = 1
|
||||
)
|
||||
|
||||
type WindowsExtension struct {
|
||||
id string
|
||||
data []byte
|
||||
module *memmod.Module
|
||||
arch string
|
||||
init string
|
||||
onFinish func([]byte)
|
||||
serverStore bool
|
||||
}
|
||||
|
||||
func NewWindowsExtension(data []byte, id string, arch string, init string) *WindowsExtension {
|
||||
return &WindowsExtension{
|
||||
id: id,
|
||||
data: data,
|
||||
arch: arch,
|
||||
init: init,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WindowsExtension) GetID() string {
|
||||
return w.id
|
||||
}
|
||||
|
||||
func (w *WindowsExtension) GetArch() string {
|
||||
return w.arch
|
||||
}
|
||||
|
||||
func (w *WindowsExtension) Load() error {
|
||||
var err error
|
||||
w.module, err = memmod.LoadLibrary(w.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if w.init != "" {
|
||||
initProc, errInit := w.module.ProcAddressByName(w.init)
|
||||
if errInit == nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("Calling %s\n", w.init)
|
||||
// {{end}}
|
||||
syscall.Syscall(initProc, 0, 0, 0, 0)
|
||||
} else {
|
||||
return errInit
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WindowsExtension) Call(export string, arguments []byte, onFinish func([]byte)) error {
|
||||
var (
|
||||
argumentsPtr uintptr
|
||||
argumentsSize uintptr
|
||||
)
|
||||
if w.module == nil {
|
||||
return errors.New("{{if .Config.Debug}} module not loaded {{end}}")
|
||||
}
|
||||
w.onFinish = onFinish
|
||||
callback := syscall.NewCallback(w.extensionCallback)
|
||||
exportPtr, err := w.module.ProcAddressByName(export)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(arguments) > 0 {
|
||||
argumentsPtr = uintptr(unsafe.Pointer(&arguments[0]))
|
||||
argumentsSize = uintptr(uint32(len(arguments)))
|
||||
}
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("Calling %s, arguments addr: 0x%08x, args size: %08x\n", export, argumentsPtr, argumentsSize)
|
||||
// {{end}}
|
||||
// The extension API must respect the following prototype:
|
||||
// int Run(buffer char*, bufferSize uint32_t, goCallback callback)
|
||||
// where goCallback = int(char *, int)
|
||||
_, _, errNo := syscall.Syscall(exportPtr, 3, argumentsPtr, argumentsSize, callback)
|
||||
if errNo != 0 {
|
||||
return errors.New(errNo.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extensionCallback takes a buffer (char *) and its size (int) as parameters
|
||||
// so we can pass data back to the Go process from the loaded DLL
|
||||
func (w *WindowsExtension) extensionCallback(data uintptr, dataLen uintptr) uintptr {
|
||||
outDataSize := int(dataLen)
|
||||
outBuff := new(bytes.Buffer)
|
||||
for i := 0; i < outDataSize; i++ {
|
||||
b := (*byte)(unsafe.Pointer(uintptr(i) + data))
|
||||
outBuff.WriteByte(*b)
|
||||
}
|
||||
//TODO: do something with outBuff
|
||||
if outBuff.Len() > 0 {
|
||||
w.onFinish(outBuff.Bytes())
|
||||
}
|
||||
return Success
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/bishopfox/sliver/implant/sliver/extension"
|
||||
"github.com/bishopfox/sliver/implant/sliver/pivots"
|
||||
"github.com/bishopfox/sliver/implant/sliver/priv"
|
||||
"github.com/bishopfox/sliver/implant/sliver/registry"
|
||||
@@ -91,6 +92,11 @@ var (
|
||||
sliverpb.MsgPollIntervalReq: pollIntervalHandler,
|
||||
sliverpb.MsgSSHCommandReq: runSSHCommandHandler,
|
||||
|
||||
// Extensions
|
||||
sliverpb.MsgRegisterExtensionReq: registerExtensionHandler,
|
||||
sliverpb.MsgCallExtensionReq: callExtensionHandler,
|
||||
sliverpb.MsgListExtensionsReq: listExtensionsHandler,
|
||||
|
||||
// {{if .Config.WGc2Enabled}}
|
||||
// Wireguard specific
|
||||
sliverpb.MsgWGStartPortFwdReq: wgStartPortfwdHandler,
|
||||
@@ -578,3 +584,66 @@ func getPrivsHandler(data []byte, resp RPCResponse) {
|
||||
data, err = proto.Marshal(getPrivsResp)
|
||||
resp(data, err)
|
||||
}
|
||||
|
||||
// Extensions
|
||||
|
||||
func registerExtensionHandler(data []byte, resp RPCResponse) {
|
||||
registerReq := &sliverpb.RegisterExtensionReq{}
|
||||
|
||||
err := proto.Unmarshal(data, registerReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ext := extension.NewWindowsExtension(registerReq.Data, registerReq.Name, registerReq.OS, registerReq.Init)
|
||||
extension.Add(ext)
|
||||
err = ext.Load()
|
||||
registerResp := &sliverpb.RegisterExtension{
|
||||
Response: &commonpb.Response{},
|
||||
}
|
||||
if err != nil {
|
||||
registerResp.Response.Err = err.Error()
|
||||
}
|
||||
data, err = proto.Marshal(registerResp)
|
||||
resp(data, err)
|
||||
}
|
||||
|
||||
func callExtensionHandler(data []byte, resp RPCResponse) {
|
||||
callReq := &sliverpb.CallExtensionReq{}
|
||||
|
||||
err := proto.Unmarshal(data, callReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
callResp := &sliverpb.CallExtension{
|
||||
Response: &commonpb.Response{},
|
||||
}
|
||||
err = extension.Run(callReq.Name, callReq.Export, callReq.Args, func(out []byte) {
|
||||
callResp.Output = out
|
||||
data, err = proto.Marshal(callResp)
|
||||
resp(data, err)
|
||||
})
|
||||
// Only send back synchronously if there was an error
|
||||
if err != nil {
|
||||
callResp.Response.Err = err.Error()
|
||||
data, err = proto.Marshal(callResp)
|
||||
resp(data, err)
|
||||
}
|
||||
}
|
||||
|
||||
func listExtensionsHandler(data []byte, resp RPCResponse) {
|
||||
lstReq := &sliverpb.ListExtensionsReq{}
|
||||
err := proto.Unmarshal(data, lstReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
exts := extension.List()
|
||||
lstResp := &sliverpb.ListExtensions{
|
||||
Response: &commonpb.Response{},
|
||||
Names: exts,
|
||||
}
|
||||
data, err = proto.Marshal(lstResp)
|
||||
resp(data, err)
|
||||
}
|
||||
@@ -21,23 +21,35 @@ package hostuuid
|
||||
*/
|
||||
|
||||
import (
|
||||
// {{if .Config.Debug}}
|
||||
"log"
|
||||
// {{end}}
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
// Stored Format: {U-U-I-D}
|
||||
var uuid_keypath = "HKEY_LOCAL_MACHINE\\SYSTEM\\HardwareConfig"
|
||||
var uuid_key = "LastConfig"
|
||||
var uuidKeyPath = "SYSTEM\\HardwareConfig"
|
||||
var uuidKey = "LastConfig"
|
||||
|
||||
func GetUUID() string {
|
||||
key, err := registry.OpenKey(registry.CURRENT_USER, uuid_keypath, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var uuidStr string
|
||||
var err error
|
||||
|
||||
str, _, err := key.GetStringValue(uuid_key)
|
||||
if err != nil {
|
||||
return ""
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, uuidKeyPath, registry.QUERY_VALUE)
|
||||
if err == nil {
|
||||
uuidStr, _, err = key.GetStringValue(uuidKey)
|
||||
if err != nil {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("Failed to read reg key string value: %s", err)
|
||||
// {{end}}
|
||||
return ""
|
||||
}
|
||||
return uuidStr[1:37]
|
||||
} else {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("Failed to read reg key: %s", err)
|
||||
// {{end}}
|
||||
}
|
||||
|
||||
return str[1:37]
|
||||
return ""
|
||||
}
|
||||
+428
-85
@@ -380,28 +380,29 @@ type Session struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
ID uint32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
Hostname string `protobuf:"bytes,3,opt,name=Hostname,proto3" json:"Hostname,omitempty"`
|
||||
UUID string `protobuf:"bytes,4,opt,name=UUID,proto3" json:"UUID,omitempty"`
|
||||
Username string `protobuf:"bytes,5,opt,name=Username,proto3" json:"Username,omitempty"`
|
||||
UID string `protobuf:"bytes,6,opt,name=UID,proto3" json:"UID,omitempty"`
|
||||
GID string `protobuf:"bytes,7,opt,name=GID,proto3" json:"GID,omitempty"`
|
||||
OS string `protobuf:"bytes,8,opt,name=OS,proto3" json:"OS,omitempty"`
|
||||
Arch string `protobuf:"bytes,9,opt,name=Arch,proto3" json:"Arch,omitempty"`
|
||||
Transport string `protobuf:"bytes,10,opt,name=Transport,proto3" json:"Transport,omitempty"`
|
||||
RemoteAddress string `protobuf:"bytes,11,opt,name=RemoteAddress,proto3" json:"RemoteAddress,omitempty"`
|
||||
PID int32 `protobuf:"varint,12,opt,name=PID,proto3" json:"PID,omitempty"`
|
||||
Filename string `protobuf:"bytes,13,opt,name=Filename,proto3" json:"Filename,omitempty"` // Argv[0]
|
||||
LastCheckin string `protobuf:"bytes,14,opt,name=LastCheckin,proto3" json:"LastCheckin,omitempty"`
|
||||
ActiveC2 string `protobuf:"bytes,15,opt,name=ActiveC2,proto3" json:"ActiveC2,omitempty"`
|
||||
Version string `protobuf:"bytes,16,opt,name=Version,proto3" json:"Version,omitempty"`
|
||||
Evasion bool `protobuf:"varint,17,opt,name=Evasion,proto3" json:"Evasion,omitempty"`
|
||||
IsDead bool `protobuf:"varint,18,opt,name=IsDead,proto3" json:"IsDead,omitempty"`
|
||||
ReconnectInterval uint32 `protobuf:"varint,19,opt,name=ReconnectInterval,proto3" json:"ReconnectInterval,omitempty"`
|
||||
ProxyURL string `protobuf:"bytes,20,opt,name=ProxyURL,proto3" json:"ProxyURL,omitempty"`
|
||||
PollInterval uint32 `protobuf:"varint,21,opt,name=PollInterval,proto3" json:"PollInterval,omitempty"`
|
||||
Burned bool `protobuf:"varint,22,opt,name=Burned,proto3" json:"Burned,omitempty"`
|
||||
ID uint32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
Hostname string `protobuf:"bytes,3,opt,name=Hostname,proto3" json:"Hostname,omitempty"`
|
||||
UUID string `protobuf:"bytes,4,opt,name=UUID,proto3" json:"UUID,omitempty"`
|
||||
Username string `protobuf:"bytes,5,opt,name=Username,proto3" json:"Username,omitempty"`
|
||||
UID string `protobuf:"bytes,6,opt,name=UID,proto3" json:"UID,omitempty"`
|
||||
GID string `protobuf:"bytes,7,opt,name=GID,proto3" json:"GID,omitempty"`
|
||||
OS string `protobuf:"bytes,8,opt,name=OS,proto3" json:"OS,omitempty"`
|
||||
Arch string `protobuf:"bytes,9,opt,name=Arch,proto3" json:"Arch,omitempty"`
|
||||
Transport string `protobuf:"bytes,10,opt,name=Transport,proto3" json:"Transport,omitempty"`
|
||||
RemoteAddress string `protobuf:"bytes,11,opt,name=RemoteAddress,proto3" json:"RemoteAddress,omitempty"`
|
||||
PID int32 `protobuf:"varint,12,opt,name=PID,proto3" json:"PID,omitempty"`
|
||||
Filename string `protobuf:"bytes,13,opt,name=Filename,proto3" json:"Filename,omitempty"` // Argv[0]
|
||||
LastCheckin string `protobuf:"bytes,14,opt,name=LastCheckin,proto3" json:"LastCheckin,omitempty"`
|
||||
ActiveC2 string `protobuf:"bytes,15,opt,name=ActiveC2,proto3" json:"ActiveC2,omitempty"`
|
||||
Version string `protobuf:"bytes,16,opt,name=Version,proto3" json:"Version,omitempty"`
|
||||
Evasion bool `protobuf:"varint,17,opt,name=Evasion,proto3" json:"Evasion,omitempty"`
|
||||
IsDead bool `protobuf:"varint,18,opt,name=IsDead,proto3" json:"IsDead,omitempty"`
|
||||
ReconnectInterval uint32 `protobuf:"varint,19,opt,name=ReconnectInterval,proto3" json:"ReconnectInterval,omitempty"`
|
||||
ProxyURL string `protobuf:"bytes,20,opt,name=ProxyURL,proto3" json:"ProxyURL,omitempty"`
|
||||
PollInterval uint32 `protobuf:"varint,21,opt,name=PollInterval,proto3" json:"PollInterval,omitempty"`
|
||||
Burned bool `protobuf:"varint,22,opt,name=Burned,proto3" json:"Burned,omitempty"`
|
||||
Extensions []string `protobuf:"bytes,23,rep,name=Extensions,proto3" json:"Extensions,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Session) Reset() {
|
||||
@@ -590,6 +591,13 @@ func (x *Session) GetBurned() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Session) GetExtensions() []string {
|
||||
if x != nil {
|
||||
return x.Extensions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImplantC2 struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -2622,10 +2630,11 @@ type UpdateSession struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
SessionID uint32 `protobuf:"varint,1,opt,name=SessionID,proto3" json:"SessionID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
ReconnectInterval int32 `protobuf:"varint,3,opt,name=ReconnectInterval,proto3" json:"ReconnectInterval,omitempty"`
|
||||
PollInterval int32 `protobuf:"varint,4,opt,name=PollInterval,proto3" json:"PollInterval,omitempty"`
|
||||
SessionID uint32 `protobuf:"varint,1,opt,name=SessionID,proto3" json:"SessionID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
ReconnectInterval int32 `protobuf:"varint,3,opt,name=ReconnectInterval,proto3" json:"ReconnectInterval,omitempty"`
|
||||
PollInterval int32 `protobuf:"varint,4,opt,name=PollInterval,proto3" json:"PollInterval,omitempty"`
|
||||
Extensions []string `protobuf:"bytes,5,rep,name=Extensions,proto3" json:"Extensions,omitempty"`
|
||||
}
|
||||
|
||||
func (x *UpdateSession) Reset() {
|
||||
@@ -2688,6 +2697,13 @@ func (x *UpdateSession) GetPollInterval() int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *UpdateSession) GetExtensions() []string {
|
||||
if x != nil {
|
||||
return x.Extensions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GenerateReq struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -4461,6 +4477,243 @@ func (x *AllLoot) GetLoot() []*Loot {
|
||||
return nil
|
||||
}
|
||||
|
||||
// [ Hosts ] ----------------------------------------
|
||||
type IOC struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"`
|
||||
FileHash string `protobuf:"bytes,2,opt,name=FileHash,proto3" json:"FileHash,omitempty"`
|
||||
ID string `protobuf:"bytes,3,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
}
|
||||
|
||||
func (x *IOC) Reset() {
|
||||
*x = IOC{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_clientpb_client_proto_msgTypes[61]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *IOC) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*IOC) ProtoMessage() {}
|
||||
|
||||
func (x *IOC) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_clientpb_client_proto_msgTypes[61]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use IOC.ProtoReflect.Descriptor instead.
|
||||
func (*IOC) Descriptor() ([]byte, []int) {
|
||||
return file_clientpb_client_proto_rawDescGZIP(), []int{61}
|
||||
}
|
||||
|
||||
func (x *IOC) GetPath() string {
|
||||
if x != nil {
|
||||
return x.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *IOC) GetFileHash() string {
|
||||
if x != nil {
|
||||
return x.FileHash
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *IOC) GetID() string {
|
||||
if x != nil {
|
||||
return x.ID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ExtensionData struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Output string `protobuf:"bytes,1,opt,name=Output,proto3" json:"Output,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ExtensionData) Reset() {
|
||||
*x = ExtensionData{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_clientpb_client_proto_msgTypes[62]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ExtensionData) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExtensionData) ProtoMessage() {}
|
||||
|
||||
func (x *ExtensionData) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_clientpb_client_proto_msgTypes[62]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExtensionData.ProtoReflect.Descriptor instead.
|
||||
func (*ExtensionData) Descriptor() ([]byte, []int) {
|
||||
return file_clientpb_client_proto_rawDescGZIP(), []int{62}
|
||||
}
|
||||
|
||||
func (x *ExtensionData) GetOutput() string {
|
||||
if x != nil {
|
||||
return x.Output
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Hostname string `protobuf:"bytes,1,opt,name=Hostname,proto3" json:"Hostname,omitempty"`
|
||||
HostUUID string `protobuf:"bytes,2,opt,name=HostUUID,proto3" json:"HostUUID,omitempty"`
|
||||
OSVersion string `protobuf:"bytes,3,opt,name=OSVersion,proto3" json:"OSVersion,omitempty"`
|
||||
IOCs []*IOC `protobuf:"bytes,4,rep,name=IOCs,proto3" json:"IOCs,omitempty"`
|
||||
ExtensionData map[string]*ExtensionData `protobuf:"bytes,5,rep,name=ExtensionData,proto3" json:"ExtensionData,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *Host) Reset() {
|
||||
*x = Host{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_clientpb_client_proto_msgTypes[63]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Host) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Host) ProtoMessage() {}
|
||||
|
||||
func (x *Host) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_clientpb_client_proto_msgTypes[63]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Host.ProtoReflect.Descriptor instead.
|
||||
func (*Host) Descriptor() ([]byte, []int) {
|
||||
return file_clientpb_client_proto_rawDescGZIP(), []int{63}
|
||||
}
|
||||
|
||||
func (x *Host) GetHostname() string {
|
||||
if x != nil {
|
||||
return x.Hostname
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Host) GetHostUUID() string {
|
||||
if x != nil {
|
||||
return x.HostUUID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Host) GetOSVersion() string {
|
||||
if x != nil {
|
||||
return x.OSVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Host) GetIOCs() []*IOC {
|
||||
if x != nil {
|
||||
return x.IOCs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Host) GetExtensionData() map[string]*ExtensionData {
|
||||
if x != nil {
|
||||
return x.ExtensionData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AllHosts struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Hosts []*Host `protobuf:"bytes,1,rep,name=Hosts,proto3" json:"Hosts,omitempty"`
|
||||
}
|
||||
|
||||
func (x *AllHosts) Reset() {
|
||||
*x = AllHosts{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_clientpb_client_proto_msgTypes[64]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *AllHosts) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AllHosts) ProtoMessage() {}
|
||||
|
||||
func (x *AllHosts) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_clientpb_client_proto_msgTypes[64]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AllHosts.ProtoReflect.Descriptor instead.
|
||||
func (*AllHosts) Descriptor() ([]byte, []int) {
|
||||
return file_clientpb_client_proto_rawDescGZIP(), []int{64}
|
||||
}
|
||||
|
||||
func (x *AllHosts) GetHosts() []*Host {
|
||||
if x != nil {
|
||||
return x.Hosts
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_clientpb_client_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_clientpb_client_proto_rawDesc = []byte{
|
||||
@@ -4479,7 +4732,7 @@ var file_clientpb_client_proto_rawDesc = []byte{
|
||||
0x41, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c,
|
||||
0x65, 0x64, 0x41, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x02, 0x4f, 0x53, 0x12, 0x12, 0x0a, 0x04, 0x41, 0x72, 0x63, 0x68, 0x18, 0x08, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x04, 0x41, 0x72, 0x63, 0x68, 0x22, 0xc3, 0x04, 0x0a, 0x07, 0x53, 0x65, 0x73,
|
||||
0x28, 0x09, 0x52, 0x04, 0x41, 0x72, 0x63, 0x68, 0x22, 0xe3, 0x04, 0x0a, 0x07, 0x53, 0x65, 0x73,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d,
|
||||
0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74,
|
||||
@@ -4515,7 +4768,9 @@ var file_clientpb_client_proto_rawDesc = []byte{
|
||||
0x52, 0x4c, 0x12, 0x22, 0x0a, 0x0c, 0x50, 0x6f, 0x6c, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76,
|
||||
0x61, 0x6c, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x50, 0x6f, 0x6c, 0x6c, 0x49, 0x6e,
|
||||
0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x42, 0x75, 0x72, 0x6e, 0x65, 0x64,
|
||||
0x18, 0x16, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x42, 0x75, 0x72, 0x6e, 0x65, 0x64, 0x22, 0x53,
|
||||
0x18, 0x16, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x42, 0x75, 0x72, 0x6e, 0x65, 0x64, 0x12, 0x1e,
|
||||
0x0a, 0x0a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x17, 0x20, 0x03,
|
||||
0x28, 0x09, 0x52, 0x0a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x53,
|
||||
0x0a, 0x09, 0x49, 0x6d, 0x70, 0x6c, 0x61, 0x6e, 0x74, 0x43, 0x32, 0x12, 0x1a, 0x0a, 0x08, 0x50,
|
||||
0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x50,
|
||||
0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x02,
|
||||
@@ -4753,7 +5008,7 @@ var file_clientpb_client_proto_rawDesc = []byte{
|
||||
0x08, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2d, 0x0a, 0x08, 0x53, 0x65, 0x73,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x63, 0x6c,
|
||||
0x69, 0x65, 0x6e, 0x74, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08,
|
||||
0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x0d, 0x55, 0x70, 0x64,
|
||||
0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb3, 0x01, 0x0a, 0x0d, 0x55, 0x70, 0x64,
|
||||
0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x65,
|
||||
0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x53,
|
||||
0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65,
|
||||
@@ -4762,7 +5017,9 @@ var file_clientpb_client_proto_rawDesc = []byte{
|
||||
0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x11, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x6e, 0x65,
|
||||
0x63, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x22, 0x0a, 0x0c, 0x50, 0x6f,
|
||||
0x6c, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05,
|
||||
0x52, 0x0c, 0x50, 0x6f, 0x6c, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x22, 0x3e,
|
||||
0x52, 0x0c, 0x50, 0x6f, 0x6c, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1e,
|
||||
0x0a, 0x0a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03,
|
||||
0x28, 0x09, 0x52, 0x0a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3e,
|
||||
0x0a, 0x0b, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x12, 0x2f, 0x0a,
|
||||
0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e,
|
||||
0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x70, 0x62, 0x2e, 0x49, 0x6d, 0x70, 0x6c, 0x61, 0x6e, 0x74,
|
||||
@@ -4966,29 +5223,58 @@ var file_clientpb_client_proto_rawDesc = []byte{
|
||||
0x69, 0x6c, 0x65, 0x52, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x22, 0x2d, 0x0a, 0x07, 0x41, 0x6c, 0x6c,
|
||||
0x4c, 0x6f, 0x6f, 0x74, 0x12, 0x22, 0x0a, 0x04, 0x4c, 0x6f, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x03,
|
||||
0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x70, 0x62, 0x2e, 0x4c, 0x6f,
|
||||
0x6f, 0x74, 0x52, 0x04, 0x4c, 0x6f, 0x6f, 0x74, 0x2a, 0x4a, 0x0a, 0x0c, 0x4f, 0x75, 0x74, 0x70,
|
||||
0x75, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x48, 0x41, 0x52,
|
||||
0x45, 0x44, 0x5f, 0x4c, 0x49, 0x42, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x48, 0x45, 0x4c,
|
||||
0x4c, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x45, 0x43, 0x55,
|
||||
0x54, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49,
|
||||
0x43, 0x45, 0x10, 0x03, 0x2a, 0x2d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x67, 0x65, 0x50, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x00, 0x12, 0x08,
|
||||
0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50,
|
||||
0x53, 0x10, 0x02, 0x2a, 0x2e, 0x0a, 0x08, 0x4c, 0x6f, 0x6f, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12,
|
||||
0x0d, 0x0a, 0x09, 0x4c, 0x4f, 0x4f, 0x54, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x13,
|
||||
0x0a, 0x0f, 0x4c, 0x4f, 0x4f, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x41,
|
||||
0x4c, 0x10, 0x01, 0x2a, 0x4d, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61,
|
||||
0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x4e, 0x4f, 0x5f, 0x43, 0x52, 0x45, 0x44,
|
||||
0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x55, 0x53, 0x45, 0x52,
|
||||
0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x41,
|
||||
0x50, 0x49, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x49, 0x4c, 0x45,
|
||||
0x10, 0x03, 0x2a, 0x2d, 0x0a, 0x08, 0x46, 0x69, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b,
|
||||
0x0a, 0x07, 0x4e, 0x4f, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x42,
|
||||
0x49, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x45, 0x58, 0x54, 0x10,
|
||||
0x02, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
|
||||
0x62, 0x69, 0x73, 0x68, 0x6f, 0x70, 0x66, 0x6f, 0x78, 0x2f, 0x73, 0x6c, 0x69, 0x76, 0x65, 0x72,
|
||||
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74,
|
||||
0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x6f, 0x74, 0x52, 0x04, 0x4c, 0x6f, 0x6f, 0x74, 0x22, 0x45, 0x0a, 0x03, 0x49, 0x4f, 0x43, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50,
|
||||
0x61, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x46, 0x69, 0x6c, 0x65, 0x48, 0x61, 0x73, 0x68, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x46, 0x69, 0x6c, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12,
|
||||
0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x22,
|
||||
0x27, 0x0a, 0x0d, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61,
|
||||
0x12, 0x16, 0x0a, 0x06, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x06, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0xa3, 0x02, 0x0a, 0x04, 0x48, 0x6f, 0x73,
|
||||
0x74, 0x12, 0x1a, 0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a,
|
||||
0x08, 0x48, 0x6f, 0x73, 0x74, 0x55, 0x55, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x08, 0x48, 0x6f, 0x73, 0x74, 0x55, 0x55, 0x49, 0x44, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, 0x56,
|
||||
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x53,
|
||||
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x04, 0x49, 0x4f, 0x43, 0x73, 0x18,
|
||||
0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x70, 0x62,
|
||||
0x2e, 0x49, 0x4f, 0x43, 0x52, 0x04, 0x49, 0x4f, 0x43, 0x73, 0x12, 0x47, 0x0a, 0x0d, 0x45, 0x78,
|
||||
0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28,
|
||||
0x0b, 0x32, 0x21, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x70, 0x62, 0x2e, 0x48, 0x6f, 0x73,
|
||||
0x74, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x44,
|
||||
0x61, 0x74, 0x61, 0x1a, 0x59, 0x0a, 0x12, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6c, 0x69,
|
||||
0x65, 0x6e, 0x74, 0x70, 0x62, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x44,
|
||||
0x61, 0x74, 0x61, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x30,
|
||||
0x0a, 0x08, 0x41, 0x6c, 0x6c, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x24, 0x0a, 0x05, 0x48, 0x6f,
|
||||
0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x63, 0x6c, 0x69, 0x65,
|
||||
0x6e, 0x74, 0x70, 0x62, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x05, 0x48, 0x6f, 0x73, 0x74, 0x73,
|
||||
0x2a, 0x4a, 0x0a, 0x0c, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74,
|
||||
0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x48, 0x41, 0x52, 0x45, 0x44, 0x5f, 0x4c, 0x49, 0x42, 0x10, 0x00,
|
||||
0x12, 0x0d, 0x0a, 0x09, 0x53, 0x48, 0x45, 0x4c, 0x4c, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x01, 0x12,
|
||||
0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x45, 0x43, 0x55, 0x54, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x02, 0x12,
|
||||
0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x10, 0x03, 0x2a, 0x2d, 0x0a, 0x0d,
|
||||
0x53, 0x74, 0x61, 0x67, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a,
|
||||
0x03, 0x54, 0x43, 0x50, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x01,
|
||||
0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x02, 0x2a, 0x2e, 0x0a, 0x08, 0x4c,
|
||||
0x6f, 0x6f, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x4f, 0x4f, 0x54, 0x5f,
|
||||
0x46, 0x49, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4c, 0x4f, 0x4f, 0x54, 0x5f, 0x43,
|
||||
0x52, 0x45, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x10, 0x01, 0x2a, 0x4d, 0x0a, 0x0e, 0x43,
|
||||
0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a,
|
||||
0x0d, 0x4e, 0x4f, 0x5f, 0x43, 0x52, 0x45, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x41, 0x4c, 0x10, 0x00,
|
||||
0x12, 0x11, 0x0a, 0x0d, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52,
|
||||
0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x50, 0x49, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x02,
|
||||
0x12, 0x08, 0x0a, 0x04, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x03, 0x2a, 0x2d, 0x0a, 0x08, 0x46, 0x69,
|
||||
0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x4f, 0x5f, 0x46, 0x49, 0x4c,
|
||||
0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x49, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x01, 0x12,
|
||||
0x08, 0x0a, 0x04, 0x54, 0x45, 0x58, 0x54, 0x10, 0x02, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74,
|
||||
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x69, 0x73, 0x68, 0x6f, 0x70, 0x66, 0x6f,
|
||||
0x78, 0x2f, 0x73, 0x6c, 0x69, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||
0x66, 0x2f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -5004,7 +5290,7 @@ func file_clientpb_client_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_clientpb_client_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
|
||||
var file_clientpb_client_proto_msgTypes = make([]protoimpl.MessageInfo, 64)
|
||||
var file_clientpb_client_proto_msgTypes = make([]protoimpl.MessageInfo, 69)
|
||||
var file_clientpb_client_proto_goTypes = []interface{}{
|
||||
(OutputFormat)(0), // 0: clientpb.OutputFormat
|
||||
(StageProtocol)(0), // 1: clientpb.StageProtocol
|
||||
@@ -5072,17 +5358,22 @@ var file_clientpb_client_proto_goTypes = []interface{}{
|
||||
(*Credential)(nil), // 63: clientpb.Credential
|
||||
(*Loot)(nil), // 64: clientpb.Loot
|
||||
(*AllLoot)(nil), // 65: clientpb.AllLoot
|
||||
nil, // 66: clientpb.ImplantBuilds.ConfigsEntry
|
||||
nil, // 67: clientpb.WebsiteAddContent.ContentsEntry
|
||||
nil, // 68: clientpb.Website.ContentsEntry
|
||||
(*commonpb.Request)(nil), // 69: commonpb.Request
|
||||
(*commonpb.Response)(nil), // 70: commonpb.Response
|
||||
(*commonpb.File)(nil), // 71: commonpb.File
|
||||
(*IOC)(nil), // 66: clientpb.IOC
|
||||
(*ExtensionData)(nil), // 67: clientpb.ExtensionData
|
||||
(*Host)(nil), // 68: clientpb.Host
|
||||
(*AllHosts)(nil), // 69: clientpb.AllHosts
|
||||
nil, // 70: clientpb.ImplantBuilds.ConfigsEntry
|
||||
nil, // 71: clientpb.WebsiteAddContent.ContentsEntry
|
||||
nil, // 72: clientpb.Website.ContentsEntry
|
||||
nil, // 73: clientpb.Host.ExtensionDataEntry
|
||||
(*commonpb.Request)(nil), // 74: commonpb.Request
|
||||
(*commonpb.Response)(nil), // 75: commonpb.Response
|
||||
(*commonpb.File)(nil), // 76: commonpb.File
|
||||
}
|
||||
var file_clientpb_client_proto_depIdxs = []int32{
|
||||
7, // 0: clientpb.ImplantConfig.C2:type_name -> clientpb.ImplantC2
|
||||
0, // 1: clientpb.ImplantConfig.Format:type_name -> clientpb.OutputFormat
|
||||
66, // 2: clientpb.ImplantBuilds.Configs:type_name -> clientpb.ImplantBuilds.ConfigsEntry
|
||||
70, // 2: clientpb.ImplantBuilds.Configs:type_name -> clientpb.ImplantBuilds.ConfigsEntry
|
||||
0, // 3: clientpb.CompilerTarget.Format:type_name -> clientpb.OutputFormat
|
||||
10, // 4: clientpb.Compiler.Targets:type_name -> clientpb.CompilerTarget
|
||||
11, // 5: clientpb.Compiler.CrossCompilers:type_name -> clientpb.CrossCompiler
|
||||
@@ -5091,46 +5382,50 @@ var file_clientpb_client_proto_depIdxs = []int32{
|
||||
8, // 8: clientpb.ImplantProfile.Config:type_name -> clientpb.ImplantConfig
|
||||
17, // 9: clientpb.ImplantProfiles.Profiles:type_name -> clientpb.ImplantProfile
|
||||
20, // 10: clientpb.Jobs.Active:type_name -> clientpb.Job
|
||||
69, // 11: clientpb.NamedPipesReq.Request:type_name -> commonpb.Request
|
||||
70, // 12: clientpb.NamedPipes.Response:type_name -> commonpb.Response
|
||||
69, // 13: clientpb.TCPPivotReq.Request:type_name -> commonpb.Request
|
||||
70, // 14: clientpb.TCPPivot.Response:type_name -> commonpb.Response
|
||||
74, // 11: clientpb.NamedPipesReq.Request:type_name -> commonpb.Request
|
||||
75, // 12: clientpb.NamedPipes.Response:type_name -> commonpb.Response
|
||||
74, // 13: clientpb.TCPPivotReq.Request:type_name -> commonpb.Request
|
||||
75, // 14: clientpb.TCPPivot.Response:type_name -> commonpb.Response
|
||||
6, // 15: clientpb.Sessions.Sessions:type_name -> clientpb.Session
|
||||
8, // 16: clientpb.GenerateReq.Config:type_name -> clientpb.ImplantConfig
|
||||
71, // 17: clientpb.Generate.File:type_name -> commonpb.File
|
||||
69, // 18: clientpb.MSFReq.Request:type_name -> commonpb.Request
|
||||
69, // 19: clientpb.MSFRemoteReq.Request:type_name -> commonpb.Request
|
||||
76, // 17: clientpb.Generate.File:type_name -> commonpb.File
|
||||
74, // 18: clientpb.MSFReq.Request:type_name -> commonpb.Request
|
||||
74, // 19: clientpb.MSFRemoteReq.Request:type_name -> commonpb.Request
|
||||
1, // 20: clientpb.StagerListenerReq.Protocol:type_name -> clientpb.StageProtocol
|
||||
1, // 21: clientpb.MsfStagerReq.Protocol:type_name -> clientpb.StageProtocol
|
||||
71, // 22: clientpb.MsfStager.File:type_name -> commonpb.File
|
||||
76, // 22: clientpb.MsfStager.File:type_name -> commonpb.File
|
||||
8, // 23: clientpb.GetSystemReq.Config:type_name -> clientpb.ImplantConfig
|
||||
69, // 24: clientpb.GetSystemReq.Request:type_name -> commonpb.Request
|
||||
74, // 24: clientpb.GetSystemReq.Request:type_name -> commonpb.Request
|
||||
8, // 25: clientpb.MigrateReq.Config:type_name -> clientpb.ImplantConfig
|
||||
69, // 26: clientpb.MigrateReq.Request:type_name -> commonpb.Request
|
||||
69, // 27: clientpb.CreateTunnelReq.Request:type_name -> commonpb.Request
|
||||
69, // 28: clientpb.CloseTunnelReq.Request:type_name -> commonpb.Request
|
||||
74, // 26: clientpb.MigrateReq.Request:type_name -> commonpb.Request
|
||||
74, // 27: clientpb.CreateTunnelReq.Request:type_name -> commonpb.Request
|
||||
74, // 28: clientpb.CloseTunnelReq.Request:type_name -> commonpb.Request
|
||||
56, // 29: clientpb.Client.Operator:type_name -> clientpb.Operator
|
||||
6, // 30: clientpb.Event.Session:type_name -> clientpb.Session
|
||||
20, // 31: clientpb.Event.Job:type_name -> clientpb.Job
|
||||
53, // 32: clientpb.Event.Client:type_name -> clientpb.Client
|
||||
56, // 33: clientpb.Operators.Operators:type_name -> clientpb.Operator
|
||||
67, // 34: clientpb.WebsiteAddContent.Contents:type_name -> clientpb.WebsiteAddContent.ContentsEntry
|
||||
68, // 35: clientpb.Website.Contents:type_name -> clientpb.Website.ContentsEntry
|
||||
71, // 34: clientpb.WebsiteAddContent.Contents:type_name -> clientpb.WebsiteAddContent.ContentsEntry
|
||||
72, // 35: clientpb.Website.Contents:type_name -> clientpb.Website.ContentsEntry
|
||||
60, // 36: clientpb.Websites.Websites:type_name -> clientpb.Website
|
||||
2, // 37: clientpb.Loot.Type:type_name -> clientpb.LootType
|
||||
3, // 38: clientpb.Loot.CredentialType:type_name -> clientpb.CredentialType
|
||||
63, // 39: clientpb.Loot.Credential:type_name -> clientpb.Credential
|
||||
4, // 40: clientpb.Loot.FileType:type_name -> clientpb.FileType
|
||||
71, // 41: clientpb.Loot.File:type_name -> commonpb.File
|
||||
76, // 41: clientpb.Loot.File:type_name -> commonpb.File
|
||||
64, // 42: clientpb.AllLoot.Loot:type_name -> clientpb.Loot
|
||||
8, // 43: clientpb.ImplantBuilds.ConfigsEntry.value:type_name -> clientpb.ImplantConfig
|
||||
57, // 44: clientpb.WebsiteAddContent.ContentsEntry.value:type_name -> clientpb.WebContent
|
||||
57, // 45: clientpb.Website.ContentsEntry.value:type_name -> clientpb.WebContent
|
||||
46, // [46:46] is the sub-list for method output_type
|
||||
46, // [46:46] is the sub-list for method input_type
|
||||
46, // [46:46] is the sub-list for extension type_name
|
||||
46, // [46:46] is the sub-list for extension extendee
|
||||
0, // [0:46] is the sub-list for field type_name
|
||||
66, // 43: clientpb.Host.IOCs:type_name -> clientpb.IOC
|
||||
73, // 44: clientpb.Host.ExtensionData:type_name -> clientpb.Host.ExtensionDataEntry
|
||||
68, // 45: clientpb.AllHosts.Hosts:type_name -> clientpb.Host
|
||||
8, // 46: clientpb.ImplantBuilds.ConfigsEntry.value:type_name -> clientpb.ImplantConfig
|
||||
57, // 47: clientpb.WebsiteAddContent.ContentsEntry.value:type_name -> clientpb.WebContent
|
||||
57, // 48: clientpb.Website.ContentsEntry.value:type_name -> clientpb.WebContent
|
||||
67, // 49: clientpb.Host.ExtensionDataEntry.value:type_name -> clientpb.ExtensionData
|
||||
50, // [50:50] is the sub-list for method output_type
|
||||
50, // [50:50] is the sub-list for method input_type
|
||||
50, // [50:50] is the sub-list for extension type_name
|
||||
50, // [50:50] is the sub-list for extension extendee
|
||||
0, // [0:50] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_clientpb_client_proto_init() }
|
||||
@@ -5871,6 +6166,54 @@ func file_clientpb_client_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_clientpb_client_proto_msgTypes[61].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*IOC); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_clientpb_client_proto_msgTypes[62].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*ExtensionData); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_clientpb_client_proto_msgTypes[63].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Host); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_clientpb_client_proto_msgTypes[64].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*AllHosts); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
@@ -5878,7 +6221,7 @@ func file_clientpb_client_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_clientpb_client_proto_rawDesc,
|
||||
NumEnums: 5,
|
||||
NumMessages: 64,
|
||||
NumMessages: 69,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -43,6 +43,7 @@ message Session {
|
||||
string ProxyURL = 20;
|
||||
uint32 PollInterval = 21;
|
||||
bool Burned = 22;
|
||||
repeated string Extensions = 23;
|
||||
}
|
||||
|
||||
message ImplantC2 {
|
||||
@@ -274,6 +275,7 @@ message UpdateSession {
|
||||
string Name = 2;
|
||||
int32 ReconnectInterval = 3;
|
||||
int32 PollInterval = 4;
|
||||
repeated string Extensions = 5;
|
||||
}
|
||||
|
||||
message GenerateReq {
|
||||
@@ -496,3 +498,27 @@ message Loot {
|
||||
message AllLoot {
|
||||
repeated Loot Loot = 1;
|
||||
}
|
||||
|
||||
// [ Hosts ] ----------------------------------------
|
||||
message IOC {
|
||||
string Path = 1;
|
||||
string FileHash = 2;
|
||||
string ID = 3;
|
||||
}
|
||||
|
||||
message ExtensionData {
|
||||
string Output = 1;
|
||||
}
|
||||
|
||||
message Host {
|
||||
string Hostname = 1;
|
||||
string HostUUID = 2;
|
||||
string OSVersion = 3;
|
||||
|
||||
repeated IOC IOCs = 4;
|
||||
map<string, ExtensionData> ExtensionData = 5;
|
||||
}
|
||||
|
||||
message AllHosts {
|
||||
repeated Host Hosts = 1;
|
||||
}
|
||||
+638
-591
File diff suppressed because it is too large.
Load diff
@@ -47,6 +47,12 @@ service SliverRPC {
|
||||
rpc LootAll(commonpb.Empty) returns(clientpb.AllLoot);
|
||||
rpc LootAllOf(clientpb.Loot) returns(clientpb.AllLoot);
|
||||
|
||||
// *** Hosts ***
|
||||
rpc Hosts(commonpb.Empty) returns(clientpb.AllHosts);
|
||||
rpc Host(clientpb.Host) returns(clientpb.Host);
|
||||
rpc HostRm(clientpb.Host) returns(commonpb.Empty);
|
||||
rpc HostIOCRm(clientpb.IOC) returns(commonpb.Empty);
|
||||
|
||||
// *** Implants ***
|
||||
rpc Generate(clientpb.GenerateReq) returns (clientpb.Generate);
|
||||
rpc Regenerate(clientpb.RegenerateReq) returns (clientpb.Generate);
|
||||
@@ -117,6 +123,10 @@ service SliverRPC {
|
||||
rpc RunSSHCommand(sliverpb.SSHCommandReq) returns (sliverpb.SSHCommand);
|
||||
rpc HijackDLL(sliverpb.DllHijackReq) returns (sliverpb.DllHijack);
|
||||
rpc GetPrivs(sliverpb.GetPrivsReq) returns (sliverpb.GetPrivs);
|
||||
// Extensions
|
||||
rpc RegisterExtension(sliverpb.RegisterExtensionReq) returns (sliverpb.RegisterExtension);
|
||||
rpc CallExtension(sliverpb.CallExtensionReq) returns (sliverpb.CallExtension);
|
||||
rpc ListExtensions(sliverpb.ListExtensionsReq) returns (sliverpb.ListExtensions);
|
||||
|
||||
// *** Wireguard Specific ***
|
||||
rpc WGStartPortForward(sliverpb.WGPortForwardStartReq) returns (sliverpb.WGPortForward);
|
||||
|
||||
@@ -51,6 +51,11 @@ type SliverRPCClient interface {
|
||||
LootContent(ctx context.Context, in *clientpb.Loot, opts ...grpc.CallOption) (*clientpb.Loot, error)
|
||||
LootAll(ctx context.Context, in *commonpb.Empty, opts ...grpc.CallOption) (*clientpb.AllLoot, error)
|
||||
LootAllOf(ctx context.Context, in *clientpb.Loot, opts ...grpc.CallOption) (*clientpb.AllLoot, error)
|
||||
// *** Hosts ***
|
||||
Hosts(ctx context.Context, in *commonpb.Empty, opts ...grpc.CallOption) (*clientpb.AllHosts, error)
|
||||
Host(ctx context.Context, in *clientpb.Host, opts ...grpc.CallOption) (*clientpb.Host, error)
|
||||
HostRm(ctx context.Context, in *clientpb.Host, opts ...grpc.CallOption) (*commonpb.Empty, error)
|
||||
HostIOCRm(ctx context.Context, in *clientpb.IOC, opts ...grpc.CallOption) (*commonpb.Empty, error)
|
||||
// *** Implants ***
|
||||
Generate(ctx context.Context, in *clientpb.GenerateReq, opts ...grpc.CallOption) (*clientpb.Generate, error)
|
||||
Regenerate(ctx context.Context, in *clientpb.RegenerateReq, opts ...grpc.CallOption) (*clientpb.Generate, error)
|
||||
@@ -119,6 +124,10 @@ type SliverRPCClient interface {
|
||||
RunSSHCommand(ctx context.Context, in *sliverpb.SSHCommandReq, opts ...grpc.CallOption) (*sliverpb.SSHCommand, error)
|
||||
HijackDLL(ctx context.Context, in *sliverpb.DllHijackReq, opts ...grpc.CallOption) (*sliverpb.DllHijack, error)
|
||||
GetPrivs(ctx context.Context, in *sliverpb.GetPrivsReq, opts ...grpc.CallOption) (*sliverpb.GetPrivs, error)
|
||||
// Extensions
|
||||
RegisterExtension(ctx context.Context, in *sliverpb.RegisterExtensionReq, opts ...grpc.CallOption) (*sliverpb.RegisterExtension, error)
|
||||
CallExtension(ctx context.Context, in *sliverpb.CallExtensionReq, opts ...grpc.CallOption) (*sliverpb.CallExtension, error)
|
||||
ListExtensions(ctx context.Context, in *sliverpb.ListExtensionsReq, opts ...grpc.CallOption) (*sliverpb.ListExtensions, error)
|
||||
// *** Wireguard Specific ***
|
||||
WGStartPortForward(ctx context.Context, in *sliverpb.WGPortForwardStartReq, opts ...grpc.CallOption) (*sliverpb.WGPortForward, error)
|
||||
WGStopPortForward(ctx context.Context, in *sliverpb.WGPortForwardStopReq, opts ...grpc.CallOption) (*sliverpb.WGPortForward, error)
|
||||
@@ -343,6 +352,42 @@ func (c *sliverRPCClient) LootAllOf(ctx context.Context, in *clientpb.Loot, opts
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *sliverRPCClient) Hosts(ctx context.Context, in *commonpb.Empty, opts ...grpc.CallOption) (*clientpb.AllHosts, error) {
|
||||
out := new(clientpb.AllHosts)
|
||||
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/Hosts", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *sliverRPCClient) Host(ctx context.Context, in *clientpb.Host, opts ...grpc.CallOption) (*clientpb.Host, error) {
|
||||
out := new(clientpb.Host)
|
||||
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/Host", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *sliverRPCClient) HostRm(ctx context.Context, in *clientpb.Host, opts ...grpc.CallOption) (*commonpb.Empty, error) {
|
||||
out := new(commonpb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/HostRm", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *sliverRPCClient) HostIOCRm(ctx context.Context, in *clientpb.IOC, opts ...grpc.CallOption) (*commonpb.Empty, error) {
|
||||
out := new(commonpb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/HostIOCRm", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *sliverRPCClient) Generate(ctx context.Context, in *clientpb.GenerateReq, opts ...grpc.CallOption) (*clientpb.Generate, error) {
|
||||
out := new(clientpb.Generate)
|
||||
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/Generate", in, out, opts...)
|
||||
@@ -928,6 +973,33 @@ func (c *sliverRPCClient) GetPrivs(ctx context.Context, in *sliverpb.GetPrivsReq
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *sliverRPCClient) RegisterExtension(ctx context.Context, in *sliverpb.RegisterExtensionReq, opts ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
|
||||
out := new(sliverpb.RegisterExtension)
|
||||
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/RegisterExtension", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *sliverRPCClient) CallExtension(ctx context.Context, in *sliverpb.CallExtensionReq, opts ...grpc.CallOption) (*sliverpb.CallExtension, error) {
|
||||
out := new(sliverpb.CallExtension)
|
||||
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/CallExtension", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *sliverRPCClient) ListExtensions(ctx context.Context, in *sliverpb.ListExtensionsReq, opts ...grpc.CallOption) (*sliverpb.ListExtensions, error) {
|
||||
out := new(sliverpb.ListExtensions)
|
||||
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/ListExtensions", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *sliverRPCClient) WGStartPortForward(ctx context.Context, in *sliverpb.WGPortForwardStartReq, opts ...grpc.CallOption) (*sliverpb.WGPortForward, error) {
|
||||
out := new(sliverpb.WGPortForward)
|
||||
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/WGStartPortForward", in, out, opts...)
|
||||
@@ -1115,6 +1187,11 @@ type SliverRPCServer interface {
|
||||
LootContent(context.Context, *clientpb.Loot) (*clientpb.Loot, error)
|
||||
LootAll(context.Context, *commonpb.Empty) (*clientpb.AllLoot, error)
|
||||
LootAllOf(context.Context, *clientpb.Loot) (*clientpb.AllLoot, error)
|
||||
// *** Hosts ***
|
||||
Hosts(context.Context, *commonpb.Empty) (*clientpb.AllHosts, error)
|
||||
Host(context.Context, *clientpb.Host) (*clientpb.Host, error)
|
||||
HostRm(context.Context, *clientpb.Host) (*commonpb.Empty, error)
|
||||
HostIOCRm(context.Context, *clientpb.IOC) (*commonpb.Empty, error)
|
||||
// *** Implants ***
|
||||
Generate(context.Context, *clientpb.GenerateReq) (*clientpb.Generate, error)
|
||||
Regenerate(context.Context, *clientpb.RegenerateReq) (*clientpb.Generate, error)
|
||||
@@ -1183,6 +1260,10 @@ type SliverRPCServer interface {
|
||||
RunSSHCommand(context.Context, *sliverpb.SSHCommandReq) (*sliverpb.SSHCommand, error)
|
||||
HijackDLL(context.Context, *sliverpb.DllHijackReq) (*sliverpb.DllHijack, error)
|
||||
GetPrivs(context.Context, *sliverpb.GetPrivsReq) (*sliverpb.GetPrivs, error)
|
||||
// Extensions
|
||||
RegisterExtension(context.Context, *sliverpb.RegisterExtensionReq) (*sliverpb.RegisterExtension, error)
|
||||
CallExtension(context.Context, *sliverpb.CallExtensionReq) (*sliverpb.CallExtension, error)
|
||||
ListExtensions(context.Context, *sliverpb.ListExtensionsReq) (*sliverpb.ListExtensions, error)
|
||||
// *** Wireguard Specific ***
|
||||
WGStartPortForward(context.Context, *sliverpb.WGPortForwardStartReq) (*sliverpb.WGPortForward, error)
|
||||
WGStopPortForward(context.Context, *sliverpb.WGPortForwardStopReq) (*sliverpb.WGPortForward, error)
|
||||
@@ -1272,6 +1353,18 @@ func (UnimplementedSliverRPCServer) LootAll(context.Context, *commonpb.Empty) (*
|
||||
func (UnimplementedSliverRPCServer) LootAllOf(context.Context, *clientpb.Loot) (*clientpb.AllLoot, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method LootAllOf not implemented")
|
||||
}
|
||||
func (UnimplementedSliverRPCServer) Hosts(context.Context, *commonpb.Empty) (*clientpb.AllHosts, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Hosts not implemented")
|
||||
}
|
||||
func (UnimplementedSliverRPCServer) Host(context.Context, *clientpb.Host) (*clientpb.Host, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Host not implemented")
|
||||
}
|
||||
func (UnimplementedSliverRPCServer) HostRm(context.Context, *clientpb.Host) (*commonpb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method HostRm not implemented")
|
||||
}
|
||||
func (UnimplementedSliverRPCServer) HostIOCRm(context.Context, *clientpb.IOC) (*commonpb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method HostIOCRm not implemented")
|
||||
}
|
||||
func (UnimplementedSliverRPCServer) Generate(context.Context, *clientpb.GenerateReq) (*clientpb.Generate, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Generate not implemented")
|
||||
}
|
||||
@@ -1467,6 +1560,15 @@ func (UnimplementedSliverRPCServer) HijackDLL(context.Context, *sliverpb.DllHija
|
||||
func (UnimplementedSliverRPCServer) GetPrivs(context.Context, *sliverpb.GetPrivsReq) (*sliverpb.GetPrivs, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetPrivs not implemented")
|
||||
}
|
||||
func (UnimplementedSliverRPCServer) RegisterExtension(context.Context, *sliverpb.RegisterExtensionReq) (*sliverpb.RegisterExtension, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RegisterExtension not implemented")
|
||||
}
|
||||
func (UnimplementedSliverRPCServer) CallExtension(context.Context, *sliverpb.CallExtensionReq) (*sliverpb.CallExtension, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CallExtension not implemented")
|
||||
}
|
||||
func (UnimplementedSliverRPCServer) ListExtensions(context.Context, *sliverpb.ListExtensionsReq) (*sliverpb.ListExtensions, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListExtensions not implemented")
|
||||
}
|
||||
func (UnimplementedSliverRPCServer) WGStartPortForward(context.Context, *sliverpb.WGPortForwardStartReq) (*sliverpb.WGPortForward, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method WGStartPortForward not implemented")
|
||||
}
|
||||
@@ -1912,6 +2014,78 @@ func _SliverRPC_LootAllOf_Handler(srv interface{}, ctx context.Context, dec func
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _SliverRPC_Hosts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(commonpb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(SliverRPCServer).Hosts(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/rpcpb.SliverRPC/Hosts",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(SliverRPCServer).Hosts(ctx, req.(*commonpb.Empty))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _SliverRPC_Host_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(clientpb.Host)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(SliverRPCServer).Host(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/rpcpb.SliverRPC/Host",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(SliverRPCServer).Host(ctx, req.(*clientpb.Host))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _SliverRPC_HostRm_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(clientpb.Host)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(SliverRPCServer).HostRm(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/rpcpb.SliverRPC/HostRm",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(SliverRPCServer).HostRm(ctx, req.(*clientpb.Host))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _SliverRPC_HostIOCRm_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(clientpb.IOC)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(SliverRPCServer).HostIOCRm(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/rpcpb.SliverRPC/HostIOCRm",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(SliverRPCServer).HostIOCRm(ctx, req.(*clientpb.IOC))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _SliverRPC_Generate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(clientpb.GenerateReq)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -3082,6 +3256,60 @@ func _SliverRPC_GetPrivs_Handler(srv interface{}, ctx context.Context, dec func(
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _SliverRPC_RegisterExtension_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(sliverpb.RegisterExtensionReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(SliverRPCServer).RegisterExtension(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/rpcpb.SliverRPC/RegisterExtension",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(SliverRPCServer).RegisterExtension(ctx, req.(*sliverpb.RegisterExtensionReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _SliverRPC_CallExtension_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(sliverpb.CallExtensionReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(SliverRPCServer).CallExtension(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/rpcpb.SliverRPC/CallExtension",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(SliverRPCServer).CallExtension(ctx, req.(*sliverpb.CallExtensionReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _SliverRPC_ListExtensions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(sliverpb.ListExtensionsReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(SliverRPCServer).ListExtensions(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/rpcpb.SliverRPC/ListExtensions",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(SliverRPCServer).ListExtensions(ctx, req.(*sliverpb.ListExtensionsReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _SliverRPC_WGStartPortForward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(sliverpb.WGPortForwardStartReq)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -3404,6 +3632,22 @@ var SliverRPC_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "LootAllOf",
|
||||
Handler: _SliverRPC_LootAllOf_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Hosts",
|
||||
Handler: _SliverRPC_Hosts_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Host",
|
||||
Handler: _SliverRPC_Host_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "HostRm",
|
||||
Handler: _SliverRPC_HostRm_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "HostIOCRm",
|
||||
Handler: _SliverRPC_HostIOCRm_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Generate",
|
||||
Handler: _SliverRPC_Generate_Handler,
|
||||
@@ -3664,6 +3908,18 @@ var SliverRPC_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetPrivs",
|
||||
Handler: _SliverRPC_GetPrivs_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RegisterExtension",
|
||||
Handler: _SliverRPC_RegisterExtension_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CallExtension",
|
||||
Handler: _SliverRPC_CallExtension_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListExtensions",
|
||||
Handler: _SliverRPC_ListExtensions_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "WGStartPortForward",
|
||||
Handler: _SliverRPC_WGStartPortForward_Handler,
|
||||
|
||||
@@ -251,6 +251,14 @@ const (
|
||||
|
||||
// MsgRegistryListValuesReq - List registry values
|
||||
MsgRegistryListValuesReq
|
||||
// MsgRegisterExtensionReq - Register a new extension
|
||||
MsgRegisterExtensionReq
|
||||
|
||||
// MsgCallExtensionReq - Run an extension command
|
||||
MsgCallExtensionReq
|
||||
|
||||
// MsgListExtensionsReq - List loaded extensions
|
||||
MsgListExtensionsReq
|
||||
)
|
||||
|
||||
// Constants to replace enums
|
||||
@@ -480,6 +488,15 @@ func MsgNumber(request proto.Message) uint32 {
|
||||
return MsgRegistrySubKeysListReq
|
||||
case *RegistryListValuesReq:
|
||||
return MsgRegistryListValuesReq
|
||||
|
||||
case *RegisterExtensionReq:
|
||||
return MsgRegisterExtensionReq
|
||||
|
||||
case *CallExtensionReq:
|
||||
return MsgCallExtensionReq
|
||||
|
||||
case *ListExtensionsReq:
|
||||
return MsgListExtensionsReq
|
||||
}
|
||||
|
||||
return uint32(0)
|
||||
|
||||
+1278
-783
File diff suppressed because it is too large.
Load diff
@@ -190,6 +190,7 @@ message UploadReq {
|
||||
string Path = 1;
|
||||
string Encoder = 2;
|
||||
bytes Data = 3;
|
||||
bool IsIOC = 4;
|
||||
|
||||
commonpb.Request Request = 9;
|
||||
}
|
||||
@@ -827,5 +828,46 @@ message WindowsPrivilegeEntry {
|
||||
message GetPrivs {
|
||||
repeated WindowsPrivilegeEntry PrivInfo = 1;
|
||||
|
||||
commonpb.Response Response = 9;
|
||||
}
|
||||
|
||||
// Extensions
|
||||
|
||||
message RegisterExtensionReq {
|
||||
string Name = 1;
|
||||
bytes Data = 2;
|
||||
string OS = 3;
|
||||
string Init = 4;
|
||||
|
||||
commonpb.Request Request = 9;
|
||||
}
|
||||
|
||||
message RegisterExtension {
|
||||
commonpb.Response Response = 9;
|
||||
}
|
||||
|
||||
message CallExtensionReq {
|
||||
string Name = 1;
|
||||
string Export = 2;
|
||||
bytes Args = 3;
|
||||
bool ServerStore = 2;
|
||||
|
||||
commonpb.Request Request = 9;
|
||||
}
|
||||
|
||||
message CallExtension {
|
||||
bytes Output = 1;
|
||||
bool ServerStore = 2;
|
||||
|
||||
commonpb.Response Response = 9;
|
||||
}
|
||||
|
||||
message ListExtensionsReq {
|
||||
commonpb.Request Request = 9;
|
||||
}
|
||||
|
||||
message ListExtensions {
|
||||
repeated string Names = 1;
|
||||
|
||||
commonpb.Response Response = 9;
|
||||
}
|
||||
+1
-25
@@ -42,8 +42,7 @@ import (
|
||||
|
||||
const (
|
||||
// GoDirName - The directory to store the go compiler/toolchain files in
|
||||
GoDirName = "go"
|
||||
dllDirName = "dll"
|
||||
GoDirName = "go"
|
||||
|
||||
goPathDirName = "gopath"
|
||||
versionFileName = "version"
|
||||
@@ -76,12 +75,6 @@ func GetRootAppDir() string {
|
||||
return dir
|
||||
}
|
||||
|
||||
// GetDllDir - Returns the full path to the data directory
|
||||
func GetDllDir() string {
|
||||
dir := path.Join(GetRootAppDir(), dllDirName)
|
||||
return dir
|
||||
}
|
||||
|
||||
func assetVersion() string {
|
||||
appDir := GetRootAppDir()
|
||||
data, err := ioutil.ReadFile(path.Join(appDir, versionFileName))
|
||||
@@ -110,7 +103,6 @@ func Setup(force bool, echo bool) {
|
||||
}
|
||||
setupGo(appDir)
|
||||
setupCodenames(appDir)
|
||||
setupDllPath(appDir)
|
||||
saveAssetVersion(appDir)
|
||||
}
|
||||
}
|
||||
@@ -263,22 +255,6 @@ func SetupGoPath(goPathSrc string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupDllPath - Sets the data directory up
|
||||
func setupDllPath(appDir string) error {
|
||||
dataDir := GetDllDir()
|
||||
if _, err := os.Stat(dataDir); os.IsNotExist(err) {
|
||||
setupLog.Infof("Creating data directory: %s", dataDir)
|
||||
os.MkdirAll(dataDir, 0700)
|
||||
}
|
||||
hostingDll, err := assetsFs.ReadFile(path.Join("fs", "dll", "HostingCLRx64.dll"))
|
||||
if err != nil {
|
||||
setupLog.Info("failed to find the dll")
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(path.Join(dataDir, "HostingCLRx64.dll"), hostingDll, 0600)
|
||||
return err
|
||||
}
|
||||
|
||||
func unzipGoDependency(fsPath string, targetPath string) error {
|
||||
setupLog.Infof("Unpacking go dependency %s -> %s", fsPath, targetPath)
|
||||
|
||||
|
||||
@@ -5,6 +5,6 @@ package assets
|
||||
import "embed"
|
||||
|
||||
var (
|
||||
//go:embed fs/sliver.asc fs/*.txt fs/*.zip fs/dll/*.dll fs/darwin/amd64/*
|
||||
//go:embed fs/sliver.asc fs/*.txt fs/*.zip fs/darwin/amd64/*
|
||||
assetsFs embed.FS
|
||||
)
|
||||
@@ -5,6 +5,6 @@ package assets
|
||||
import "embed"
|
||||
|
||||
var (
|
||||
//go:embed fs/sliver.asc fs/*.txt fs/*.zip fs/dll/*.dll fs/darwin/arm64/*
|
||||
//go:embed fs/sliver.asc fs/*.txt fs/*.zip fs/darwin/arm64/*
|
||||
assetsFs embed.FS
|
||||
)
|
||||
@@ -5,6 +5,6 @@ package assets
|
||||
import "embed"
|
||||
|
||||
var (
|
||||
//go:embed fs/sliver.asc fs/*.txt fs/*.zip fs/dll/*.dll fs/linux/amd64/*
|
||||
//go:embed fs/sliver.asc fs/*.txt fs/*.zip fs/linux/amd64/*
|
||||
assetsFs embed.FS
|
||||
)
|
||||
@@ -5,6 +5,6 @@ package assets
|
||||
import "embed"
|
||||
|
||||
var (
|
||||
//go:embed fs/sliver.asc fs/*.txt fs/*.zip fs/dll/*.dll fs/windows/amd64/*
|
||||
//go:embed fs/sliver.asc fs/*.txt fs/*.zip fs/windows/amd64/*
|
||||
assetsFs embed.FS
|
||||
)
|
||||
Binary file not shown.
@@ -1,29 +0,0 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2018, b4rtik
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -113,5 +113,4 @@ func serverOnlyCmds(console *clientconsole.SliverConsoleClient) {
|
||||
},
|
||||
HelpGroup: consts.MultiplayerHelpGroup,
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package core
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 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 (
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
"github.com/gofrs/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
coreLog = log.NamedLogger("core", "hosts")
|
||||
)
|
||||
|
||||
func StartEventAutomation() {
|
||||
go func() {
|
||||
for event := range EventBroker.Subscribe() {
|
||||
switch event.EventType {
|
||||
|
||||
case consts.SessionOpenedEvent:
|
||||
if event.Session != nil {
|
||||
hostsSessionCallback(event.Session)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Triggered on new sessione events, checks to see if the host is in
|
||||
// the database and adds it if not.
|
||||
func hostsSessionCallback(session *Session) {
|
||||
dbSession := db.Session()
|
||||
_, err := db.HostByHostUUID(session.UUID)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
coreLog.Error(err)
|
||||
return
|
||||
}
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
err := dbSession.Create(&models.Host{
|
||||
HostUUID: uuid.FromStringOrNil(session.UUID),
|
||||
Hostname: session.Hostname,
|
||||
OSVersion: session.Os,
|
||||
IOCs: []models.IOC{},
|
||||
ExtensionData: []models.ExtensionData{},
|
||||
}).Error
|
||||
if err != nil {
|
||||
coreLog.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ type Session struct {
|
||||
ProxyURL string
|
||||
PollInterval uint32
|
||||
Burned bool
|
||||
Extensions []string
|
||||
}
|
||||
|
||||
// ToProtobuf - Get the protobuf version of the object
|
||||
|
||||
@@ -25,6 +25,7 @@ package db
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
"github.com/gofrs/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -188,3 +189,44 @@ func WGPeerIPs() ([]string, error) {
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
// ListHosts - List of all hosts in the database
|
||||
func ListHosts() ([]*models.Host, error) {
|
||||
hosts := []*models.Host{}
|
||||
err := Session().Where(
|
||||
&models.Host{},
|
||||
).Preload("IOCs").Preload("ExtensionData").Find(&hosts).Error
|
||||
return hosts, err
|
||||
}
|
||||
|
||||
// HostByHostID - Get host by the session's reported HostUUID
|
||||
func HostByHostID(id uuid.UUID) (*models.Host, error) {
|
||||
host := models.Host{}
|
||||
err := Session().Where(&models.Host{ID: id}).First(&host).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
// HostByHostUUID - Get host by the session's reported HostUUID
|
||||
func HostByHostUUID(id string) (*models.Host, error) {
|
||||
host := models.Host{}
|
||||
err := Session().Where(
|
||||
&models.Host{HostUUID: uuid.FromStringOrNil(id)},
|
||||
).Preload("IOCs").Preload("ExtensionData").Find(&host).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
// IOCByID - Select an IOC by ID
|
||||
func IOCByID(id string) (*models.IOC, error) {
|
||||
ioc := &models.IOC{}
|
||||
err := Session().Where(
|
||||
&models.IOC{ID: uuid.FromStringOrNil(id)},
|
||||
).First(ioc).Error
|
||||
return ioc, err
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package models
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 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 (
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/gofrs/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Host - Represents a host machine
|
||||
type Host struct {
|
||||
gorm.Model
|
||||
|
||||
ID uuid.UUID `gorm:"primaryKey;->;<-:create;type:uuid;"`
|
||||
HostUUID uuid.UUID `gorm:"type:uuid;"`
|
||||
CreatedAt time.Time `gorm:"->;<-:create;"`
|
||||
|
||||
Hostname string
|
||||
OSVersion string // Verbore OS version
|
||||
|
||||
IOCs []IOC
|
||||
ExtensionData []ExtensionData
|
||||
}
|
||||
|
||||
// BeforeCreate - GORM hook
|
||||
func (h *Host) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
h.ID, err = uuid.NewV4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Host) ToProtobuf() *clientpb.Host {
|
||||
return &clientpb.Host{
|
||||
HostUUID: h.HostUUID.String(),
|
||||
Hostname: h.Hostname,
|
||||
OSVersion: h.OSVersion,
|
||||
IOCs: h.iocsToProtobuf(),
|
||||
ExtensionData: h.extensionDataToProtobuf(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Host) iocsToProtobuf() []*clientpb.IOC {
|
||||
iocs := []*clientpb.IOC{}
|
||||
for _, dbIOC := range h.IOCs {
|
||||
iocs = append(iocs, dbIOC.ToProtobuf())
|
||||
}
|
||||
return iocs
|
||||
}
|
||||
|
||||
func (h *Host) extensionDataToProtobuf() map[string]*clientpb.ExtensionData {
|
||||
extData := map[string]*clientpb.ExtensionData{}
|
||||
for _, dbExtData := range h.ExtensionData {
|
||||
extData[dbExtData.Name] = &clientpb.ExtensionData{
|
||||
Output: dbExtData.Output,
|
||||
}
|
||||
}
|
||||
return extData
|
||||
}
|
||||
|
||||
// IOC - Represents an indicator of compromise, generally a file we've
|
||||
// uploaded to a remote system.
|
||||
type IOC struct {
|
||||
gorm.Model
|
||||
|
||||
ID uuid.UUID `gorm:"primaryKey;->;<-:create;type:uuid;"`
|
||||
HostID uuid.UUID `gorm:"type:uuid;"`
|
||||
CreatedAt time.Time `gorm:"->;<-:create;"`
|
||||
|
||||
Path string
|
||||
FileHash string
|
||||
}
|
||||
|
||||
// BeforeCreate - GORM hook
|
||||
func (i *IOC) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
i.ID, err = uuid.NewV4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *IOC) ToProtobuf() *clientpb.IOC {
|
||||
return &clientpb.IOC{
|
||||
Path: i.Path,
|
||||
FileHash: i.FileHash,
|
||||
}
|
||||
}
|
||||
|
||||
// ExtensionData - Represents an indicator of compromise, generally a file we've
|
||||
// uploaded to a remote system.
|
||||
type ExtensionData struct {
|
||||
gorm.Model
|
||||
|
||||
ID uuid.UUID `gorm:"primaryKey;->;<-:create;type:uuid;"`
|
||||
HostID uuid.UUID `gorm:"type:uuid;"`
|
||||
CreatedAt time.Time `gorm:"->;<-:create;"`
|
||||
|
||||
Name string
|
||||
Output string
|
||||
}
|
||||
|
||||
// BeforeCreate - GORM hook
|
||||
func (e *ExtensionData) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
e.ID, err = uuid.NewV4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
@@ -36,6 +36,8 @@ type Loot struct {
|
||||
FileType int
|
||||
CredentialType int
|
||||
Name string
|
||||
|
||||
OriginHost uuid.UUID `gorm:"type:uuid;"`
|
||||
}
|
||||
|
||||
// BeforeCreate - GORM hook
|
||||
|
||||
@@ -61,7 +61,7 @@ type WebContent struct {
|
||||
// gorm.Model
|
||||
|
||||
ID uuid.UUID `gorm:"primaryKey;->;<-:create;type:uuid;"`
|
||||
WebsiteID uuid.UUID
|
||||
WebsiteID uuid.UUID `gorm:"type:uuid;"`
|
||||
|
||||
Path string `gorm:"primaryKey"`
|
||||
Size int
|
||||
|
||||
@@ -54,6 +54,9 @@ func newDBClient() *gorm.DB {
|
||||
err := dbClient.AutoMigrate(
|
||||
&models.DNSCanary{},
|
||||
&models.Certificate{},
|
||||
&models.Host{},
|
||||
&models.IOC{},
|
||||
&models.ExtensionData{},
|
||||
&models.ImplantC2{},
|
||||
&models.ImplantConfig{},
|
||||
&models.ImplantBuild{},
|
||||
|
||||
@@ -41,6 +41,9 @@ var (
|
||||
"evasion/evasion_linux.go",
|
||||
"evasion/evasion_windows.go",
|
||||
|
||||
"extension/extension.go",
|
||||
"extension/extension_windows.go",
|
||||
|
||||
"forwarder/forwarder.go",
|
||||
"forwarder/socks.go",
|
||||
"forwarder/portforward.go",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
)
|
||||
|
||||
// RegisterExtension registers a new extension in the implant
|
||||
func (rpc *Server) RegisterExtension(ctx context.Context, req *sliverpb.RegisterExtensionReq) (*sliverpb.RegisterExtension, error) {
|
||||
resp := &sliverpb.RegisterExtension{}
|
||||
err := rpc.GenericHandler(req, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListExtensions lists the registered extensions
|
||||
func (rpc *Server) ListExtensions(ctx context.Context, req *sliverpb.ListExtensionsReq) (*sliverpb.ListExtensions, error) {
|
||||
resp := &sliverpb.ListExtensions{}
|
||||
err := rpc.GenericHandler(req, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CallExtension calls a specific export of the loaded extension
|
||||
func (rpc *Server) CallExtension(ctx context.Context, req *sliverpb.CallExtensionReq) (*sliverpb.CallExtension, error) {
|
||||
resp := &sliverpb.CallExtension{}
|
||||
err := rpc.GenericHandler(req, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -20,8 +20,18 @@ package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/server/core"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/server/db/models"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
)
|
||||
|
||||
var (
|
||||
fsLog = log.NamedLogger("rcp", "fs")
|
||||
)
|
||||
|
||||
// Ls - List a directory
|
||||
@@ -91,5 +101,37 @@ func (rpc *Server) Upload(ctx context.Context, req *sliverpb.UploadReq) (*sliver
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.IsIOC {
|
||||
go trackIOC(req, resp)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func trackIOC(req *sliverpb.UploadReq, resp *sliverpb.Upload) {
|
||||
fsLog.Debugf("Adding IOC to database ...")
|
||||
request := req.GetRequest()
|
||||
if request == nil {
|
||||
fsLog.Error("No request for upload")
|
||||
return
|
||||
}
|
||||
session := core.Sessions.Get(request.SessionID)
|
||||
if session == nil {
|
||||
fsLog.Error("No session for upload request")
|
||||
return
|
||||
}
|
||||
host, err := db.HostByHostUUID(session.UUID)
|
||||
if err != nil {
|
||||
fsLog.Errorf("No host for session uuid %v", session.UUID)
|
||||
return
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(req.Data)
|
||||
ioc := &models.IOC{
|
||||
HostID: host.ID,
|
||||
Path: resp.Path,
|
||||
FileHash: fmt.Sprintf("%x", sum),
|
||||
}
|
||||
if db.Session().Create(ioc).Error != nil {
|
||||
fsLog.Error("Failed to create IOC")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package rpc
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 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 (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
)
|
||||
|
||||
var (
|
||||
hostsRPCLog = log.NamedLogger("rpc", "hosts")
|
||||
)
|
||||
|
||||
// Hosts - List all hosts
|
||||
func (rpc *Server) Hosts(ctx context.Context, _ *commonpb.Empty) (*clientpb.AllHosts, error) {
|
||||
dbHosts, err := db.ListHosts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hosts := []*clientpb.Host{}
|
||||
for _, dbHost := range dbHosts {
|
||||
hosts = append(hosts, dbHost.ToProtobuf())
|
||||
}
|
||||
return &clientpb.AllHosts{Hosts: hosts}, nil
|
||||
}
|
||||
|
||||
// Host - Host by ID
|
||||
func (rpc *Server) Host(ctx context.Context, req *clientpb.Host) (*clientpb.Host, error) {
|
||||
dbHost, err := db.HostByHostUUID(req.HostUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dbHost.ToProtobuf(), nil
|
||||
}
|
||||
|
||||
// HostRm - Remove a host from the database
|
||||
func (rpc *Server) HostRm(ctx context.Context, req *clientpb.Host) (*commonpb.Empty, error) {
|
||||
dbHost, err := db.HostByHostUUID(req.HostUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = db.Session().Delete(dbHost).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
|
||||
// HostIOCRm - Remove a host from the database
|
||||
func (rpc *Server) HostIOCRm(ctx context.Context, req *clientpb.IOC) (*commonpb.Empty, error) {
|
||||
dbIOC, err := db.IOCByID(req.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = db.Session().Delete(dbIOC).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
@@ -130,6 +130,9 @@ func (rpc *Server) UpdateSession(ctx context.Context, update *clientpb.UpdateSes
|
||||
}
|
||||
session.Request(sliverpb.MsgNumber(&req), rpc.getTimeout(&req), data)
|
||||
}
|
||||
if len(update.Extensions) != 0 {
|
||||
session.Extensions = update.Extensions
|
||||
}
|
||||
core.Sessions.UpdateSession(session)
|
||||
resp = session.ToProtobuf()
|
||||
return resp, nil
|
||||
|
||||
@@ -74,6 +74,7 @@ type GenericResponse interface {
|
||||
|
||||
// NewServer - Create new server instance
|
||||
func NewServer() *Server {
|
||||
core.StartEventAutomation()
|
||||
return &Server{}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package encoders
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
)
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
@@ -23,6 +18,11 @@ import (
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
)
|
||||
|
||||
// GzipEncoderID - EncoderID
|
||||
const GzipEncoderID = 49
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package encoders
|
||||
|
||||
import "encoding/hex"
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
@@ -20,6 +18,8 @@ import "encoding/hex"
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "encoding/hex"
|
||||
|
||||
// HexEncoderID - EncoderID
|
||||
const HexEncoderID = 92
|
||||
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 jedib0t
|
||||
|
||||
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.
|
||||
+446
@@ -0,0 +1,446 @@
|
||||
# Table
|
||||
[](https://pkg.go.dev/github.com/jedib0t/go-pretty/v6/table)
|
||||
|
||||
Pretty-print tables into ASCII/Unicode strings.
|
||||
|
||||
- Add Rows one-by-one or as a group (`AppendRow`/`AppendRows`)
|
||||
- Add Header(s) and Footer(s) (`AppendHeader`/`AppendFooter`)
|
||||
- Add a Separator manually after any Row (`AppendSeparator`)
|
||||
- Auto Index Rows (1, 2, 3 ...) and Columns (A, B, C, ...) (`SetAutoIndex`)
|
||||
- Auto Merge
|
||||
- Cells in a Row (`RowConfig.AutoMerge`)
|
||||
- Columns (`ColumnConfig.AutoMerge`)
|
||||
- Limit the length of
|
||||
- Rows (`SetAllowedRowLength`)
|
||||
- Columns (`ColumnConfig.Width*`)
|
||||
- Page results by a specified number of Lines (`SetPageSize`)
|
||||
- Alignment - Horizontal & Vertical
|
||||
- Auto (horizontal) Align (numeric columns aligned Right)
|
||||
- Custom (horizontal) Align per column (`ColumnConfig.Align*`)
|
||||
- Custom (vertical) VAlign per column with multi-line cell support (`ColumnConfig.VAlign*`)
|
||||
- Mirror output to an `io.Writer` (ex. `os.StdOut`) (`SetOutputMirror`)
|
||||
- Sort by one or more Columns (`SortBy`)
|
||||
- Suppress/hide columns with no content (`SuppressEmptyColumns`)
|
||||
- Customizable Cell rendering per Column (`ColumnConfig.Transformer*`)
|
||||
- Hide any columns that you don't want displayed (`ColumnConfig.Hidden`)
|
||||
- Reset Headers/Rows/Footers at will to reuse the same Table Writer (`Reset*`)
|
||||
- Completely customizable styles (`SetStyle`/`Style`)
|
||||
- Many ready-to-use styles: [style.go](style.go)
|
||||
- Colorize Headers/Body/Footers using [../text/color.go](../text/color.go)
|
||||
- Custom text-case for Headers/Body/Footers
|
||||
- Enable separators between each row
|
||||
- Render table without a Border
|
||||
- and a lot more...
|
||||
- Render as:
|
||||
- (ASCII/Unicode) Table
|
||||
- CSV
|
||||
- HTML Table (with custom CSS Class)
|
||||
- Markdown Table
|
||||
|
||||
|
||||
```
|
||||
+---------------------------------------------------------------------+
|
||||
| Game of Thrones +
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| # | FIRST NAME | LAST NAME | SALARY | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| 1 | Arya | Stark | 3000 | |
|
||||
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
| 300 | Tyrion | Lannister | 5000 | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| | | TOTAL | 10000 | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
```
|
||||
|
||||
A demonstration of all the capabilities can be found here:
|
||||
[../cmd/demo-table](../cmd/demo-table)
|
||||
|
||||
If you want very specific examples, read ahead.
|
||||
|
||||
# Examples
|
||||
|
||||
All the examples below are going to start with the following block, although
|
||||
nothing except a single Row is mandatory for the `Render()` function to render
|
||||
something:
|
||||
```golang
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
func main() {
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"})
|
||||
t.AppendRows([]table.Row{
|
||||
{1, "Arya", "Stark", 3000},
|
||||
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||
})
|
||||
t.AppendSeparator()
|
||||
t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000})
|
||||
t.AppendFooter(table.Row{"", "", "Total", 10000})
|
||||
t.Render()
|
||||
}
|
||||
```
|
||||
Running the above will result in:
|
||||
```
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| # | FIRST NAME | LAST NAME | SALARY | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| 1 | Arya | Stark | 3000 | |
|
||||
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| 300 | Tyrion | Lannister | 5000 | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| | | TOTAL | 10000 | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
```
|
||||
|
||||
## Styles
|
||||
|
||||
You can customize almost every single thing about the table above. The previous
|
||||
example just defaulted to `StyleDefault` during `Render()`. You can use a
|
||||
ready-to-use style (as in [style.go](style.go)) or customize it as you want.
|
||||
|
||||
### Ready-to-use Styles
|
||||
|
||||
Table comes with a bunch of ready-to-use Styles that make the table look really
|
||||
good. Set or Change the style using:
|
||||
```golang
|
||||
t.SetStyle(table.StyleLight)
|
||||
t.Render()
|
||||
```
|
||||
to get:
|
||||
```
|
||||
┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
│ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
│ 1 │ Arya │ Stark │ 3000 │ │
|
||||
│ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
│ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
│ │ │ TOTAL │ 10000 │ │
|
||||
└─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
```
|
||||
|
||||
Or if you want to use a full-color mode, and don't care for boxes, use:
|
||||
```golang
|
||||
t.SetStyle(table.StyleColoredBright)
|
||||
t.Render()
|
||||
```
|
||||
to get:
|
||||
|
||||
<img src="images/table-StyleColoredBright.png" width="640px" alt="Colored Table"/>
|
||||
|
||||
### Roll your own Style
|
||||
|
||||
You can also roll your own style:
|
||||
```golang
|
||||
t.SetStyle(table.Style{
|
||||
Name: "myNewStyle",
|
||||
Box: table.BoxStyle{
|
||||
BottomLeft: "\\",
|
||||
BottomRight: "/",
|
||||
BottomSeparator: "v",
|
||||
Left: "[",
|
||||
LeftSeparator: "{",
|
||||
MiddleHorizontal: "-",
|
||||
MiddleSeparator: "+",
|
||||
MiddleVertical: "|",
|
||||
PaddingLeft: "<",
|
||||
PaddingRight: ">",
|
||||
Right: "]",
|
||||
RightSeparator: "}",
|
||||
TopLeft: "(",
|
||||
TopRight: ")",
|
||||
TopSeparator: "^",
|
||||
UnfinishedRow: " ~~~",
|
||||
},
|
||||
Color: table.ColorOptions{
|
||||
IndexColumn: text.Colors{text.BgCyan, text.FgBlack},
|
||||
Footer: text.Colors{text.BgCyan, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiCyan, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
},
|
||||
Format: table.FormatOptions{
|
||||
Footer: text.FormatUpper,
|
||||
Header: text.FormatUpper,
|
||||
Row: text.FormatDefault,
|
||||
},
|
||||
Options: table.Options{
|
||||
DrawBorder: true,
|
||||
SeparateColumns: true,
|
||||
SeparateFooter: true,
|
||||
SeparateHeader: true,
|
||||
SeparateRows: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Or you can use one of the ready-to-use Styles, and just make a few tweaks:
|
||||
```golang
|
||||
t.SetStyle(table.StyleLight)
|
||||
t.Style().Color.Header = text.Colors{text.BgHiCyan, text.FgBlack}
|
||||
t.Style().Color.IndexColumn = text.Colors{text.BgHiCyan, text.FgBlack}
|
||||
t.Style().Format.Footer = text.FormatLower
|
||||
t.Style().Options.DrawBorder = false
|
||||
```
|
||||
|
||||
## Auto-Merge
|
||||
|
||||
You can auto-merge cells horizontally and vertically, but you have request for
|
||||
it specifically for each row/column using `RowConfig` or `ColumnConfig`.
|
||||
|
||||
```golang
|
||||
rowConfigAutoMerge := table.RowConfig{AutoMerge: true}
|
||||
|
||||
t := table.NewWriter()
|
||||
t.AppendHeader(table.Row{"Node IP", "Pods", "Namespace", "Container", "RCE", "RCE"}, rowConfigAutoMerge)
|
||||
t.AppendHeader(table.Row{"", "", "", "", "EXE", "RUN"})
|
||||
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 1", "Y", "Y"}, rowConfigAutoMerge)
|
||||
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1A", "C 2", "Y", "N"}, rowConfigAutoMerge)
|
||||
t.AppendRow(table.Row{"1.1.1.1", "Pod 1A", "NS 1B", "C 3", "N", "N"}, rowConfigAutoMerge)
|
||||
t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 4", "N", "N"}, rowConfigAutoMerge)
|
||||
t.AppendRow(table.Row{"1.1.1.1", "Pod 1B", "NS 2", "C 5", "Y", "N"}, rowConfigAutoMerge)
|
||||
t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 6", "Y", "Y"}, rowConfigAutoMerge)
|
||||
t.AppendRow(table.Row{"2.2.2.2", "Pod 2", "NS 3", "C 7", "Y", "Y"}, rowConfigAutoMerge)
|
||||
t.AppendFooter(table.Row{"", "", "", 7, 5, 3})
|
||||
t.SetAutoIndex(true)
|
||||
t.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 1, AutoMerge: true},
|
||||
{Number: 2, AutoMerge: true},
|
||||
{Number: 3, AutoMerge: true},
|
||||
{Number: 4, AutoMerge: true},
|
||||
{Number: 5, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},
|
||||
{Number: 6, Align: text.AlignCenter, AlignFooter: text.AlignCenter, AlignHeader: text.AlignCenter},
|
||||
})
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.SetStyle(table.StyleLight)
|
||||
t.Style().Options.SeparateRows = true
|
||||
fmt.Println(t.Render())
|
||||
```
|
||||
to get:
|
||||
```
|
||||
┌───┬─────────┬────────┬───────────┬───────────┬───────────┐
|
||||
│ │ NODE IP │ PODS │ NAMESPACE │ CONTAINER │ RCE │
|
||||
│ │ │ │ │ ├─────┬─────┤
|
||||
│ │ │ │ │ │ EXE │ RUN │
|
||||
├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤
|
||||
│ 1 │ 1.1.1.1 │ Pod 1A │ NS 1A │ C 1 │ Y │
|
||||
├───┤ │ │ ├───────────┼─────┬─────┤
|
||||
│ 2 │ │ │ │ C 2 │ Y │ N │
|
||||
├───┤ │ ├───────────┼───────────┼─────┴─────┤
|
||||
│ 3 │ │ │ NS 1B │ C 3 │ N │
|
||||
├───┤ ├────────┼───────────┼───────────┼───────────┤
|
||||
│ 4 │ │ Pod 1B │ NS 2 │ C 4 │ N │
|
||||
├───┤ │ │ ├───────────┼─────┬─────┤
|
||||
│ 5 │ │ │ │ C 5 │ Y │ N │
|
||||
├───┼─────────┼────────┼───────────┼───────────┼─────┴─────┤
|
||||
│ 6 │ 2.2.2.2 │ Pod 2 │ NS 3 │ C 6 │ Y │
|
||||
├───┤ │ │ ├───────────┼───────────┤
|
||||
│ 7 │ │ │ │ C 7 │ Y │
|
||||
├───┼─────────┼────────┼───────────┼───────────┼─────┬─────┤
|
||||
│ │ │ │ │ 7 │ 5 │ 3 │
|
||||
└───┴─────────┴────────┴───────────┴───────────┴─────┴─────┘
|
||||
```
|
||||
|
||||
## Paging
|
||||
|
||||
You can limit then number of lines rendered in a single "Page". This logic
|
||||
can handle rows with multiple lines too. Here is a simple example:
|
||||
```golang
|
||||
t.SetPageSize(1)
|
||||
t.Render()
|
||||
```
|
||||
to get:
|
||||
```
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| # | FIRST NAME | LAST NAME | SALARY | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| 1 | Arya | Stark | 3000 | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| | | TOTAL | 10000 | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| # | FIRST NAME | LAST NAME | SALARY | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| | | TOTAL | 10000 | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| # | FIRST NAME | LAST NAME | SALARY | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| 300 | Tyrion | Lannister | 5000 | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
| | | TOTAL | 10000 | |
|
||||
+-----+------------+-----------+--------+-----------------------------+
|
||||
```
|
||||
|
||||
## Sorting
|
||||
|
||||
Sorting can be done on one or more columns. The following code will make the
|
||||
rows be sorted first by "First Name" and then by "Last Name" (in case of similar
|
||||
"First Name" entries).
|
||||
```golang
|
||||
t.SortBy([]table.SortBy{
|
||||
{Name: "First Name", Mode: table.Asc},
|
||||
{Name: "Last Name", Mode: table.Asc},
|
||||
})
|
||||
```
|
||||
|
||||
## Wrapping (or) Row/Column Width restrictions
|
||||
|
||||
You can restrict the maximum (text) width for a Row:
|
||||
```golang
|
||||
t.SetAllowedRowLength(50)
|
||||
t.Render()
|
||||
```
|
||||
to get:
|
||||
```
|
||||
+-----+------------+-----------+--------+------- ~
|
||||
| # | FIRST NAME | LAST NAME | SALARY | ~
|
||||
+-----+------------+-----------+--------+------- ~
|
||||
| 1 | Arya | Stark | 3000 | ~
|
||||
| 20 | Jon | Snow | 2000 | You kn ~
|
||||
+-----+------------+-----------+--------+------- ~
|
||||
| 300 | Tyrion | Lannister | 5000 | ~
|
||||
+-----+------------+-----------+--------+------- ~
|
||||
| | | TOTAL | 10000 | ~
|
||||
+-----+------------+-----------+--------+------- ~
|
||||
```
|
||||
|
||||
## Column Control - Alignment, Colors, Width and more
|
||||
|
||||
You can control a lot of things about individual cells/columns which overrides
|
||||
global properties/styles using the `SetColumnConfig()` interface:
|
||||
- Alignment (horizontal & vertical)
|
||||
- Colorization
|
||||
- Transform individual cells based on the content
|
||||
- Visibility
|
||||
- Width (minimum & maximum)
|
||||
|
||||
```golang
|
||||
nameTransformer := text.Transformer(func(val interface{}) string {
|
||||
return text.Bold.Sprint(val)
|
||||
})
|
||||
|
||||
t.SetColumnConfigs([]ColumnConfig{
|
||||
{
|
||||
Name: "First Name",
|
||||
Align: text.AlignLeft,
|
||||
AlignFooter: text.AlignLeft,
|
||||
AlignHeader: text.AlignLeft,
|
||||
Colors: text.Colors{text.BgBlack, text.FgRed},
|
||||
ColorsHeader: text.Colors{text.BgRed, text.FgBlack, text.Bold},
|
||||
ColorsFooter: text.Colors{text.BgRed, text.FgBlack},
|
||||
Hidden: false,
|
||||
Transformer: nameTransformer,
|
||||
TransformerFooter: nameTransformer,
|
||||
TransformerHeader: nameTransformer,
|
||||
VAlign: text.VAlignMiddle,
|
||||
VAlignFooter: text.VAlignTop,
|
||||
VAlignHeader: text.VAlignBottom,
|
||||
WidthMin: 6,
|
||||
WidthMax: 64,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Render As ...
|
||||
|
||||
Tables can be rendered in other common formats such as:
|
||||
|
||||
### ... CSV
|
||||
|
||||
```golang
|
||||
t.RenderCSV()
|
||||
```
|
||||
to get:
|
||||
```
|
||||
,First Name,Last Name,Salary,
|
||||
1,Arya,Stark,3000,
|
||||
20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
|
||||
300,Tyrion,Lannister,5000,
|
||||
,,Total,10000,
|
||||
```
|
||||
|
||||
### ... HTML Table
|
||||
|
||||
```golang
|
||||
t.Style().HTML = table.HTMLOptions{
|
||||
CSSClass: "game-of-thrones",
|
||||
EmptyColumn: " ",
|
||||
EscapeText: true,
|
||||
Newline: "<br/>",
|
||||
}
|
||||
t.RenderHTML()
|
||||
```
|
||||
to get:
|
||||
```html
|
||||
<table class="game-of-thrones">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="right">#</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th align="right">Salary</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="right">1</td>
|
||||
<td>Arya</td>
|
||||
<td>Stark</td>
|
||||
<td align="right">3000</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">20</td>
|
||||
<td>Jon</td>
|
||||
<td>Snow</td>
|
||||
<td align="right">2000</td>
|
||||
<td>You know nothing, Jon Snow!</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">300</td>
|
||||
<td>Tyrion</td>
|
||||
<td>Lannister</td>
|
||||
<td align="right">5000</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td align="right"> </td>
|
||||
<td> </td>
|
||||
<td>Total</td>
|
||||
<td align="right">10000</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
```
|
||||
|
||||
### ... Markdown Table
|
||||
|
||||
```golang
|
||||
t.RenderMarkdown()
|
||||
```
|
||||
to get:
|
||||
```markdown
|
||||
| # | First Name | Last Name | Salary | |
|
||||
| ---:| --- | --- | ---:| --- |
|
||||
| 1 | Arya | Stark | 3000 | |
|
||||
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
| 300 | Tyrion | Lannister | 5000 | |
|
||||
| | | Total | 10000 | |
|
||||
```
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
)
|
||||
|
||||
// ColumnConfig contains configurations that determine and modify the way the
|
||||
// contents of the column get rendered.
|
||||
type ColumnConfig struct {
|
||||
// Name is the name of the Column as it appears in the first Header row.
|
||||
// If a Header is not provided, or the name is not found in the header, this
|
||||
// will not work.
|
||||
Name string
|
||||
// Number is the Column # from left. When specified, it overrides the Name
|
||||
// property. If you know the exact Column number, use this instead of Name.
|
||||
Number int
|
||||
|
||||
// Align defines the horizontal alignment
|
||||
Align text.Align
|
||||
// AlignFooter defines the horizontal alignment of Footer rows
|
||||
AlignFooter text.Align
|
||||
// AlignHeader defines the horizontal alignment of Header rows
|
||||
AlignHeader text.Align
|
||||
|
||||
// AutoMerge merges cells with similar values and prevents separators from
|
||||
// being drawn. Caveats:
|
||||
// * VAlign is applied on the individual cell and not on the merged cell
|
||||
// * Does not work in CSV/HTML/Markdown render modes
|
||||
// * Does not work well with horizontal auto-merge (RowConfig.AutoMerge)
|
||||
//
|
||||
// Works best when:
|
||||
// * Style().Options.SeparateRows == true
|
||||
// * Style().Color.Row == Style().Color.RowAlternate (or not set)
|
||||
AutoMerge bool
|
||||
|
||||
// Colors defines the colors to be used on the column
|
||||
Colors text.Colors
|
||||
// ColorsFooter defines the colors to be used on the column in Footer rows
|
||||
ColorsFooter text.Colors
|
||||
// ColorsHeader defines the colors to be used on the column in Header rows
|
||||
ColorsHeader text.Colors
|
||||
|
||||
// Hidden when set to true will prevent the column from being rendered.
|
||||
// This is useful in cases like needing a column for sorting, but not for
|
||||
// display.
|
||||
Hidden bool
|
||||
|
||||
// Transformer is a custom-function that changes the way the value gets
|
||||
// rendered to the console. Refer to text/transformer.go for ready-to-use
|
||||
// Transformer functions.
|
||||
Transformer text.Transformer
|
||||
// TransformerFooter is like Transformer but for Footer rows
|
||||
TransformerFooter text.Transformer
|
||||
// TransformerHeader is like Transformer but for Header rows
|
||||
TransformerHeader text.Transformer
|
||||
|
||||
// VAlign defines the vertical alignment
|
||||
VAlign text.VAlign
|
||||
// VAlignFooter defines the vertical alignment in Footer rows
|
||||
VAlignFooter text.VAlign
|
||||
// VAlignHeader defines the vertical alignment in Header rows
|
||||
VAlignHeader text.VAlign
|
||||
|
||||
// WidthMax defines the maximum character length of the column
|
||||
WidthMax int
|
||||
// WidthEnforcer enforces the WidthMax value on the column contents;
|
||||
// default: text.WrapText
|
||||
WidthMaxEnforcer WidthEnforcer
|
||||
// WidthMin defines the minimum character length of the column
|
||||
WidthMin int
|
||||
}
|
||||
|
||||
func (c ColumnConfig) getWidthMaxEnforcer() WidthEnforcer {
|
||||
if c.WidthMax == 0 {
|
||||
return widthEnforcerNone
|
||||
}
|
||||
if c.WidthMaxEnforcer != nil {
|
||||
return c.WidthMaxEnforcer
|
||||
}
|
||||
return text.WrapText
|
||||
}
|
||||
|
||||
// RowConfig contains configurations that determine and modify the way the
|
||||
// contents of a row get rendered.
|
||||
type RowConfig struct {
|
||||
// AutoMerge merges cells with similar values and prevents separators from
|
||||
// being drawn. Caveats:
|
||||
// * Align is overridden to text.AlignCenter on the merged cell
|
||||
// * Does not work in CSV/HTML/Markdown render modes
|
||||
// * Does not work well with vertical auto-merge (ColumnConfig.AutoMerge)
|
||||
AutoMerge bool
|
||||
}
|
||||
+431
@@ -0,0 +1,431 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
)
|
||||
|
||||
// Render renders the Table in a human-readable "pretty" format. Example:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
func (t *Table) Render() string {
|
||||
t.initForRender()
|
||||
|
||||
var out strings.Builder
|
||||
if t.numColumns > 0 {
|
||||
t.renderTitle(&out)
|
||||
|
||||
// top-most border
|
||||
t.renderRowsBorderTop(&out)
|
||||
|
||||
// header rows
|
||||
t.renderRowsHeader(&out)
|
||||
|
||||
// (data) rows
|
||||
t.renderRows(&out, t.rows, renderHint{})
|
||||
|
||||
// footer rows
|
||||
t.renderRowsFooter(&out)
|
||||
|
||||
// bottom-most border
|
||||
t.renderRowsBorderBottom(&out)
|
||||
|
||||
// caption
|
||||
if t.caption != "" {
|
||||
out.WriteRune('\n')
|
||||
out.WriteString(t.caption)
|
||||
}
|
||||
}
|
||||
return t.render(&out)
|
||||
}
|
||||
|
||||
func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxColumnLength int, hint renderHint) int {
|
||||
numColumnsRenderer := 1
|
||||
|
||||
// when working on the first column, and autoIndex is true, insert a new
|
||||
// column with the row number on it.
|
||||
if colIdx == 0 && t.autoIndex {
|
||||
hintAutoIndex := hint
|
||||
hintAutoIndex.isAutoIndexColumn = true
|
||||
t.renderColumnAutoIndex(out, hintAutoIndex)
|
||||
}
|
||||
|
||||
// when working on column number 2 or more, render the column separator
|
||||
if colIdx > 0 {
|
||||
t.renderColumnSeparator(out, row, colIdx, hint)
|
||||
}
|
||||
|
||||
// extract the text, convert-case if not-empty and align horizontally
|
||||
mergeVertically := t.shouldMergeCellsVertically(colIdx, hint)
|
||||
var colStr string
|
||||
if mergeVertically {
|
||||
// leave colStr empty; align will expand the column as necessary
|
||||
} else if colIdx < len(row) {
|
||||
colStr = t.getFormat(hint).Apply(row[colIdx])
|
||||
}
|
||||
align := t.getAlign(colIdx, hint)
|
||||
|
||||
// if horizontal cell merges are enabled, look ahead and see how many cells
|
||||
// have the same content and merge them all until a cell with a different
|
||||
// content is found; override alignment to Center in this case
|
||||
if t.getRowConfig(hint).AutoMerge && !hint.isSeparatorRow {
|
||||
for idx := colIdx + 1; idx < len(row); idx++ {
|
||||
if row[colIdx] != row[idx] {
|
||||
break
|
||||
}
|
||||
align = text.AlignCenter
|
||||
maxColumnLength += t.maxColumnLengths[idx] +
|
||||
text.RuneCount(t.style.Box.PaddingRight+t.style.Box.PaddingLeft) +
|
||||
text.RuneCount(t.style.Box.PaddingRight)
|
||||
numColumnsRenderer++
|
||||
}
|
||||
}
|
||||
colStr = align.Apply(colStr, maxColumnLength)
|
||||
|
||||
// pad both sides of the column
|
||||
if !hint.isSeparatorRow || (hint.isSeparatorRow && mergeVertically) {
|
||||
colStr = t.style.Box.PaddingLeft + colStr + t.style.Box.PaddingRight
|
||||
}
|
||||
|
||||
t.renderColumnColorized(out, colIdx, colStr, hint)
|
||||
|
||||
return colIdx + numColumnsRenderer
|
||||
}
|
||||
|
||||
func (t *Table) renderColumnAutoIndex(out *strings.Builder, hint renderHint) {
|
||||
var outAutoIndex strings.Builder
|
||||
outAutoIndex.Grow(t.maxColumnLengths[0])
|
||||
|
||||
if hint.isSeparatorRow {
|
||||
numChars := t.autoIndexVIndexMaxLength + utf8.RuneCountInString(t.style.Box.PaddingLeft) +
|
||||
utf8.RuneCountInString(t.style.Box.PaddingRight)
|
||||
chars := t.style.Box.MiddleHorizontal
|
||||
if hint.isAutoIndexColumn && hint.isHeaderOrFooterSeparator() {
|
||||
chars = text.RepeatAndTrim(" ", len(t.style.Box.MiddleHorizontal))
|
||||
}
|
||||
outAutoIndex.WriteString(text.RepeatAndTrim(chars, numChars))
|
||||
} else {
|
||||
outAutoIndex.WriteString(t.style.Box.PaddingLeft)
|
||||
rowNumStr := fmt.Sprint(hint.rowNumber)
|
||||
if hint.isHeaderRow || hint.isFooterRow || hint.rowLineNumber > 1 {
|
||||
rowNumStr = strings.Repeat(" ", t.autoIndexVIndexMaxLength)
|
||||
}
|
||||
outAutoIndex.WriteString(text.AlignRight.Apply(rowNumStr, t.autoIndexVIndexMaxLength))
|
||||
outAutoIndex.WriteString(t.style.Box.PaddingRight)
|
||||
}
|
||||
|
||||
if t.style.Color.IndexColumn != nil {
|
||||
colors := t.style.Color.IndexColumn
|
||||
if hint.isFooterRow {
|
||||
colors = t.style.Color.Footer
|
||||
}
|
||||
out.WriteString(colors.Sprint(outAutoIndex.String()))
|
||||
} else {
|
||||
out.WriteString(outAutoIndex.String())
|
||||
}
|
||||
hint.isAutoIndexColumn = true
|
||||
t.renderColumnSeparator(out, rowStr{}, 0, hint)
|
||||
}
|
||||
|
||||
func (t *Table) renderColumnColorized(out *strings.Builder, colIdx int, colStr string, hint renderHint) {
|
||||
colors := t.getColumnColors(colIdx, hint)
|
||||
if colors != nil {
|
||||
out.WriteString(colors.Sprint(colStr))
|
||||
} else if hint.isHeaderRow && t.style.Color.Header != nil {
|
||||
out.WriteString(t.style.Color.Header.Sprint(colStr))
|
||||
} else if hint.isFooterRow && t.style.Color.Footer != nil {
|
||||
out.WriteString(t.style.Color.Footer.Sprint(colStr))
|
||||
} else if hint.isRegularRow() {
|
||||
if colIdx == t.indexColumn-1 && t.style.Color.IndexColumn != nil {
|
||||
out.WriteString(t.style.Color.IndexColumn.Sprint(colStr))
|
||||
} else if hint.rowNumber%2 == 0 && t.style.Color.RowAlternate != nil {
|
||||
out.WriteString(t.style.Color.RowAlternate.Sprint(colStr))
|
||||
} else if t.style.Color.Row != nil {
|
||||
out.WriteString(t.style.Color.Row.Sprint(colStr))
|
||||
} else {
|
||||
out.WriteString(colStr)
|
||||
}
|
||||
} else {
|
||||
out.WriteString(colStr)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderColumnSeparator(out *strings.Builder, row rowStr, colIdx int, hint renderHint) {
|
||||
if t.style.Options.SeparateColumns {
|
||||
separator := t.getColumnSeparator(row, colIdx, hint)
|
||||
|
||||
colors := t.getSeparatorColors(hint)
|
||||
if colors.EscapeSeq() != "" {
|
||||
out.WriteString(colors.Sprint(separator))
|
||||
} else {
|
||||
out.WriteString(separator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
// if the output has content, it means that this call is working on line
|
||||
// number 2 or more; separate them with a newline
|
||||
if out.Len() > 0 {
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
|
||||
// use a brand new strings.Builder if a row length limit has been set
|
||||
var outLine *strings.Builder
|
||||
if t.allowedRowLength > 0 {
|
||||
outLine = &strings.Builder{}
|
||||
} else {
|
||||
outLine = out
|
||||
}
|
||||
// grow the strings.Builder to the maximum possible row length
|
||||
outLine.Grow(t.maxRowLength)
|
||||
|
||||
nextColIdx := 0
|
||||
t.renderMarginLeft(outLine, hint)
|
||||
for colIdx, maxColumnLength := range t.maxColumnLengths {
|
||||
if colIdx != nextColIdx {
|
||||
continue
|
||||
}
|
||||
nextColIdx = t.renderColumn(outLine, row, colIdx, maxColumnLength, hint)
|
||||
}
|
||||
t.renderMarginRight(outLine, hint)
|
||||
|
||||
// merge the strings.Builder objects if a new one was created earlier
|
||||
if outLine != out {
|
||||
outLineStr := outLine.String()
|
||||
if text.RuneCount(outLineStr) > t.allowedRowLength {
|
||||
trimLength := t.allowedRowLength - utf8.RuneCountInString(t.style.Box.UnfinishedRow)
|
||||
if trimLength > 0 {
|
||||
out.WriteString(text.Trim(outLineStr, trimLength))
|
||||
out.WriteString(t.style.Box.UnfinishedRow)
|
||||
}
|
||||
} else {
|
||||
out.WriteString(outLineStr)
|
||||
}
|
||||
}
|
||||
|
||||
// if a page size has been set, and said number of lines has already
|
||||
// been rendered, and the header is not being rendered right now, render
|
||||
// the header all over again with a spacing line
|
||||
if hint.isRegularRow() {
|
||||
t.numLinesRendered++
|
||||
if t.pageSize > 0 && t.numLinesRendered%t.pageSize == 0 && !hint.isLastLineOfLastRow() {
|
||||
t.renderRowsFooter(out)
|
||||
t.renderRowsBorderBottom(out)
|
||||
out.WriteString(t.style.Box.PageSeparator)
|
||||
t.renderRowsBorderTop(out)
|
||||
t.renderRowsHeader(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) {
|
||||
if t.style.Options.DrawBorder {
|
||||
border := t.style.Box.Left
|
||||
if hint.isBorderTop {
|
||||
if t.title != "" {
|
||||
border = t.style.Box.LeftSeparator
|
||||
} else {
|
||||
border = t.style.Box.TopLeft
|
||||
}
|
||||
} else if hint.isBorderBottom {
|
||||
border = t.style.Box.BottomLeft
|
||||
} else if hint.isSeparatorRow {
|
||||
if t.autoIndex && hint.isHeaderOrFooterSeparator() {
|
||||
border = t.style.Box.Left
|
||||
} else if !t.autoIndex && t.shouldMergeCellsVertically(0, hint) {
|
||||
border = t.style.Box.Left
|
||||
} else {
|
||||
border = t.style.Box.LeftSeparator
|
||||
}
|
||||
}
|
||||
|
||||
colors := t.getBorderColors(hint)
|
||||
if colors.EscapeSeq() != "" {
|
||||
out.WriteString(colors.Sprint(border))
|
||||
} else {
|
||||
out.WriteString(border)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderMarginRight(out *strings.Builder, hint renderHint) {
|
||||
if t.style.Options.DrawBorder {
|
||||
border := t.style.Box.Right
|
||||
if hint.isBorderTop {
|
||||
if t.title != "" {
|
||||
border = t.style.Box.RightSeparator
|
||||
} else {
|
||||
border = t.style.Box.TopRight
|
||||
}
|
||||
} else if hint.isBorderBottom {
|
||||
border = t.style.Box.BottomRight
|
||||
} else if hint.isSeparatorRow {
|
||||
if t.shouldMergeCellsVertically(t.numColumns-1, hint) {
|
||||
border = t.style.Box.Right
|
||||
} else {
|
||||
border = t.style.Box.RightSeparator
|
||||
}
|
||||
}
|
||||
|
||||
colors := t.getBorderColors(hint)
|
||||
if colors.EscapeSeq() != "" {
|
||||
out.WriteString(colors.Sprint(border))
|
||||
} else {
|
||||
out.WriteString(border)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
if len(row) > 0 {
|
||||
// fit every column into the allowedColumnLength/maxColumnLength limit
|
||||
// and in the process find the max. number of lines in any column in
|
||||
// this row
|
||||
colMaxLines := 0
|
||||
rowWrapped := make(rowStr, len(row))
|
||||
for colIdx, colStr := range row {
|
||||
widthEnforcer := t.columnConfigMap[colIdx].getWidthMaxEnforcer()
|
||||
rowWrapped[colIdx] = widthEnforcer(colStr, t.maxColumnLengths[colIdx])
|
||||
colNumLines := strings.Count(rowWrapped[colIdx], "\n") + 1
|
||||
if colNumLines > colMaxLines {
|
||||
colMaxLines = colNumLines
|
||||
}
|
||||
}
|
||||
|
||||
// if there is just 1 line in all columns, add the row as such; else
|
||||
// split each column into individual lines and render them one-by-one
|
||||
if colMaxLines == 1 {
|
||||
hint.isLastLineOfRow = true
|
||||
t.renderLine(out, rowWrapped, hint)
|
||||
} else {
|
||||
// convert one row into N # of rows based on colMaxLines
|
||||
rowLines := make([]rowStr, len(row))
|
||||
for colIdx, colStr := range rowWrapped {
|
||||
rowLines[colIdx] = t.getVAlign(colIdx, hint).ApplyStr(colStr, colMaxLines)
|
||||
}
|
||||
for colLineIdx := 0; colLineIdx < colMaxLines; colLineIdx++ {
|
||||
rowLine := make(rowStr, len(rowLines))
|
||||
for colIdx, colLines := range rowLines {
|
||||
rowLine[colIdx] = colLines[colLineIdx]
|
||||
}
|
||||
hint.isLastLineOfRow = colLineIdx == colMaxLines-1
|
||||
hint.rowLineNumber = colLineIdx + 1
|
||||
t.renderLine(out, rowLine, hint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderRowSeparator(out *strings.Builder, hint renderHint) {
|
||||
if hint.isBorderTop || hint.isBorderBottom {
|
||||
if !t.style.Options.DrawBorder {
|
||||
return
|
||||
}
|
||||
} else if hint.isHeaderRow && !t.style.Options.SeparateHeader {
|
||||
return
|
||||
} else if hint.isFooterRow && !t.style.Options.SeparateFooter {
|
||||
return
|
||||
}
|
||||
hint.isSeparatorRow = true
|
||||
t.renderLine(out, t.rowSeparator, hint)
|
||||
}
|
||||
|
||||
func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||
for rowIdx, row := range rows {
|
||||
hint.isFirstRow = rowIdx == 0
|
||||
hint.isLastRow = rowIdx == len(rows)-1
|
||||
hint.rowNumber = rowIdx + 1
|
||||
t.renderRow(out, row, hint)
|
||||
|
||||
if (t.style.Options.SeparateRows && rowIdx < len(rows)-1) || // last row before footer
|
||||
(t.separators[rowIdx] && rowIdx != len(rows)-1) { // manually added separator not after last row
|
||||
hint.isFirstRow = false
|
||||
t.renderRowSeparator(out, hint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderRowsBorderBottom(out *strings.Builder) {
|
||||
t.renderRowSeparator(out, renderHint{isBorderBottom: true, isFooterRow: true})
|
||||
}
|
||||
|
||||
func (t *Table) renderRowsBorderTop(out *strings.Builder) {
|
||||
t.renderRowSeparator(out, renderHint{isBorderTop: true, isHeaderRow: true})
|
||||
}
|
||||
|
||||
func (t *Table) renderRowsFooter(out *strings.Builder) {
|
||||
if len(t.rowsFooter) > 0 {
|
||||
t.renderRowSeparator(out, renderHint{
|
||||
isFooterRow: true,
|
||||
isFirstRow: true,
|
||||
isSeparatorRow: true,
|
||||
})
|
||||
t.renderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderRowsHeader(out *strings.Builder) {
|
||||
if len(t.rowsHeader) > 0 || t.autoIndex {
|
||||
if len(t.rowsHeader) > 0 {
|
||||
t.renderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||
} else if t.autoIndex {
|
||||
t.renderRow(out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true})
|
||||
}
|
||||
t.renderRowSeparator(out, renderHint{
|
||||
isHeaderRow: true,
|
||||
isLastRow: true,
|
||||
isSeparatorRow: true,
|
||||
rowNumber: len(t.rowsHeader),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderTitle(out *strings.Builder) {
|
||||
if t.title != "" {
|
||||
rowLength := t.maxRowLength
|
||||
if t.allowedRowLength != 0 && t.allowedRowLength < rowLength {
|
||||
rowLength = t.allowedRowLength
|
||||
}
|
||||
if t.style.Options.DrawBorder {
|
||||
lenBorder := rowLength - text.RuneCount(t.style.Box.TopLeft+t.style.Box.TopRight)
|
||||
out.WriteString(t.style.Box.TopLeft)
|
||||
out.WriteString(text.RepeatAndTrim(t.style.Box.MiddleHorizontal, lenBorder))
|
||||
out.WriteString(t.style.Box.TopRight)
|
||||
}
|
||||
|
||||
lenText := rowLength - text.RuneCount(t.style.Box.PaddingLeft+t.style.Box.PaddingRight)
|
||||
if t.style.Options.DrawBorder {
|
||||
lenText -= text.RuneCount(t.style.Box.Left + t.style.Box.Right)
|
||||
}
|
||||
titleText := text.WrapText(t.title, lenText)
|
||||
for _, titleLine := range strings.Split(titleText, "\n") {
|
||||
titleLine = strings.TrimSpace(titleLine)
|
||||
titleLine = t.style.Title.Format.Apply(titleLine)
|
||||
titleLine = t.style.Title.Align.Apply(titleLine, lenText)
|
||||
titleLine = t.style.Box.PaddingLeft + titleLine + t.style.Box.PaddingRight
|
||||
titleLine = t.style.Title.Colors.Sprint(titleLine)
|
||||
|
||||
if out.Len() > 0 {
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
if t.style.Options.DrawBorder {
|
||||
out.WriteString(t.style.Box.Left)
|
||||
}
|
||||
out.WriteString(titleLine)
|
||||
if t.style.Options.DrawBorder {
|
||||
out.WriteString(t.style.Box.Right)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// RenderCSV renders the Table in CSV format. Example:
|
||||
// #,First Name,Last Name,Salary,
|
||||
// 1,Arya,Stark,3000,
|
||||
// 20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
|
||||
// 300,Tyrion,Lannister,5000,
|
||||
// ,,Total,10000,
|
||||
func (t *Table) RenderCSV() string {
|
||||
t.initForRender()
|
||||
|
||||
var out strings.Builder
|
||||
if t.numColumns > 0 {
|
||||
if t.title != "" {
|
||||
out.WriteString(t.title)
|
||||
}
|
||||
if t.autoIndex && len(t.rowsHeader) == 0 {
|
||||
t.csvRenderRow(&out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true})
|
||||
}
|
||||
t.csvRenderRows(&out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||
t.csvRenderRows(&out, t.rows, renderHint{})
|
||||
t.csvRenderRows(&out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||
if t.caption != "" {
|
||||
out.WriteRune('\n')
|
||||
out.WriteString(t.caption)
|
||||
}
|
||||
}
|
||||
return t.render(&out)
|
||||
}
|
||||
|
||||
func (t *Table) csvFixCommas(str string) string {
|
||||
return strings.Replace(str, ",", "\\,", -1)
|
||||
}
|
||||
|
||||
func (t *Table) csvFixDoubleQuotes(str string) string {
|
||||
return strings.Replace(str, "\"", "\\\"", -1)
|
||||
}
|
||||
|
||||
func (t *Table) csvRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
// when working on line number 2 or more, insert a newline first
|
||||
if out.Len() > 0 {
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
|
||||
// generate the columns to render in CSV format and append to "out"
|
||||
for colIdx, colStr := range row {
|
||||
// auto-index column
|
||||
if colIdx == 0 && t.autoIndex {
|
||||
if hint.isRegularRow() {
|
||||
out.WriteString(fmt.Sprint(hint.rowNumber))
|
||||
}
|
||||
out.WriteRune(',')
|
||||
}
|
||||
if colIdx > 0 {
|
||||
out.WriteRune(',')
|
||||
}
|
||||
if strings.ContainsAny(colStr, "\",\n") {
|
||||
out.WriteRune('"')
|
||||
out.WriteString(t.csvFixCommas(t.csvFixDoubleQuotes(colStr)))
|
||||
out.WriteRune('"')
|
||||
} else if utf8.RuneCountInString(colStr) > 0 {
|
||||
out.WriteString(colStr)
|
||||
}
|
||||
}
|
||||
for colIdx := len(row); colIdx < t.numColumns; colIdx++ {
|
||||
out.WriteRune(',')
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) csvRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||
for rowIdx, row := range rows {
|
||||
hint.rowNumber = rowIdx + 1
|
||||
t.csvRenderRow(out, row, hint)
|
||||
}
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultHTMLCSSClass stores the css-class to use when none-provided via
|
||||
// SetHTMLCSSClass(cssClass string).
|
||||
DefaultHTMLCSSClass = "go-pretty-table"
|
||||
)
|
||||
|
||||
// RenderHTML renders the Table in HTML format. Example:
|
||||
// <table class="go-pretty-table">
|
||||
// <thead>
|
||||
// <tr>
|
||||
// <th align="right">#</th>
|
||||
// <th>First Name</th>
|
||||
// <th>Last Name</th>
|
||||
// <th align="right">Salary</th>
|
||||
// <th> </th>
|
||||
// </tr>
|
||||
// </thead>
|
||||
// <tbody>
|
||||
// <tr>
|
||||
// <td align="right">1</td>
|
||||
// <td>Arya</td>
|
||||
// <td>Stark</td>
|
||||
// <td align="right">3000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// <tr>
|
||||
// <td align="right">20</td>
|
||||
// <td>Jon</td>
|
||||
// <td>Snow</td>
|
||||
// <td align="right">2000</td>
|
||||
// <td>You know nothing, Jon Snow!</td>
|
||||
// </tr>
|
||||
// <tr>
|
||||
// <td align="right">300</td>
|
||||
// <td>Tyrion</td>
|
||||
// <td>Lannister</td>
|
||||
// <td align="right">5000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// </tbody>
|
||||
// <tfoot>
|
||||
// <tr>
|
||||
// <td align="right"> </td>
|
||||
// <td> </td>
|
||||
// <td>Total</td>
|
||||
// <td align="right">10000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// </tfoot>
|
||||
// </table>
|
||||
func (t *Table) RenderHTML() string {
|
||||
t.initForRender()
|
||||
|
||||
var out strings.Builder
|
||||
if t.numColumns > 0 {
|
||||
out.WriteString("<table class=\"")
|
||||
if t.htmlCSSClass != "" {
|
||||
out.WriteString(t.htmlCSSClass)
|
||||
} else {
|
||||
out.WriteString(t.style.HTML.CSSClass)
|
||||
}
|
||||
out.WriteString("\">\n")
|
||||
t.htmlRenderTitle(&out)
|
||||
t.htmlRenderRowsHeader(&out)
|
||||
t.htmlRenderRows(&out, t.rows, renderHint{})
|
||||
t.htmlRenderRowsFooter(&out)
|
||||
t.htmlRenderCaption(&out)
|
||||
out.WriteString("</table>")
|
||||
}
|
||||
return t.render(&out)
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderCaption(out *strings.Builder) {
|
||||
if t.caption != "" {
|
||||
out.WriteString(" <caption class=\"caption\" style=\"caption-side: bottom;\">")
|
||||
out.WriteString(t.caption)
|
||||
out.WriteString("</caption>\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderColumnAttributes(out *strings.Builder, row rowStr, colIdx int, hint renderHint) {
|
||||
// determine the HTML "align"/"valign" property values
|
||||
align := t.getAlign(colIdx, hint).HTMLProperty()
|
||||
vAlign := t.getVAlign(colIdx, hint).HTMLProperty()
|
||||
// determine the HTML "class" property values for the colors
|
||||
class := t.getColumnColors(colIdx, hint).HTMLProperty()
|
||||
|
||||
if align != "" {
|
||||
out.WriteRune(' ')
|
||||
out.WriteString(align)
|
||||
}
|
||||
if class != "" {
|
||||
out.WriteRune(' ')
|
||||
out.WriteString(class)
|
||||
}
|
||||
if vAlign != "" {
|
||||
out.WriteRune(' ')
|
||||
out.WriteString(vAlign)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderColumnAutoIndex(out *strings.Builder, hint renderHint) {
|
||||
if hint.isHeaderRow {
|
||||
out.WriteString(" <th>")
|
||||
out.WriteString(t.style.HTML.EmptyColumn)
|
||||
out.WriteString("</th>\n")
|
||||
} else if hint.isFooterRow {
|
||||
out.WriteString(" <td>")
|
||||
out.WriteString(t.style.HTML.EmptyColumn)
|
||||
out.WriteString("</td>\n")
|
||||
} else {
|
||||
out.WriteString(" <td align=\"right\">")
|
||||
out.WriteString(fmt.Sprint(hint.rowNumber))
|
||||
out.WriteString("</td>\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
out.WriteString(" <tr>\n")
|
||||
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
|
||||
// auto-index column
|
||||
if colIdx == 0 && t.autoIndex {
|
||||
t.htmlRenderColumnAutoIndex(out, hint)
|
||||
}
|
||||
|
||||
// get the column contents
|
||||
var colStr string
|
||||
if colIdx < len(row) {
|
||||
colStr = row[colIdx]
|
||||
}
|
||||
|
||||
// header uses "th" instead of "td"
|
||||
colTagName := "td"
|
||||
if hint.isHeaderRow {
|
||||
colTagName = "th"
|
||||
}
|
||||
|
||||
// write the row
|
||||
out.WriteString(" <")
|
||||
out.WriteString(colTagName)
|
||||
t.htmlRenderColumnAttributes(out, row, colIdx, hint)
|
||||
out.WriteString(">")
|
||||
if len(colStr) == 0 {
|
||||
out.WriteString(t.style.HTML.EmptyColumn)
|
||||
} else {
|
||||
if t.style.HTML.EscapeText {
|
||||
colStr = html.EscapeString(colStr)
|
||||
}
|
||||
if t.style.HTML.Newline != "\n" {
|
||||
colStr = strings.Replace(colStr, "\n", t.style.HTML.Newline, -1)
|
||||
}
|
||||
out.WriteString(colStr)
|
||||
}
|
||||
out.WriteString("</")
|
||||
out.WriteString(colTagName)
|
||||
out.WriteString(">\n")
|
||||
}
|
||||
out.WriteString(" </tr>\n")
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||
if len(rows) > 0 {
|
||||
// determine that tag to use based on the type of the row
|
||||
rowsTag := "tbody"
|
||||
if hint.isHeaderRow {
|
||||
rowsTag = "thead"
|
||||
} else if hint.isFooterRow {
|
||||
rowsTag = "tfoot"
|
||||
}
|
||||
|
||||
var renderedTagOpen, shouldRenderTagClose bool
|
||||
for idx, row := range rows {
|
||||
hint.rowNumber = idx + 1
|
||||
if len(row) > 0 {
|
||||
if !renderedTagOpen {
|
||||
out.WriteString(" <")
|
||||
out.WriteString(rowsTag)
|
||||
out.WriteString(">\n")
|
||||
renderedTagOpen = true
|
||||
}
|
||||
t.htmlRenderRow(out, row, hint)
|
||||
shouldRenderTagClose = true
|
||||
}
|
||||
}
|
||||
if shouldRenderTagClose {
|
||||
out.WriteString(" </")
|
||||
out.WriteString(rowsTag)
|
||||
out.WriteString(">\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderRowsFooter(out *strings.Builder) {
|
||||
if len(t.rowsFooter) > 0 {
|
||||
t.htmlRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderRowsHeader(out *strings.Builder) {
|
||||
if len(t.rowsHeader) > 0 {
|
||||
t.htmlRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||
} else if t.autoIndex {
|
||||
hint := renderHint{isAutoIndexRow: true, isHeaderRow: true}
|
||||
t.htmlRenderRows(out, []rowStr{t.getAutoIndexColumnIDs()}, hint)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderTitle(out *strings.Builder) {
|
||||
if t.title != "" {
|
||||
align := t.style.Title.Align.HTMLProperty()
|
||||
colors := t.style.Title.Colors.HTMLProperty()
|
||||
title := t.style.Title.Format.Apply(t.title)
|
||||
|
||||
out.WriteString(" <caption class=\"title\"")
|
||||
if align != "" {
|
||||
out.WriteRune(' ')
|
||||
out.WriteString(align)
|
||||
}
|
||||
if colors != "" {
|
||||
out.WriteRune(' ')
|
||||
out.WriteString(colors)
|
||||
}
|
||||
out.WriteRune('>')
|
||||
out.WriteString(title)
|
||||
out.WriteString("</caption>\n")
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RenderMarkdown renders the Table in Markdown format. Example:
|
||||
// | # | First Name | Last Name | Salary | |
|
||||
// | ---:| --- | --- | ---:| --- |
|
||||
// | 1 | Arya | Stark | 3000 | |
|
||||
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
// | 300 | Tyrion | Lannister | 5000 | |
|
||||
// | | | Total | 10000 | |
|
||||
func (t *Table) RenderMarkdown() string {
|
||||
t.initForRender()
|
||||
|
||||
var out strings.Builder
|
||||
if t.numColumns > 0 {
|
||||
t.markdownRenderTitle(&out)
|
||||
t.markdownRenderRowsHeader(&out)
|
||||
t.markdownRenderRows(&out, t.rows, renderHint{})
|
||||
t.markdownRenderRowsFooter(&out)
|
||||
t.markdownRenderCaption(&out)
|
||||
}
|
||||
return t.render(&out)
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderCaption(out *strings.Builder) {
|
||||
if t.caption != "" {
|
||||
out.WriteRune('\n')
|
||||
out.WriteRune('_')
|
||||
out.WriteString(t.caption)
|
||||
out.WriteRune('_')
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
// when working on line number 2 or more, insert a newline first
|
||||
if out.Len() > 0 {
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
|
||||
// render each column up to the max. columns seen in all the rows
|
||||
out.WriteRune('|')
|
||||
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
|
||||
// auto-index column
|
||||
if colIdx == 0 && t.autoIndex {
|
||||
out.WriteRune(' ')
|
||||
if hint.isSeparatorRow {
|
||||
out.WriteString("---:")
|
||||
} else if hint.isRegularRow() {
|
||||
out.WriteString(fmt.Sprintf("%d ", hint.rowNumber))
|
||||
}
|
||||
out.WriteRune('|')
|
||||
}
|
||||
|
||||
if hint.isSeparatorRow {
|
||||
out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty())
|
||||
} else {
|
||||
var colStr string
|
||||
if colIdx < len(row) {
|
||||
colStr = row[colIdx]
|
||||
}
|
||||
out.WriteRune(' ')
|
||||
if strings.Contains(colStr, "|") {
|
||||
colStr = strings.Replace(colStr, "|", "\\|", -1)
|
||||
}
|
||||
if strings.Contains(colStr, "\n") {
|
||||
colStr = strings.Replace(colStr, "\n", "<br/>", -1)
|
||||
}
|
||||
out.WriteString(colStr)
|
||||
out.WriteRune(' ')
|
||||
}
|
||||
out.WriteRune('|')
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||
if len(rows) > 0 {
|
||||
for idx, row := range rows {
|
||||
hint.rowNumber = idx + 1
|
||||
t.markdownRenderRow(out, row, hint)
|
||||
|
||||
if idx == len(rows)-1 && hint.isHeaderRow {
|
||||
t.markdownRenderRow(out, t.rowSeparator, renderHint{isSeparatorRow: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderRowsFooter(out *strings.Builder) {
|
||||
t.markdownRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderRowsHeader(out *strings.Builder) {
|
||||
if len(t.rowsHeader) > 0 {
|
||||
t.markdownRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||
} else if t.autoIndex {
|
||||
t.markdownRenderRows(out, []rowStr{t.getAutoIndexColumnIDs()}, renderHint{isAutoIndexRow: true, isHeaderRow: true})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderTitle(out *strings.Builder) {
|
||||
if t.title != "" {
|
||||
out.WriteString("# ")
|
||||
out.WriteString(t.title)
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SortBy defines What to sort (Column Name or Number), and How to sort (Mode).
|
||||
type SortBy struct {
|
||||
// Name is the name of the Column as it appears in the first Header row.
|
||||
// If a Header is not provided, or the name is not found in the header, this
|
||||
// will not work.
|
||||
Name string
|
||||
// Number is the Column # from left. When specified, it overrides the Name
|
||||
// property. If you know the exact Column number, use this instead of Name.
|
||||
Number int
|
||||
|
||||
// Mode tells the Writer how to Sort. Asc/Dsc/etc.
|
||||
Mode SortMode
|
||||
}
|
||||
|
||||
// SortMode defines How to sort.
|
||||
type SortMode int
|
||||
|
||||
const (
|
||||
// Asc sorts the column in Ascending order alphabetically.
|
||||
Asc SortMode = iota
|
||||
// AscNumeric sorts the column in Ascending order numerically.
|
||||
AscNumeric
|
||||
// Dsc sorts the column in Descending order alphabetically.
|
||||
Dsc
|
||||
// DscNumeric sorts the column in Descending order numerically.
|
||||
DscNumeric
|
||||
)
|
||||
|
||||
type rowsSorter struct {
|
||||
rows []rowStr
|
||||
sortBy []SortBy
|
||||
sortedIndices []int
|
||||
}
|
||||
|
||||
// getSortedRowIndices sorts and returns the row indices in Sorted order as
|
||||
// directed by Table.sortBy which can be set using Table.SortBy(...)
|
||||
func (t *Table) getSortedRowIndices() []int {
|
||||
sortedIndices := make([]int, len(t.rows))
|
||||
for idx := range t.rows {
|
||||
sortedIndices[idx] = idx
|
||||
}
|
||||
|
||||
if t.sortBy != nil && len(t.sortBy) > 0 {
|
||||
sort.Sort(rowsSorter{
|
||||
rows: t.rows,
|
||||
sortBy: t.parseSortBy(t.sortBy),
|
||||
sortedIndices: sortedIndices,
|
||||
})
|
||||
}
|
||||
|
||||
return sortedIndices
|
||||
}
|
||||
|
||||
func (t *Table) parseSortBy(sortBy []SortBy) []SortBy {
|
||||
var resSortBy []SortBy
|
||||
for _, col := range sortBy {
|
||||
colNum := 0
|
||||
if col.Number > 0 && col.Number <= t.numColumns {
|
||||
colNum = col.Number
|
||||
} else if col.Name != "" && len(t.rowsHeader) > 0 {
|
||||
for idx, colName := range t.rowsHeader[0] {
|
||||
if col.Name == colName {
|
||||
colNum = idx + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if colNum > 0 {
|
||||
resSortBy = append(resSortBy, SortBy{
|
||||
Name: col.Name,
|
||||
Number: colNum,
|
||||
Mode: col.Mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
return resSortBy
|
||||
}
|
||||
|
||||
func (rs rowsSorter) Len() int {
|
||||
return len(rs.rows)
|
||||
}
|
||||
|
||||
func (rs rowsSorter) Swap(i, j int) {
|
||||
rs.sortedIndices[i], rs.sortedIndices[j] = rs.sortedIndices[j], rs.sortedIndices[i]
|
||||
}
|
||||
|
||||
func (rs rowsSorter) Less(i, j int) bool {
|
||||
realI, realJ := rs.sortedIndices[i], rs.sortedIndices[j]
|
||||
for _, col := range rs.sortBy {
|
||||
rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], col.Number-1
|
||||
if colIdx < len(rowI) && colIdx < len(rowJ) {
|
||||
if rowI[colIdx] == rowJ[colIdx] {
|
||||
continue
|
||||
} else if col.Mode == Asc {
|
||||
return rowI[colIdx] < rowJ[colIdx]
|
||||
} else if col.Mode == Dsc {
|
||||
return rowI[colIdx] > rowJ[colIdx]
|
||||
}
|
||||
|
||||
iVal, iErr := strconv.ParseFloat(rowI[colIdx], 64)
|
||||
jVal, jErr := strconv.ParseFloat(rowJ[colIdx], 64)
|
||||
if iErr == nil && jErr == nil {
|
||||
if col.Mode == AscNumeric {
|
||||
return iVal < jVal
|
||||
} else if col.Mode == DscNumeric {
|
||||
return jVal < iVal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+874
@@ -0,0 +1,874 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
)
|
||||
|
||||
// Style declares how to render the Table and provides very fine-grained control
|
||||
// on how the Table gets rendered on the Console.
|
||||
type Style struct {
|
||||
Name string // name of the Style
|
||||
Box BoxStyle // characters to use for the boxes
|
||||
Color ColorOptions // colors to use for the rows and columns
|
||||
Format FormatOptions // formatting options for the rows and columns
|
||||
HTML HTMLOptions // rendering options for HTML mode
|
||||
Options Options // misc. options for the table
|
||||
Title TitleOptions // formation options for the title text
|
||||
}
|
||||
|
||||
var (
|
||||
// StyleDefault renders a Table like below:
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | # | FIRST NAME | LAST NAME | SALARY | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | 1 | Arya | Stark | 3000 | |
|
||||
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
// | 300 | Tyrion | Lannister | 5000 | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | | | TOTAL | 10000 | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
StyleDefault = Style{
|
||||
Name: "StyleDefault",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
|
||||
// StyleBold renders a Table like below:
|
||||
// ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃
|
||||
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃
|
||||
// ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃
|
||||
// ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃
|
||||
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃
|
||||
// ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
StyleBold = Style{
|
||||
Name: "StyleBold",
|
||||
Box: StyleBoxBold,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
|
||||
// StyleColoredBright renders a Table without any borders or separators,
|
||||
// and with Black text on Cyan background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBright = Style{
|
||||
Name: "StyleColoredBright",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBright,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsDark,
|
||||
}
|
||||
|
||||
// StyleColoredDark renders a Table without any borders or separators, and
|
||||
// with Header/Footer in Cyan text and other rows with White text, all on
|
||||
// Black background.
|
||||
StyleColoredDark = Style{
|
||||
Name: "StyleColoredDark",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsDark,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBright,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnBlueWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Blue background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnBlueWhite = Style{
|
||||
Name: "StyleColoredBlackOnBlueWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnBlueWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlueOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnCyanWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Cyan background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnCyanWhite = Style{
|
||||
Name: "StyleColoredBlackOnCyanWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnCyanWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsCyanOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnGreenWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Green background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnGreenWhite = Style{
|
||||
Name: "StyleColoredBlackOnGreenWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnGreenWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsGreenOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnMagentaWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Magenta background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnMagentaWhite = Style{
|
||||
Name: "StyleColoredBlackOnMagentaWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnMagentaWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsMagentaOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnYellowWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Yellow background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnYellowWhite = Style{
|
||||
Name: "StyleColoredBlackOnYellowWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnYellowWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsYellowOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnRedWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Red background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnRedWhite = Style{
|
||||
Name: "StyleColoredBlackOnRedWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnRedWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsRedOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlueWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Blue text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredBlueWhiteOnBlack = Style{
|
||||
Name: "StyleColoredBlueWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlueWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnBlue,
|
||||
}
|
||||
|
||||
// StyleColoredCyanWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Cyan text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredCyanWhiteOnBlack = Style{
|
||||
Name: "StyleColoredCyanWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsCyanWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnCyan,
|
||||
}
|
||||
|
||||
// StyleColoredGreenWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Green text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredGreenWhiteOnBlack = Style{
|
||||
Name: "StyleColoredGreenWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsGreenWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnGreen,
|
||||
}
|
||||
|
||||
// StyleColoredMagentaWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Magenta text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredMagentaWhiteOnBlack = Style{
|
||||
Name: "StyleColoredMagentaWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsMagentaWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnMagenta,
|
||||
}
|
||||
|
||||
// StyleColoredRedWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Red text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredRedWhiteOnBlack = Style{
|
||||
Name: "StyleColoredRedWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsRedWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnRed,
|
||||
}
|
||||
|
||||
// StyleColoredYellowWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Yellow text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredYellowWhiteOnBlack = Style{
|
||||
Name: "StyleColoredYellowWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsYellowWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnYellow,
|
||||
}
|
||||
|
||||
// StyleDouble renders a Table like below:
|
||||
// ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗
|
||||
// ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║
|
||||
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||
// ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║
|
||||
// ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║
|
||||
// ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║
|
||||
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||
// ║ ║ ║ TOTAL ║ 10000 ║ ║
|
||||
// ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝
|
||||
StyleDouble = Style{
|
||||
Name: "StyleDouble",
|
||||
Box: StyleBoxDouble,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
|
||||
// StyleLight renders a Table like below:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
StyleLight = Style{
|
||||
Name: "StyleLight",
|
||||
Box: StyleBoxLight,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
|
||||
// StyleRounded renders a Table like below:
|
||||
// ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯
|
||||
StyleRounded = Style{
|
||||
Name: "StyleRounded",
|
||||
Box: StyleBoxRounded,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
|
||||
// styleTest renders a Table like below:
|
||||
// (-----^------------^-----------^--------^-----------------------------)
|
||||
// [< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
|
||||
// {-----+------------+-----------+--------+-----------------------------}
|
||||
// [< 1>|<Arya >|<Stark >|< 3000>|< >]
|
||||
// [< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
|
||||
// [<300>|<Tyrion >|<Lannister>|< 5000>|< >]
|
||||
// {-----+------------+-----------+--------+-----------------------------}
|
||||
// [< >|< >|<TOTAL >|< 10000>|< >]
|
||||
// \-----v------------v-----------v--------v-----------------------------/
|
||||
styleTest = Style{
|
||||
Name: "styleTest",
|
||||
Box: styleBoxTest,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
)
|
||||
|
||||
// BoxStyle defines the characters/strings to use to render the borders and
|
||||
// separators for the Table.
|
||||
type BoxStyle struct {
|
||||
BottomLeft string
|
||||
BottomRight string
|
||||
BottomSeparator string
|
||||
EmptySeparator string
|
||||
Left string
|
||||
LeftSeparator string
|
||||
MiddleHorizontal string
|
||||
MiddleSeparator string
|
||||
MiddleVertical string
|
||||
PaddingLeft string
|
||||
PaddingRight string
|
||||
PageSeparator string
|
||||
Right string
|
||||
RightSeparator string
|
||||
TopLeft string
|
||||
TopRight string
|
||||
TopSeparator string
|
||||
UnfinishedRow string
|
||||
}
|
||||
|
||||
var (
|
||||
// StyleBoxDefault defines a Boxed-Table like below:
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | # | FIRST NAME | LAST NAME | SALARY | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | 1 | Arya | Stark | 3000 | |
|
||||
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
// | 300 | Tyrion | Lannister | 5000 | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | | | TOTAL | 10000 | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
StyleBoxDefault = BoxStyle{
|
||||
BottomLeft: "+",
|
||||
BottomRight: "+",
|
||||
BottomSeparator: "+",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("+")),
|
||||
Left: "|",
|
||||
LeftSeparator: "+",
|
||||
MiddleHorizontal: "-",
|
||||
MiddleSeparator: "+",
|
||||
MiddleVertical: "|",
|
||||
PaddingLeft: " ",
|
||||
PaddingRight: " ",
|
||||
PageSeparator: "\n",
|
||||
Right: "|",
|
||||
RightSeparator: "+",
|
||||
TopLeft: "+",
|
||||
TopRight: "+",
|
||||
TopSeparator: "+",
|
||||
UnfinishedRow: " ~",
|
||||
}
|
||||
|
||||
// StyleBoxBold defines a Boxed-Table like below:
|
||||
// ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃
|
||||
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃
|
||||
// ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃
|
||||
// ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃
|
||||
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃
|
||||
// ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
StyleBoxBold = BoxStyle{
|
||||
BottomLeft: "┗",
|
||||
BottomRight: "┛",
|
||||
BottomSeparator: "┻",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("╋")),
|
||||
Left: "┃",
|
||||
LeftSeparator: "┣",
|
||||
MiddleHorizontal: "━",
|
||||
MiddleSeparator: "╋",
|
||||
MiddleVertical: "┃",
|
||||
PaddingLeft: " ",
|
||||
PaddingRight: " ",
|
||||
PageSeparator: "\n",
|
||||
Right: "┃",
|
||||
RightSeparator: "┫",
|
||||
TopLeft: "┏",
|
||||
TopRight: "┓",
|
||||
TopSeparator: "┳",
|
||||
UnfinishedRow: " ≈",
|
||||
}
|
||||
|
||||
// StyleBoxDouble defines a Boxed-Table like below:
|
||||
// ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗
|
||||
// ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║
|
||||
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||
// ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║
|
||||
// ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║
|
||||
// ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║
|
||||
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||
// ║ ║ ║ TOTAL ║ 10000 ║ ║
|
||||
// ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝
|
||||
StyleBoxDouble = BoxStyle{
|
||||
BottomLeft: "╚",
|
||||
BottomRight: "╝",
|
||||
BottomSeparator: "╩",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("╬")),
|
||||
Left: "║",
|
||||
LeftSeparator: "╠",
|
||||
MiddleHorizontal: "═",
|
||||
MiddleSeparator: "╬",
|
||||
MiddleVertical: "║",
|
||||
PaddingLeft: " ",
|
||||
PaddingRight: " ",
|
||||
PageSeparator: "\n",
|
||||
Right: "║",
|
||||
RightSeparator: "╣",
|
||||
TopLeft: "╔",
|
||||
TopRight: "╗",
|
||||
TopSeparator: "╦",
|
||||
UnfinishedRow: " ≈",
|
||||
}
|
||||
|
||||
// StyleBoxLight defines a Boxed-Table like below:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
StyleBoxLight = BoxStyle{
|
||||
BottomLeft: "└",
|
||||
BottomRight: "┘",
|
||||
BottomSeparator: "┴",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("┼")),
|
||||
Left: "│",
|
||||
LeftSeparator: "├",
|
||||
MiddleHorizontal: "─",
|
||||
MiddleSeparator: "┼",
|
||||
MiddleVertical: "│",
|
||||
PaddingLeft: " ",
|
||||
PaddingRight: " ",
|
||||
PageSeparator: "\n",
|
||||
Right: "│",
|
||||
RightSeparator: "┤",
|
||||
TopLeft: "┌",
|
||||
TopRight: "┐",
|
||||
TopSeparator: "┬",
|
||||
UnfinishedRow: " ≈",
|
||||
}
|
||||
|
||||
// StyleBoxRounded defines a Boxed-Table like below:
|
||||
// ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯
|
||||
StyleBoxRounded = BoxStyle{
|
||||
BottomLeft: "╰",
|
||||
BottomRight: "╯",
|
||||
BottomSeparator: "┴",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("┼")),
|
||||
Left: "│",
|
||||
LeftSeparator: "├",
|
||||
MiddleHorizontal: "─",
|
||||
MiddleSeparator: "┼",
|
||||
MiddleVertical: "│",
|
||||
PaddingLeft: " ",
|
||||
PaddingRight: " ",
|
||||
PageSeparator: "\n",
|
||||
Right: "│",
|
||||
RightSeparator: "┤",
|
||||
TopLeft: "╭",
|
||||
TopRight: "╮",
|
||||
TopSeparator: "┬",
|
||||
UnfinishedRow: " ≈",
|
||||
}
|
||||
|
||||
// styleBoxTest defines a Boxed-Table like below:
|
||||
// (-----^------------^-----------^--------^-----------------------------)
|
||||
// [< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
|
||||
// {-----+------------+-----------+--------+-----------------------------}
|
||||
// [< 1>|<Arya >|<Stark >|< 3000>|< >]
|
||||
// [< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
|
||||
// [<300>|<Tyrion >|<Lannister>|< 5000>|< >]
|
||||
// {-----+------------+-----------+--------+-----------------------------}
|
||||
// [< >|< >|<TOTAL >|< 10000>|< >]
|
||||
// \-----v------------v-----------v--------v-----------------------------/
|
||||
styleBoxTest = BoxStyle{
|
||||
BottomLeft: "\\",
|
||||
BottomRight: "/",
|
||||
BottomSeparator: "v",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("+")),
|
||||
Left: "[",
|
||||
LeftSeparator: "{",
|
||||
MiddleHorizontal: "--",
|
||||
MiddleSeparator: "+",
|
||||
MiddleVertical: "|",
|
||||
PaddingLeft: "<",
|
||||
PaddingRight: ">",
|
||||
PageSeparator: "\n",
|
||||
Right: "]",
|
||||
RightSeparator: "}",
|
||||
TopLeft: "(",
|
||||
TopRight: ")",
|
||||
TopSeparator: "^",
|
||||
UnfinishedRow: " ~~~",
|
||||
}
|
||||
)
|
||||
|
||||
// ColorOptions defines the ANSI colors to use for parts of the Table.
|
||||
type ColorOptions struct {
|
||||
IndexColumn text.Colors // index-column colors (row #, etc.)
|
||||
Footer text.Colors // footer row(s) colors
|
||||
Header text.Colors // header row(s) colors
|
||||
Row text.Colors // regular row(s) colors
|
||||
RowAlternate text.Colors // regular row(s) colors for the even-numbered rows
|
||||
}
|
||||
|
||||
var (
|
||||
// ColorOptionsDefault defines sensible ANSI color options - basically NONE.
|
||||
ColorOptionsDefault = ColorOptions{}
|
||||
|
||||
// ColorOptionsBright renders dark text on bright background.
|
||||
ColorOptionsBright = ColorOptionsBlackOnCyanWhite
|
||||
|
||||
// ColorOptionsDark renders bright text on dark background.
|
||||
ColorOptionsDark = ColorOptionsCyanWhiteOnBlack
|
||||
|
||||
// ColorOptionsBlackOnBlueWhite renders Black text on Blue/White background.
|
||||
ColorOptionsBlackOnBlueWhite = ColorOptions{
|
||||
IndexColumn: text.Colors{text.BgHiBlue, text.FgBlack},
|
||||
Footer: text.Colors{text.BgBlue, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiBlue, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlackOnCyanWhite renders Black text on Cyan/White background.
|
||||
ColorOptionsBlackOnCyanWhite = ColorOptions{
|
||||
IndexColumn: text.Colors{text.BgHiCyan, text.FgBlack},
|
||||
Footer: text.Colors{text.BgCyan, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiCyan, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlackOnGreenWhite renders Black text on Green/White
|
||||
// background.
|
||||
ColorOptionsBlackOnGreenWhite = ColorOptions{
|
||||
IndexColumn: text.Colors{text.BgHiGreen, text.FgBlack},
|
||||
Footer: text.Colors{text.BgGreen, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiGreen, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlackOnMagentaWhite renders Black text on Magenta/White
|
||||
// background.
|
||||
ColorOptionsBlackOnMagentaWhite = ColorOptions{
|
||||
IndexColumn: text.Colors{text.BgHiMagenta, text.FgBlack},
|
||||
Footer: text.Colors{text.BgMagenta, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiMagenta, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlackOnRedWhite renders Black text on Red/White background.
|
||||
ColorOptionsBlackOnRedWhite = ColorOptions{
|
||||
IndexColumn: text.Colors{text.BgHiRed, text.FgBlack},
|
||||
Footer: text.Colors{text.BgRed, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiRed, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlackOnYellowWhite renders Black text on Yellow/White
|
||||
// background.
|
||||
ColorOptionsBlackOnYellowWhite = ColorOptions{
|
||||
IndexColumn: text.Colors{text.BgHiYellow, text.FgBlack},
|
||||
Footer: text.Colors{text.BgYellow, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiYellow, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlueWhiteOnBlack renders Blue/White text on Black background.
|
||||
ColorOptionsBlueWhiteOnBlack = ColorOptions{
|
||||
IndexColumn: text.Colors{text.FgHiBlue, text.BgHiBlack},
|
||||
Footer: text.Colors{text.FgBlue, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiBlue, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsCyanWhiteOnBlack renders Cyan/White text on Black background.
|
||||
ColorOptionsCyanWhiteOnBlack = ColorOptions{
|
||||
IndexColumn: text.Colors{text.FgHiCyan, text.BgHiBlack},
|
||||
Footer: text.Colors{text.FgCyan, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiCyan, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsGreenWhiteOnBlack renders Green/White text on Black
|
||||
// background.
|
||||
ColorOptionsGreenWhiteOnBlack = ColorOptions{
|
||||
IndexColumn: text.Colors{text.FgHiGreen, text.BgHiBlack},
|
||||
Footer: text.Colors{text.FgGreen, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiGreen, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsMagentaWhiteOnBlack renders Magenta/White text on Black
|
||||
// background.
|
||||
ColorOptionsMagentaWhiteOnBlack = ColorOptions{
|
||||
IndexColumn: text.Colors{text.FgHiMagenta, text.BgHiBlack},
|
||||
Footer: text.Colors{text.FgMagenta, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiMagenta, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsRedWhiteOnBlack renders Red/White text on Black background.
|
||||
ColorOptionsRedWhiteOnBlack = ColorOptions{
|
||||
IndexColumn: text.Colors{text.FgHiRed, text.BgHiBlack},
|
||||
Footer: text.Colors{text.FgRed, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiRed, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsYellowWhiteOnBlack renders Yellow/White text on Black
|
||||
// background.
|
||||
ColorOptionsYellowWhiteOnBlack = ColorOptions{
|
||||
IndexColumn: text.Colors{text.FgHiYellow, text.BgHiBlack},
|
||||
Footer: text.Colors{text.FgYellow, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiYellow, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
)
|
||||
|
||||
// FormatOptions defines the text-formatting to perform on parts of the Table.
|
||||
type FormatOptions struct {
|
||||
Footer text.Format // footer row(s) text format
|
||||
Header text.Format // header row(s) text format
|
||||
Row text.Format // (data) row(s) text format
|
||||
}
|
||||
|
||||
var (
|
||||
// FormatOptionsDefault defines sensible formatting options.
|
||||
FormatOptionsDefault = FormatOptions{
|
||||
Footer: text.FormatUpper,
|
||||
Header: text.FormatUpper,
|
||||
Row: text.FormatDefault,
|
||||
}
|
||||
)
|
||||
|
||||
// HTMLOptions defines the global options to control HTML rendering.
|
||||
type HTMLOptions struct {
|
||||
CSSClass string // CSS class to set on the overall <table> tag
|
||||
EmptyColumn string // string to replace "" columns with (entire content being "")
|
||||
EscapeText bool // escape text into HTML-safe content?
|
||||
Newline string // string to replace "\n" characters with
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultHTMLOptions defines sensible HTML rendering defaults.
|
||||
DefaultHTMLOptions = HTMLOptions{
|
||||
CSSClass: DefaultHTMLCSSClass,
|
||||
EmptyColumn: " ",
|
||||
EscapeText: true,
|
||||
Newline: "<br/>",
|
||||
}
|
||||
)
|
||||
|
||||
// Options defines the global options that determine how the Table is
|
||||
// rendered.
|
||||
type Options struct {
|
||||
// DrawBorder enables or disables drawing the border around the Table.
|
||||
// Example of a table where it is disabled:
|
||||
// # │ FIRST NAME │ LAST NAME │ SALARY │
|
||||
// ─────┼────────────┼───────────┼────────┼─────────────────────────────
|
||||
// 1 │ Arya │ Stark │ 3000 │
|
||||
// 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow!
|
||||
// 300 │ Tyrion │ Lannister │ 5000 │
|
||||
// ─────┼────────────┼───────────┼────────┼─────────────────────────────
|
||||
// │ │ TOTAL │ 10000 │
|
||||
DrawBorder bool
|
||||
|
||||
// SeparateColumns enables or disable drawing border between columns.
|
||||
// Example of a table where it is disabled:
|
||||
// ┌─────────────────────────────────────────────────────────────────┐
|
||||
// │ # FIRST NAME LAST NAME SALARY │
|
||||
// ├─────────────────────────────────────────────────────────────────┤
|
||||
// │ 1 Arya Stark 3000 │
|
||||
// │ 20 Jon Snow 2000 You know nothing, Jon Snow! │
|
||||
// │ 300 Tyrion Lannister 5000 │
|
||||
// │ TOTAL 10000 │
|
||||
// └─────────────────────────────────────────────────────────────────┘
|
||||
SeparateColumns bool
|
||||
|
||||
// SeparateFooter enables or disable drawing border between the footer and
|
||||
// the rows. Example of a table where it is disabled:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
SeparateFooter bool
|
||||
|
||||
// SeparateHeader enables or disable drawing border between the header and
|
||||
// the rows. Example of a table where it is disabled:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
SeparateHeader bool
|
||||
|
||||
// SeparateRows enables or disables drawing separators between each row.
|
||||
// Example of a table where it is enabled:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
SeparateRows bool
|
||||
}
|
||||
|
||||
var (
|
||||
// OptionsDefault defines sensible global options.
|
||||
OptionsDefault = Options{
|
||||
DrawBorder: true,
|
||||
SeparateColumns: true,
|
||||
SeparateFooter: true,
|
||||
SeparateHeader: true,
|
||||
SeparateRows: false,
|
||||
}
|
||||
|
||||
// OptionsNoBorders sets up a table without any borders.
|
||||
OptionsNoBorders = Options{
|
||||
DrawBorder: false,
|
||||
SeparateColumns: true,
|
||||
SeparateFooter: true,
|
||||
SeparateHeader: true,
|
||||
SeparateRows: false,
|
||||
}
|
||||
|
||||
// OptionsNoBordersAndSeparators sets up a table without any borders or
|
||||
// separators.
|
||||
OptionsNoBordersAndSeparators = Options{
|
||||
DrawBorder: false,
|
||||
SeparateColumns: false,
|
||||
SeparateFooter: false,
|
||||
SeparateHeader: false,
|
||||
SeparateRows: false,
|
||||
}
|
||||
)
|
||||
|
||||
// TitleOptions defines the way the title text is to be rendered.
|
||||
type TitleOptions struct {
|
||||
Align text.Align
|
||||
Colors text.Colors
|
||||
Format text.Format
|
||||
}
|
||||
|
||||
var (
|
||||
// TitleOptionsDefault defines sensible title options - basically NONE.
|
||||
TitleOptionsDefault = TitleOptions{}
|
||||
|
||||
// TitleOptionsBright renders Bright Bold text on Dark background.
|
||||
TitleOptionsBright = TitleOptionsBlackOnCyan
|
||||
|
||||
// TitleOptionsDark renders Dark Bold text on Bright background.
|
||||
TitleOptionsDark = TitleOptionsCyanOnBlack
|
||||
|
||||
// TitleOptionsBlackOnBlue renders Black text on Blue background.
|
||||
TitleOptionsBlackOnBlue = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnBlueWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlackOnCyan renders Black Bold text on Cyan background.
|
||||
TitleOptionsBlackOnCyan = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnCyanWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlackOnGreen renders Black Bold text onGreen background.
|
||||
TitleOptionsBlackOnGreen = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnGreenWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlackOnMagenta renders Black Bold text on Magenta background.
|
||||
TitleOptionsBlackOnMagenta = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnMagentaWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlackOnRed renders Black Bold text on Red background.
|
||||
TitleOptionsBlackOnRed = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnRedWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlackOnYellow renders Black Bold text on Yellow background.
|
||||
TitleOptionsBlackOnYellow = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnYellowWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlueOnBlack renders Blue Bold text on Black background.
|
||||
TitleOptionsBlueOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsBlueWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsCyanOnBlack renders Cyan Bold text on Black background.
|
||||
TitleOptionsCyanOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsCyanWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsGreenOnBlack renders Green Bold text on Black background.
|
||||
TitleOptionsGreenOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsGreenWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsMagentaOnBlack renders Magenta Bold text on Black background.
|
||||
TitleOptionsMagentaOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsMagentaWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsRedOnBlack renders Red Bold text on Black background.
|
||||
TitleOptionsRedOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsRedWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsYellowOnBlack renders Yellow Bold text on Black background.
|
||||
TitleOptionsYellowOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsYellowWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
)
|
||||
+923
@@ -0,0 +1,923 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
)
|
||||
|
||||
// Row defines a single row in the Table.
|
||||
type Row []interface{}
|
||||
|
||||
// RowPainter is a custom function that takes a Row as input and returns the
|
||||
// text.Colors{} to use on the entire row
|
||||
type RowPainter func(row Row) text.Colors
|
||||
|
||||
// rowStr defines a single row in the Table comprised of just string objects.
|
||||
type rowStr []string
|
||||
|
||||
// areEqual returns true if the contents of the 2 given columns are the same
|
||||
func (row rowStr) areEqual(colIdx1 int, colIdx2 int) bool {
|
||||
return colIdx1 >= 0 && colIdx2 < len(row) && row[colIdx1] == row[colIdx2]
|
||||
}
|
||||
|
||||
// Table helps print a 2-dimensional array in a human readable pretty-table.
|
||||
type Table struct {
|
||||
// allowedRowLength is the max allowed length for a row (or line of output)
|
||||
allowedRowLength int
|
||||
// enable automatic indexing of the rows and columns like a spreadsheet?
|
||||
autoIndex bool
|
||||
// autoIndexVIndexMaxLength denotes the length in chars for the last rownum
|
||||
autoIndexVIndexMaxLength int
|
||||
// caption stores the text to be rendered just below the table; and doesn't
|
||||
// get used when rendered as a CSV
|
||||
caption string
|
||||
// columnIsNonNumeric stores if a column contains non-numbers in all rows
|
||||
columnIsNonNumeric []bool
|
||||
// columnConfigs stores the custom-configuration for 1 or more columns
|
||||
columnConfigs []ColumnConfig
|
||||
// columnConfigMap stores the custom-configuration by column
|
||||
// number and is generated before rendering
|
||||
columnConfigMap map[int]ColumnConfig
|
||||
// htmlCSSClass stores the HTML CSS Class to use on the <table> node
|
||||
htmlCSSClass string
|
||||
// indexColumn stores the number of the column considered as the "index"
|
||||
indexColumn int
|
||||
// maxColumnLengths stores the length of the longest line in each column
|
||||
maxColumnLengths []int
|
||||
// maxRowLength stores the length of the longest row
|
||||
maxRowLength int
|
||||
// numColumns stores the (max.) number of columns seen
|
||||
numColumns int
|
||||
// numLinesRendered keeps track of the number of lines rendered and helps in
|
||||
// paginating long tables
|
||||
numLinesRendered int
|
||||
// outputMirror stores an io.Writer where the "Render" functions would write
|
||||
outputMirror io.Writer
|
||||
// pageSize stores the maximum lines to render before rendering the header
|
||||
// again (to denote a page break) - useful when you are dealing with really
|
||||
// long tables
|
||||
pageSize int
|
||||
// rows stores the rows that make up the body (in string form)
|
||||
rows []rowStr
|
||||
// rowsColors stores the text.Colors over-rides for each row as defined by
|
||||
// rowPainter
|
||||
rowsColors []text.Colors
|
||||
// rowsConfigs stores RowConfig for each row
|
||||
rowsConfigMap map[int]RowConfig
|
||||
// rowsRaw stores the rows that make up the body
|
||||
rowsRaw []Row
|
||||
// rowsFooter stores the rows that make up the footer (in string form)
|
||||
rowsFooter []rowStr
|
||||
// rowsFooterConfigs stores RowConfig for each footer row
|
||||
rowsFooterConfigMap map[int]RowConfig
|
||||
// rowsFooterRaw stores the rows that make up the footer
|
||||
rowsFooterRaw []Row
|
||||
// rowsHeader stores the rows that make up the header (in string form)
|
||||
rowsHeader []rowStr
|
||||
// rowsHeaderConfigs stores RowConfig for each header row
|
||||
rowsHeaderConfigMap map[int]RowConfig
|
||||
// rowsHeaderRaw stores the rows that make up the header
|
||||
rowsHeaderRaw []Row
|
||||
// rowPainter is a custom function that given a Row, returns the colors to
|
||||
// use on the entire row
|
||||
rowPainter RowPainter
|
||||
// rowSeparator is a dummy row that contains the separator columns (dashes
|
||||
// that make up the separator between header/body/footer
|
||||
rowSeparator rowStr
|
||||
// separators is used to keep track of all rowIndices after which a
|
||||
// separator has to be rendered
|
||||
separators map[int]bool
|
||||
// sortBy stores a map of Column
|
||||
sortBy []SortBy
|
||||
// style contains all the strings used to draw the table, and more
|
||||
style *Style
|
||||
// suppressEmptyColumns hides columns which have no content on all regular
|
||||
// rows
|
||||
suppressEmptyColumns bool
|
||||
// title contains the text to appear above the table
|
||||
title string
|
||||
}
|
||||
|
||||
// AppendFooter appends the row to the List of footers to render.
|
||||
//
|
||||
// Only the first item in the "config" will be tagged against this row.
|
||||
func (t *Table) AppendFooter(row Row, config ...RowConfig) {
|
||||
t.rowsFooterRaw = append(t.rowsFooterRaw, row)
|
||||
if len(config) > 0 {
|
||||
if t.rowsFooterConfigMap == nil {
|
||||
t.rowsFooterConfigMap = make(map[int]RowConfig)
|
||||
}
|
||||
t.rowsFooterConfigMap[len(t.rowsFooterRaw)-1] = config[0]
|
||||
}
|
||||
}
|
||||
|
||||
// AppendHeader appends the row to the List of headers to render.
|
||||
//
|
||||
// Only the first item in the "config" will be tagged against this row.
|
||||
func (t *Table) AppendHeader(row Row, config ...RowConfig) {
|
||||
t.rowsHeaderRaw = append(t.rowsHeaderRaw, row)
|
||||
if len(config) > 0 {
|
||||
if t.rowsHeaderConfigMap == nil {
|
||||
t.rowsHeaderConfigMap = make(map[int]RowConfig)
|
||||
}
|
||||
t.rowsHeaderConfigMap[len(t.rowsHeaderRaw)-1] = config[0]
|
||||
}
|
||||
}
|
||||
|
||||
// AppendRow appends the row to the List of rows to render.
|
||||
//
|
||||
// Only the first item in the "config" will be tagged against this row.
|
||||
func (t *Table) AppendRow(row Row, config ...RowConfig) {
|
||||
t.rowsRaw = append(t.rowsRaw, row)
|
||||
if len(config) > 0 {
|
||||
if t.rowsConfigMap == nil {
|
||||
t.rowsConfigMap = make(map[int]RowConfig)
|
||||
}
|
||||
t.rowsConfigMap[len(t.rowsRaw)-1] = config[0]
|
||||
}
|
||||
}
|
||||
|
||||
// AppendRows appends the rows to the List of rows to render.
|
||||
//
|
||||
// Only the first item in the "config" will be tagged against all the rows.
|
||||
func (t *Table) AppendRows(rows []Row, config ...RowConfig) {
|
||||
for _, row := range rows {
|
||||
t.AppendRow(row, config...)
|
||||
}
|
||||
}
|
||||
|
||||
// AppendSeparator helps render a separator row after the current last row. You
|
||||
// could call this function over and over, but it will be a no-op unless you
|
||||
// call AppendRow or AppendRows in between. Likewise, if the last thing you
|
||||
// append is a separator, it will not be rendered in addition to the usual table
|
||||
// separator.
|
||||
//
|
||||
//******************************************************************************
|
||||
// Please note the following caveats:
|
||||
// 1. SetPageSize(): this may end up creating consecutive separator rows near
|
||||
// the end of a page or at the beginning of a page
|
||||
// 2. SortBy(): since SortBy could inherently alter the ordering of rows, the
|
||||
// separators may not appear after the row it was originally intended to
|
||||
// follow
|
||||
//******************************************************************************
|
||||
func (t *Table) AppendSeparator() {
|
||||
if t.separators == nil {
|
||||
t.separators = make(map[int]bool)
|
||||
}
|
||||
if len(t.rowsRaw) > 0 {
|
||||
t.separators[len(t.rowsRaw)-1] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Length returns the number of rows to be rendered.
|
||||
func (t *Table) Length() int {
|
||||
return len(t.rowsRaw)
|
||||
}
|
||||
|
||||
// ResetFooters resets and clears all the Footer rows appended earlier.
|
||||
func (t *Table) ResetFooters() {
|
||||
t.rowsFooterRaw = nil
|
||||
}
|
||||
|
||||
// ResetHeaders resets and clears all the Header rows appended earlier.
|
||||
func (t *Table) ResetHeaders() {
|
||||
t.rowsHeaderRaw = nil
|
||||
}
|
||||
|
||||
// ResetRows resets and clears all the rows appended earlier.
|
||||
func (t *Table) ResetRows() {
|
||||
t.rowsRaw = nil
|
||||
t.separators = nil
|
||||
}
|
||||
|
||||
// SetAllowedRowLength sets the maximum allowed length or a row (or line of
|
||||
// output) when rendered as a table. Rows that are longer than this limit will
|
||||
// be "snipped" to the length. Length has to be a positive value to take effect.
|
||||
func (t *Table) SetAllowedRowLength(length int) {
|
||||
t.allowedRowLength = length
|
||||
}
|
||||
|
||||
// SetAutoIndex adds a generated header with columns such as "A", "B", "C", etc.
|
||||
// and a leading column with the row number similar to what you'd see on any
|
||||
// spreadsheet application. NOTE: Appending a Header will void this
|
||||
// functionality.
|
||||
func (t *Table) SetAutoIndex(autoIndex bool) {
|
||||
t.autoIndex = autoIndex
|
||||
}
|
||||
|
||||
// SetCaption sets the text to be rendered just below the table. This will not
|
||||
// show up when the Table is rendered as a CSV.
|
||||
func (t *Table) SetCaption(format string, a ...interface{}) {
|
||||
t.caption = fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
// SetColumnConfigs sets the configs for each Column.
|
||||
func (t *Table) SetColumnConfigs(configs []ColumnConfig) {
|
||||
t.columnConfigs = configs
|
||||
}
|
||||
|
||||
// SetHTMLCSSClass sets the the HTML CSS Class to use on the <table> node
|
||||
// when rendering the Table in HTML format.
|
||||
//
|
||||
// Deprecated: in favor of Style().HTML.CSSClass
|
||||
func (t *Table) SetHTMLCSSClass(cssClass string) {
|
||||
t.htmlCSSClass = cssClass
|
||||
}
|
||||
|
||||
// SetIndexColumn sets the given Column # as the column that has the row
|
||||
// "Number". Valid values range from 1 to N. Note that this is not 0-indexed.
|
||||
func (t *Table) SetIndexColumn(colNum int) {
|
||||
t.indexColumn = colNum
|
||||
}
|
||||
|
||||
// SetOutputMirror sets an io.Writer for all the Render functions to "Write" to
|
||||
// in addition to returning a string.
|
||||
func (t *Table) SetOutputMirror(mirror io.Writer) {
|
||||
t.outputMirror = mirror
|
||||
}
|
||||
|
||||
// SetPageSize sets the maximum number of lines to render before rendering the
|
||||
// header rows again. This can be useful when dealing with tables containing a
|
||||
// long list of rows that can span pages. Please note that the pagination logic
|
||||
// will not consider Header/Footer lines for paging.
|
||||
func (t *Table) SetPageSize(numLines int) {
|
||||
t.pageSize = numLines
|
||||
}
|
||||
|
||||
// SetRowPainter sets the RowPainter function which determines the colors to use
|
||||
// on a row. Before rendering, this function is invoked on all rows and the
|
||||
// color of each row is determined. This color takes precedence over other ways
|
||||
// to set color (ColumnConfig.Color*, SetColor*()).
|
||||
func (t *Table) SetRowPainter(painter RowPainter) {
|
||||
t.rowPainter = painter
|
||||
}
|
||||
|
||||
// SetStyle overrides the DefaultStyle with the provided one.
|
||||
func (t *Table) SetStyle(style Style) {
|
||||
t.style = &style
|
||||
}
|
||||
|
||||
// SetTitle sets the title text to be rendered above the table.
|
||||
func (t *Table) SetTitle(format string, a ...interface{}) {
|
||||
t.title = fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
// SortBy sets the rules for sorting the Rows in the order specified. i.e., the
|
||||
// first SortBy instruction takes precedence over the second and so on. Any
|
||||
// duplicate instructions on the same column will be discarded while sorting.
|
||||
func (t *Table) SortBy(sortBy []SortBy) {
|
||||
t.sortBy = sortBy
|
||||
}
|
||||
|
||||
// Style returns the current style.
|
||||
func (t *Table) Style() *Style {
|
||||
if t.style == nil {
|
||||
tempStyle := StyleDefault
|
||||
t.style = &tempStyle
|
||||
}
|
||||
return t.style
|
||||
}
|
||||
|
||||
// SuppressEmptyColumns hides columns when the column is empty in ALL the
|
||||
// regular rows.
|
||||
func (t *Table) SuppressEmptyColumns() {
|
||||
t.suppressEmptyColumns = true
|
||||
}
|
||||
|
||||
func (t *Table) analyzeAndStringify(row Row, hint renderHint) rowStr {
|
||||
// update t.numColumns if this row is the longest seen till now
|
||||
if len(row) > t.numColumns {
|
||||
// init the slice for the first time; and pad it the rest of the time
|
||||
if t.numColumns == 0 {
|
||||
t.columnIsNonNumeric = make([]bool, len(row))
|
||||
} else {
|
||||
t.columnIsNonNumeric = append(t.columnIsNonNumeric, make([]bool, len(row)-t.numColumns)...)
|
||||
}
|
||||
// update t.numColumns
|
||||
t.numColumns = len(row)
|
||||
}
|
||||
|
||||
// convert each column to string and figure out if it has non-numeric data
|
||||
rowOut := make(rowStr, len(row))
|
||||
for colIdx, col := range row {
|
||||
// if the column is not a number, keep track of it
|
||||
if !hint.isHeaderRow && !hint.isFooterRow && !t.columnIsNonNumeric[colIdx] && !isNumber(col) {
|
||||
t.columnIsNonNumeric[colIdx] = true
|
||||
}
|
||||
|
||||
// convert to a string and store it in the row
|
||||
var colStr string
|
||||
if transformer := t.getColumnTransformer(colIdx, hint); transformer != nil {
|
||||
colStr = transformer(col)
|
||||
} else if colStrVal, ok := col.(string); ok {
|
||||
colStr = colStrVal
|
||||
} else {
|
||||
colStr = fmt.Sprint(col)
|
||||
}
|
||||
if strings.Contains(colStr, "\t") {
|
||||
colStr = strings.Replace(colStr, "\t", " ", -1)
|
||||
}
|
||||
if strings.Contains(colStr, "\r") {
|
||||
colStr = strings.Replace(colStr, "\r", "", -1)
|
||||
}
|
||||
rowOut[colIdx] = colStr
|
||||
}
|
||||
return rowOut
|
||||
}
|
||||
|
||||
func (t *Table) getAlign(colIdx int, hint renderHint) text.Align {
|
||||
align := text.AlignDefault
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
if hint.isHeaderRow {
|
||||
align = cfg.AlignHeader
|
||||
} else if hint.isFooterRow {
|
||||
align = cfg.AlignFooter
|
||||
} else {
|
||||
align = cfg.Align
|
||||
}
|
||||
}
|
||||
if align == text.AlignDefault {
|
||||
if !t.columnIsNonNumeric[colIdx] {
|
||||
align = text.AlignRight
|
||||
} else if hint.isAutoIndexRow {
|
||||
align = text.AlignCenter
|
||||
}
|
||||
}
|
||||
return align
|
||||
}
|
||||
|
||||
func (t *Table) getAutoIndexColumnIDs() rowStr {
|
||||
row := make(rowStr, t.numColumns)
|
||||
for colIdx := range row {
|
||||
row[colIdx] = AutoIndexColumnID(colIdx)
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func (t *Table) getBorderColors(hint renderHint) text.Colors {
|
||||
if hint.isFooterRow {
|
||||
return t.style.Color.Footer
|
||||
} else if t.autoIndex {
|
||||
return t.style.Color.IndexColumn
|
||||
}
|
||||
return t.style.Color.Header
|
||||
}
|
||||
|
||||
func (t *Table) getColumnColors(colIdx int, hint renderHint) text.Colors {
|
||||
if t.rowPainter != nil && hint.isRegularRow() && !t.isIndexColumn(colIdx, hint) {
|
||||
colors := t.rowsColors[hint.rowNumber-1]
|
||||
if colors != nil {
|
||||
return colors
|
||||
}
|
||||
}
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
if hint.isSeparatorRow {
|
||||
return nil
|
||||
} else if hint.isHeaderRow {
|
||||
return cfg.ColorsHeader
|
||||
} else if hint.isFooterRow {
|
||||
return cfg.ColorsFooter
|
||||
}
|
||||
return cfg.Colors
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Table) getColumnSeparator(row rowStr, colIdx int, hint renderHint) string {
|
||||
separator := t.style.Box.MiddleVertical
|
||||
if hint.isSeparatorRow {
|
||||
if hint.isBorderTop {
|
||||
if t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint) {
|
||||
separator = t.style.Box.MiddleHorizontal
|
||||
} else {
|
||||
separator = t.style.Box.TopSeparator
|
||||
}
|
||||
} else if hint.isBorderBottom {
|
||||
if t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint) {
|
||||
separator = t.style.Box.MiddleHorizontal
|
||||
} else {
|
||||
separator = t.style.Box.BottomSeparator
|
||||
}
|
||||
} else {
|
||||
separator = t.getColumnSeparatorNonBorder(
|
||||
t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint),
|
||||
t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint),
|
||||
colIdx,
|
||||
hint,
|
||||
)
|
||||
}
|
||||
}
|
||||
return separator
|
||||
}
|
||||
|
||||
func (t *Table) getColumnSeparatorNonBorder(mergeCellsAbove bool, mergeCellsBelow bool, colIdx int, hint renderHint) string {
|
||||
mergeNextCol := t.shouldMergeCellsVertically(colIdx, hint)
|
||||
if hint.isAutoIndexColumn {
|
||||
return t.getColumnSeparatorNonBorderAutoIndex(mergeNextCol, hint)
|
||||
}
|
||||
|
||||
mergeCurrCol := t.shouldMergeCellsVertically(colIdx-1, hint)
|
||||
return t.getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove, mergeCellsBelow, mergeCurrCol, mergeNextCol)
|
||||
}
|
||||
|
||||
func (t *Table) getColumnSeparatorNonBorderAutoIndex(mergeNextCol bool, hint renderHint) string {
|
||||
if hint.isHeaderOrFooterSeparator() {
|
||||
if mergeNextCol {
|
||||
return t.style.Box.MiddleVertical
|
||||
}
|
||||
return t.style.Box.LeftSeparator
|
||||
} else if mergeNextCol {
|
||||
return t.style.Box.RightSeparator
|
||||
}
|
||||
return t.style.Box.MiddleSeparator
|
||||
}
|
||||
|
||||
func (t *Table) getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove bool, mergeCellsBelow bool, mergeCurrCol bool, mergeNextCol bool) string {
|
||||
if mergeCellsAbove && mergeCellsBelow && mergeCurrCol && mergeNextCol {
|
||||
return t.style.Box.EmptySeparator
|
||||
} else if mergeCellsAbove && mergeCellsBelow {
|
||||
return t.style.Box.MiddleHorizontal
|
||||
} else if mergeCellsAbove {
|
||||
return t.style.Box.TopSeparator
|
||||
} else if mergeCellsBelow {
|
||||
return t.style.Box.BottomSeparator
|
||||
} else if mergeCurrCol && mergeNextCol {
|
||||
return t.style.Box.MiddleVertical
|
||||
} else if mergeCurrCol {
|
||||
return t.style.Box.LeftSeparator
|
||||
}
|
||||
return t.style.Box.MiddleSeparator
|
||||
}
|
||||
|
||||
func (t *Table) getColumnTransformer(colIdx int, hint renderHint) text.Transformer {
|
||||
var transformer text.Transformer
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
if hint.isHeaderRow {
|
||||
transformer = cfg.TransformerHeader
|
||||
} else if hint.isFooterRow {
|
||||
transformer = cfg.TransformerFooter
|
||||
} else {
|
||||
transformer = cfg.Transformer
|
||||
}
|
||||
}
|
||||
return transformer
|
||||
}
|
||||
|
||||
func (t *Table) getColumnWidthMax(colIdx int) int {
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
return cfg.WidthMax
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t *Table) getColumnWidthMin(colIdx int) int {
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
return cfg.WidthMin
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t *Table) getFormat(hint renderHint) text.Format {
|
||||
if hint.isSeparatorRow {
|
||||
return text.FormatDefault
|
||||
} else if hint.isHeaderRow {
|
||||
return t.style.Format.Header
|
||||
} else if hint.isFooterRow {
|
||||
return t.style.Format.Footer
|
||||
}
|
||||
return t.style.Format.Row
|
||||
}
|
||||
|
||||
func (t *Table) getRow(rowIdx int, hint renderHint) rowStr {
|
||||
switch {
|
||||
case hint.isHeaderRow:
|
||||
if rowIdx >= 0 && rowIdx < len(t.rowsHeader) {
|
||||
return t.rowsHeader[rowIdx]
|
||||
}
|
||||
case hint.isFooterRow:
|
||||
if rowIdx >= 0 && rowIdx < len(t.rowsFooter) {
|
||||
return t.rowsFooter[rowIdx]
|
||||
}
|
||||
default:
|
||||
if rowIdx >= 0 && rowIdx < len(t.rows) {
|
||||
return t.rows[rowIdx]
|
||||
}
|
||||
}
|
||||
return rowStr{}
|
||||
}
|
||||
|
||||
func (t *Table) getRowConfig(hint renderHint) RowConfig {
|
||||
rowIdx := hint.rowNumber - 1
|
||||
if rowIdx < 0 {
|
||||
rowIdx = 0
|
||||
}
|
||||
|
||||
switch {
|
||||
case hint.isHeaderRow:
|
||||
return t.rowsHeaderConfigMap[rowIdx]
|
||||
case hint.isFooterRow:
|
||||
return t.rowsFooterConfigMap[rowIdx]
|
||||
default:
|
||||
return t.rowsConfigMap[rowIdx]
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) getSeparatorColors(hint renderHint) text.Colors {
|
||||
if hint.isHeaderRow {
|
||||
return t.style.Color.Header
|
||||
} else if hint.isFooterRow {
|
||||
return t.style.Color.Footer
|
||||
} else if hint.isAutoIndexColumn {
|
||||
return t.style.Color.IndexColumn
|
||||
} else if hint.rowNumber > 0 && hint.rowNumber%2 == 0 {
|
||||
return t.style.Color.RowAlternate
|
||||
}
|
||||
return t.style.Color.Row
|
||||
}
|
||||
|
||||
func (t *Table) getVAlign(colIdx int, hint renderHint) text.VAlign {
|
||||
vAlign := text.VAlignDefault
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
if hint.isHeaderRow {
|
||||
vAlign = cfg.VAlignHeader
|
||||
} else if hint.isFooterRow {
|
||||
vAlign = cfg.VAlignFooter
|
||||
} else {
|
||||
vAlign = cfg.VAlign
|
||||
}
|
||||
}
|
||||
return vAlign
|
||||
}
|
||||
|
||||
func (t *Table) initForRender() {
|
||||
// pick a default style if none was set until now
|
||||
t.Style()
|
||||
|
||||
// initialize the column configs and normalize them
|
||||
t.initForRenderColumnConfigs()
|
||||
|
||||
// initialize and stringify all the raw rows
|
||||
t.initForRenderRows()
|
||||
|
||||
// find the longest continuous line in each column
|
||||
t.initForRenderColumnLengths()
|
||||
|
||||
// generate a separator row and calculate maximum row length
|
||||
t.initForRenderRowSeparator()
|
||||
|
||||
// reset the counter for the number of lines rendered
|
||||
t.numLinesRendered = 0
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderColumnConfigs() {
|
||||
findColumnNumber := func(row Row, colName string) int {
|
||||
for colIdx, col := range row {
|
||||
if fmt.Sprint(col) == colName {
|
||||
return colIdx + 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
t.columnConfigMap = map[int]ColumnConfig{}
|
||||
for _, colCfg := range t.columnConfigs {
|
||||
// find the column number if none provided; this logic can work only if
|
||||
// a header row is present and has a column with the given name
|
||||
if colCfg.Number == 0 {
|
||||
for _, row := range t.rowsHeaderRaw {
|
||||
colCfg.Number = findColumnNumber(row, colCfg.Name)
|
||||
if colCfg.Number > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if colCfg.Number > 0 {
|
||||
t.columnConfigMap[colCfg.Number-1] = colCfg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderColumnLengths() {
|
||||
var findMaxColumnLengths = func(rows []rowStr) {
|
||||
for _, row := range rows {
|
||||
for colIdx, colStr := range row {
|
||||
longestLineLen := text.LongestLineLen(colStr)
|
||||
if longestLineLen > t.maxColumnLengths[colIdx] {
|
||||
t.maxColumnLengths[colIdx] = longestLineLen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.maxColumnLengths = make([]int, t.numColumns)
|
||||
findMaxColumnLengths(t.rowsHeader)
|
||||
findMaxColumnLengths(t.rows)
|
||||
findMaxColumnLengths(t.rowsFooter)
|
||||
|
||||
// restrict the column lengths if any are over or under the limits
|
||||
for colIdx := range t.maxColumnLengths {
|
||||
maxWidth := t.getColumnWidthMax(colIdx)
|
||||
if maxWidth > 0 && t.maxColumnLengths[colIdx] > maxWidth {
|
||||
t.maxColumnLengths[colIdx] = maxWidth
|
||||
}
|
||||
minWidth := t.getColumnWidthMin(colIdx)
|
||||
if minWidth > 0 && t.maxColumnLengths[colIdx] < minWidth {
|
||||
t.maxColumnLengths[colIdx] = minWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderHideColumns() {
|
||||
// if there is nothing to hide, return fast
|
||||
hasHiddenColumns := false
|
||||
for _, cc := range t.columnConfigMap {
|
||||
if cc.Hidden {
|
||||
hasHiddenColumns = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasHiddenColumns {
|
||||
return
|
||||
}
|
||||
|
||||
colIdxMap := make(map[int]int)
|
||||
numColumns := 0
|
||||
_hideColumns := func(rows []rowStr) []rowStr {
|
||||
var rsp []rowStr
|
||||
for _, row := range rows {
|
||||
var rowNew rowStr
|
||||
for colIdx, col := range row {
|
||||
cc := t.columnConfigMap[colIdx]
|
||||
if !cc.Hidden {
|
||||
rowNew = append(rowNew, col)
|
||||
colIdxMap[colIdx] = len(rowNew) - 1
|
||||
}
|
||||
}
|
||||
if len(rowNew) > numColumns {
|
||||
numColumns = len(rowNew)
|
||||
}
|
||||
rsp = append(rsp, rowNew)
|
||||
}
|
||||
return rsp
|
||||
}
|
||||
|
||||
// hide columns as directed
|
||||
t.rows = _hideColumns(t.rows)
|
||||
t.rowsFooter = _hideColumns(t.rowsFooter)
|
||||
t.rowsHeader = _hideColumns(t.rowsHeader)
|
||||
|
||||
// reset numColumns to the new number of columns
|
||||
t.numColumns = numColumns
|
||||
|
||||
// re-create columnIsNonNumeric with new column indices
|
||||
columnIsNonNumeric := make([]bool, t.numColumns)
|
||||
for oldColIdx, nonNumeric := range t.columnIsNonNumeric {
|
||||
if newColIdx, ok := colIdxMap[oldColIdx]; ok {
|
||||
columnIsNonNumeric[newColIdx] = nonNumeric
|
||||
}
|
||||
}
|
||||
t.columnIsNonNumeric = columnIsNonNumeric
|
||||
|
||||
// re-create columnConfigMap with new column indices
|
||||
columnConfigMap := make(map[int]ColumnConfig)
|
||||
for oldColIdx, cc := range t.columnConfigMap {
|
||||
if newColIdx, ok := colIdxMap[oldColIdx]; ok {
|
||||
columnConfigMap[newColIdx] = cc
|
||||
}
|
||||
}
|
||||
t.columnConfigMap = columnConfigMap
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderRows() {
|
||||
t.reset()
|
||||
|
||||
// auto-index: calc the index column's max length
|
||||
t.autoIndexVIndexMaxLength = len(fmt.Sprint(len(t.rowsRaw)))
|
||||
|
||||
// stringify all the rows to make it easy to render
|
||||
if t.rowPainter != nil {
|
||||
t.rowsColors = make([]text.Colors, len(t.rowsRaw))
|
||||
}
|
||||
t.rows = t.initForRenderRowsStringify(t.rowsRaw, renderHint{})
|
||||
t.rowsFooter = t.initForRenderRowsStringify(t.rowsFooterRaw, renderHint{isFooterRow: true})
|
||||
t.rowsHeader = t.initForRenderRowsStringify(t.rowsHeaderRaw, renderHint{isHeaderRow: true})
|
||||
|
||||
// sort the rows as requested
|
||||
t.initForRenderSortRows()
|
||||
|
||||
// suppress columns without any content
|
||||
t.initForRenderSuppressColumns()
|
||||
|
||||
// strip out hidden columns
|
||||
t.initForRenderHideColumns()
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderRowsStringify(rows []Row, hint renderHint) []rowStr {
|
||||
rowsStr := make([]rowStr, len(rows))
|
||||
for idx, row := range rows {
|
||||
if t.rowPainter != nil && hint.isRegularRow() {
|
||||
t.rowsColors[idx] = t.rowPainter(row)
|
||||
}
|
||||
rowsStr[idx] = t.analyzeAndStringify(row, hint)
|
||||
}
|
||||
return rowsStr
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderRowSeparator() {
|
||||
t.maxRowLength = 0
|
||||
if t.autoIndex {
|
||||
t.maxRowLength += text.RuneCount(t.style.Box.PaddingLeft)
|
||||
t.maxRowLength += len(fmt.Sprint(len(t.rows)))
|
||||
t.maxRowLength += text.RuneCount(t.style.Box.PaddingRight)
|
||||
if t.style.Options.SeparateColumns {
|
||||
t.maxRowLength += text.RuneCount(t.style.Box.MiddleSeparator)
|
||||
}
|
||||
}
|
||||
if t.style.Options.SeparateColumns {
|
||||
t.maxRowLength += text.RuneCount(t.style.Box.MiddleSeparator) * (t.numColumns - 1)
|
||||
}
|
||||
t.rowSeparator = make(rowStr, t.numColumns)
|
||||
for colIdx, maxColumnLength := range t.maxColumnLengths {
|
||||
maxColumnLength += text.RuneCount(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
|
||||
t.maxRowLength += maxColumnLength
|
||||
t.rowSeparator[colIdx] = text.RepeatAndTrim(t.style.Box.MiddleHorizontal, maxColumnLength)
|
||||
}
|
||||
if t.style.Options.DrawBorder {
|
||||
t.maxRowLength += text.RuneCount(t.style.Box.Left + t.style.Box.Right)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderSortRows() {
|
||||
if len(t.sortBy) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// sort the rows
|
||||
sortedRowIndices := t.getSortedRowIndices()
|
||||
sortedRows := make([]rowStr, len(t.rows))
|
||||
for idx := range t.rows {
|
||||
sortedRows[idx] = t.rows[sortedRowIndices[idx]]
|
||||
}
|
||||
t.rows = sortedRows
|
||||
|
||||
// sort the rowsColors
|
||||
if len(t.rowsColors) > 0 {
|
||||
sortedRowsColors := make([]text.Colors, len(t.rows))
|
||||
for idx := range t.rows {
|
||||
sortedRowsColors[idx] = t.rowsColors[sortedRowIndices[idx]]
|
||||
}
|
||||
t.rowsColors = sortedRowsColors
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderSuppressColumns() {
|
||||
shouldSuppressColumn := func(colIdx int) bool {
|
||||
for _, row := range t.rows {
|
||||
if colIdx < len(row) && row[colIdx] != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if t.suppressEmptyColumns {
|
||||
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
|
||||
if shouldSuppressColumn(colIdx) {
|
||||
cc := t.columnConfigMap[colIdx]
|
||||
cc.Hidden = true
|
||||
t.columnConfigMap[colIdx] = cc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) isIndexColumn(colIdx int, hint renderHint) bool {
|
||||
return t.indexColumn == colIdx+1 || hint.isAutoIndexColumn
|
||||
}
|
||||
|
||||
func (t *Table) render(out *strings.Builder) string {
|
||||
outStr := out.String()
|
||||
if t.outputMirror != nil && len(outStr) > 0 {
|
||||
_, _ = t.outputMirror.Write([]byte(outStr))
|
||||
_, _ = t.outputMirror.Write([]byte("\n"))
|
||||
}
|
||||
return outStr
|
||||
}
|
||||
|
||||
func (t *Table) reset() {
|
||||
t.autoIndexVIndexMaxLength = 0
|
||||
t.columnIsNonNumeric = nil
|
||||
t.maxColumnLengths = nil
|
||||
t.maxRowLength = 0
|
||||
t.numColumns = 0
|
||||
t.rowsColors = nil
|
||||
t.rowSeparator = nil
|
||||
t.rows = nil
|
||||
t.rowsFooter = nil
|
||||
t.rowsHeader = nil
|
||||
}
|
||||
|
||||
func (t *Table) shouldMergeCellsHorizontallyAbove(row rowStr, colIdx int, hint renderHint) bool {
|
||||
if hint.isAutoIndexColumn || hint.isAutoIndexRow {
|
||||
return false
|
||||
}
|
||||
|
||||
rowConfig := t.getRowConfig(hint)
|
||||
if hint.isSeparatorRow {
|
||||
if hint.isHeaderRow && hint.rowNumber == 1 {
|
||||
rowConfig = t.getRowConfig(hint)
|
||||
row = t.getRow(hint.rowNumber-1, hint)
|
||||
} else if hint.isFooterRow && hint.isFirstRow {
|
||||
rowConfig = t.getRowConfig(renderHint{isLastRow: true, rowNumber: len(t.rows)})
|
||||
row = t.getRow(len(t.rows)-1, renderHint{})
|
||||
} else if hint.isFooterRow && hint.isBorderBottom {
|
||||
row = t.getRow(len(t.rowsFooter)-1, renderHint{isFooterRow: true})
|
||||
} else {
|
||||
row = t.getRow(hint.rowNumber-1, hint)
|
||||
}
|
||||
}
|
||||
|
||||
if rowConfig.AutoMerge {
|
||||
return row.areEqual(colIdx-1, colIdx)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Table) shouldMergeCellsHorizontallyBelow(row rowStr, colIdx int, hint renderHint) bool {
|
||||
if hint.isAutoIndexColumn || hint.isAutoIndexRow {
|
||||
return false
|
||||
}
|
||||
|
||||
var rowConfig RowConfig
|
||||
if hint.isSeparatorRow {
|
||||
if hint.isHeaderRow && hint.rowNumber == 0 {
|
||||
rowConfig = t.getRowConfig(renderHint{isHeaderRow: true, rowNumber: 1})
|
||||
row = t.getRow(0, hint)
|
||||
} else if hint.isHeaderRow && hint.isLastRow {
|
||||
rowConfig = t.getRowConfig(renderHint{rowNumber: 1})
|
||||
row = t.getRow(0, renderHint{})
|
||||
} else if hint.isFooterRow && hint.rowNumber >= 0 {
|
||||
rowConfig = t.getRowConfig(renderHint{isFooterRow: true, rowNumber: 1})
|
||||
row = t.getRow(hint.rowNumber, renderHint{isFooterRow: true})
|
||||
} else if hint.isRegularRow() {
|
||||
rowConfig = t.getRowConfig(renderHint{rowNumber: hint.rowNumber + 1})
|
||||
row = t.getRow(hint.rowNumber, renderHint{})
|
||||
}
|
||||
}
|
||||
|
||||
if rowConfig.AutoMerge {
|
||||
return row.areEqual(colIdx-1, colIdx)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Table) shouldMergeCellsVertically(colIdx int, hint renderHint) bool {
|
||||
if t.columnConfigMap[colIdx].AutoMerge && colIdx < t.numColumns {
|
||||
if hint.isSeparatorRow {
|
||||
rowPrev := t.getRow(hint.rowNumber-1, hint)
|
||||
rowNext := t.getRow(hint.rowNumber, hint)
|
||||
if colIdx < len(rowPrev) && colIdx < len(rowNext) {
|
||||
return rowPrev[colIdx] == rowNext[colIdx] || "" == rowNext[colIdx]
|
||||
}
|
||||
} else {
|
||||
rowPrev := t.getRow(hint.rowNumber-2, hint)
|
||||
rowCurr := t.getRow(hint.rowNumber-1, hint)
|
||||
if colIdx < len(rowPrev) && colIdx < len(rowCurr) {
|
||||
return rowPrev[colIdx] == rowCurr[colIdx] || "" == rowCurr[colIdx]
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// renderHint has hints for the Render*() logic
|
||||
type renderHint struct {
|
||||
isAutoIndexColumn bool // auto-index column?
|
||||
isAutoIndexRow bool // auto-index row?
|
||||
isBorderBottom bool // bottom-border?
|
||||
isBorderTop bool // top-border?
|
||||
isFirstRow bool // first-row of header/footer/regular-rows?
|
||||
isFooterRow bool // footer row?
|
||||
isHeaderRow bool // header row?
|
||||
isLastLineOfRow bool // last-line of the current row?
|
||||
isLastRow bool // last-row of header/footer/regular-rows?
|
||||
isSeparatorRow bool // separator row?
|
||||
rowLineNumber int // the line number for a multi-line row
|
||||
rowNumber int // the row number/index
|
||||
}
|
||||
|
||||
func (h *renderHint) isRegularRow() bool {
|
||||
return !h.isHeaderRow && !h.isFooterRow
|
||||
}
|
||||
|
||||
func (h *renderHint) isHeaderOrFooterSeparator() bool {
|
||||
return h.isSeparatorRow && !h.isBorderBottom && !h.isBorderTop &&
|
||||
((h.isHeaderRow && !h.isLastRow) || (h.isFooterRow && (!h.isFirstRow || h.rowNumber > 0)))
|
||||
}
|
||||
|
||||
func (h *renderHint) isLastLineOfLastRow() bool {
|
||||
return h.isLastLineOfRow && h.isLastRow
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// AutoIndexColumnID returns a unique Column ID/Name for the given Column Number.
|
||||
// The functionality is similar to what you get in an Excel spreadsheet w.r.t.
|
||||
// the Column ID/Name.
|
||||
func AutoIndexColumnID(colIdx int) string {
|
||||
charIdx := colIdx % 26
|
||||
out := string(rune(65 + charIdx))
|
||||
colIdx = colIdx / 26
|
||||
if colIdx > 0 {
|
||||
return AutoIndexColumnID(colIdx-1) + out
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// isNumber returns true if the argument is a numeric type; false otherwise.
|
||||
func isNumber(x interface{}) bool {
|
||||
switch reflect.TypeOf(x).Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WidthEnforcer is a function that helps enforce a width condition on a string.
|
||||
type WidthEnforcer func(col string, maxLen int) string
|
||||
|
||||
// widthEnforcerNone returns the input string as is without any modifications.
|
||||
func widthEnforcerNone(col string, maxLen int) string {
|
||||
return col
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// Writer declares the interfaces that can be used to setup and render a table.
|
||||
type Writer interface {
|
||||
AppendFooter(row Row, configs ...RowConfig)
|
||||
AppendHeader(row Row, configs ...RowConfig)
|
||||
AppendRow(row Row, configs ...RowConfig)
|
||||
AppendRows(rows []Row, configs ...RowConfig)
|
||||
AppendSeparator()
|
||||
Length() int
|
||||
Render() string
|
||||
RenderCSV() string
|
||||
RenderHTML() string
|
||||
RenderMarkdown() string
|
||||
ResetFooters()
|
||||
ResetHeaders()
|
||||
ResetRows()
|
||||
SetAllowedRowLength(length int)
|
||||
SetAutoIndex(autoIndex bool)
|
||||
SetCaption(format string, a ...interface{})
|
||||
SetColumnConfigs(configs []ColumnConfig)
|
||||
SetIndexColumn(colNum int)
|
||||
SetOutputMirror(mirror io.Writer)
|
||||
SetPageSize(numLines int)
|
||||
SetRowPainter(painter RowPainter)
|
||||
SetStyle(style Style)
|
||||
SetTitle(format string, a ...interface{})
|
||||
SortBy(sortBy []SortBy)
|
||||
Style() *Style
|
||||
SuppressEmptyColumns()
|
||||
|
||||
// deprecated; in favor of Style().HTML.CSSClass
|
||||
SetHTMLCSSClass(cssClass string)
|
||||
}
|
||||
|
||||
// NewWriter initializes and returns a Writer.
|
||||
func NewWriter() Writer {
|
||||
return &Table{}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Align denotes how text is to be aligned horizontally.
|
||||
type Align int
|
||||
|
||||
// Align enumerations
|
||||
const (
|
||||
AlignDefault Align = iota // same as AlignLeft
|
||||
AlignLeft // "left "
|
||||
AlignCenter // " center "
|
||||
AlignJustify // "justify it"
|
||||
AlignRight // " right"
|
||||
)
|
||||
|
||||
// Apply aligns the text as directed. For ex.:
|
||||
// * AlignDefault.Apply("Jon Snow", 12) returns "Jon Snow "
|
||||
// * AlignLeft.Apply("Jon Snow", 12) returns "Jon Snow "
|
||||
// * AlignCenter.Apply("Jon Snow", 12) returns " Jon Snow "
|
||||
// * AlignJustify.Apply("Jon Snow", 12) returns "Jon Snow"
|
||||
// * AlignRight.Apply("Jon Snow", 12) returns " Jon Snow"
|
||||
func (a Align) Apply(text string, maxLength int) string {
|
||||
text = a.trimString(text)
|
||||
sLen := utf8.RuneCountInString(text)
|
||||
sLenWoE := RuneCount(text)
|
||||
numEscChars := sLen - sLenWoE
|
||||
|
||||
// now, align the text
|
||||
switch a {
|
||||
case AlignDefault, AlignLeft:
|
||||
return fmt.Sprintf("%-"+strconv.Itoa(maxLength+numEscChars)+"s", text)
|
||||
case AlignCenter:
|
||||
if sLenWoE < maxLength {
|
||||
// left pad with half the number of spaces needed before using %text
|
||||
return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s",
|
||||
text+strings.Repeat(" ", int((maxLength-sLenWoE)/2)))
|
||||
}
|
||||
case AlignJustify:
|
||||
return a.justifyText(text, sLenWoE, maxLength)
|
||||
}
|
||||
return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s", text)
|
||||
}
|
||||
|
||||
// HTMLProperty returns the equivalent HTML horizontal-align tag property.
|
||||
func (a Align) HTMLProperty() string {
|
||||
switch a {
|
||||
case AlignLeft:
|
||||
return "align=\"left\""
|
||||
case AlignCenter:
|
||||
return "align=\"center\""
|
||||
case AlignJustify:
|
||||
return "align=\"justify\""
|
||||
case AlignRight:
|
||||
return "align=\"right\""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// MarkdownProperty returns the equivalent Markdown horizontal-align separator.
|
||||
func (a Align) MarkdownProperty() string {
|
||||
switch a {
|
||||
case AlignLeft:
|
||||
return ":--- "
|
||||
case AlignCenter:
|
||||
return ":---:"
|
||||
case AlignRight:
|
||||
return " ---:"
|
||||
default:
|
||||
return " --- "
|
||||
}
|
||||
}
|
||||
|
||||
func (a Align) justifyText(text string, textLength int, maxLength int) string {
|
||||
// split the text into individual words
|
||||
wordsUnfiltered := strings.Split(text, " ")
|
||||
words := Filter(wordsUnfiltered, func(item string) bool {
|
||||
return item != ""
|
||||
})
|
||||
// empty string implies spaces for maxLength
|
||||
if len(words) == 0 {
|
||||
return strings.Repeat(" ", maxLength)
|
||||
}
|
||||
// get the number of spaces to insert into the text
|
||||
numSpacesNeeded := maxLength - textLength + strings.Count(text, " ")
|
||||
numSpacesNeededBetweenWords := 0
|
||||
if len(words) > 1 {
|
||||
numSpacesNeededBetweenWords = numSpacesNeeded / (len(words) - 1)
|
||||
}
|
||||
// create the output string word by word with spaces in between
|
||||
var outText strings.Builder
|
||||
outText.Grow(maxLength)
|
||||
for idx, word := range words {
|
||||
if idx > 0 {
|
||||
// insert spaces only after the first word
|
||||
if idx == len(words)-1 {
|
||||
// insert all the remaining space before the last word
|
||||
outText.WriteString(strings.Repeat(" ", numSpacesNeeded))
|
||||
numSpacesNeeded = 0
|
||||
} else {
|
||||
// insert the determined number of spaces between each word
|
||||
outText.WriteString(strings.Repeat(" ", numSpacesNeededBetweenWords))
|
||||
// and reduce the number of spaces needed after this
|
||||
numSpacesNeeded -= numSpacesNeededBetweenWords
|
||||
}
|
||||
}
|
||||
outText.WriteString(word)
|
||||
if idx == len(words)-1 && numSpacesNeeded > 0 {
|
||||
outText.WriteString(strings.Repeat(" ", numSpacesNeeded))
|
||||
}
|
||||
}
|
||||
return outText.String()
|
||||
}
|
||||
|
||||
func (a Align) trimString(text string) string {
|
||||
switch a {
|
||||
case AlignDefault, AlignLeft:
|
||||
if strings.HasSuffix(text, " ") {
|
||||
return strings.TrimRight(text, " ")
|
||||
}
|
||||
case AlignRight:
|
||||
if strings.HasPrefix(text, " ") {
|
||||
return strings.TrimLeft(text, " ")
|
||||
}
|
||||
default:
|
||||
if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") {
|
||||
return strings.Trim(text, " ")
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package text
|
||||
|
||||
import "strings"
|
||||
|
||||
// ANSICodesSupported will be true on consoles where ANSI Escape Codes/Sequences
|
||||
// are supported.
|
||||
var ANSICodesSupported = areANSICodesSupported()
|
||||
|
||||
// Escape encodes the string with the ANSI Escape Sequence.
|
||||
// For ex.:
|
||||
// Escape("Ghost", "") == "Ghost"
|
||||
// Escape("Ghost", "\x1b[91m") == "\x1b[91mGhost\x1b[0m"
|
||||
// Escape("\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||
// Escape("Nymeria\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||
// Escape("Nymeria \x1b[94mGhost\x1b[0m Lady", "\x1b[91m") == "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m"
|
||||
func Escape(str string, escapeSeq string) string {
|
||||
out := ""
|
||||
if !strings.HasPrefix(str, EscapeStart) {
|
||||
out += escapeSeq
|
||||
}
|
||||
out += strings.Replace(str, EscapeReset, EscapeReset+escapeSeq, -1)
|
||||
if !strings.HasSuffix(out, EscapeReset) {
|
||||
out += EscapeReset
|
||||
}
|
||||
if strings.Contains(out, escapeSeq+EscapeReset) {
|
||||
out = strings.Replace(out, escapeSeq+EscapeReset, "", -1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// StripEscape strips all ANSI Escape Sequence from the string.
|
||||
// For ex.:
|
||||
// StripEscape("Ghost") == "Ghost"
|
||||
// StripEscape("\x1b[91mGhost\x1b[0m") == "Ghost"
|
||||
// StripEscape("\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "GhostLady"
|
||||
// StripEscape("\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "NymeriaGhostLady"
|
||||
// StripEscape("\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m") == "Nymeria Ghost Lady"
|
||||
func StripEscape(str string) string {
|
||||
var out strings.Builder
|
||||
out.Grow(RuneCount(str))
|
||||
|
||||
isEscSeq := false
|
||||
for _, sChr := range str {
|
||||
if sChr == EscapeStartRune {
|
||||
isEscSeq = true
|
||||
}
|
||||
if !isEscSeq {
|
||||
out.WriteRune(sChr)
|
||||
}
|
||||
if isEscSeq && sChr == EscapeStopRune {
|
||||
isEscSeq = false
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// +build !windows
|
||||
|
||||
package text
|
||||
|
||||
func areANSICodesSupported() bool {
|
||||
return true
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// +build windows
|
||||
|
||||
package text
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
enableVTPMutex = sync.Mutex{}
|
||||
)
|
||||
|
||||
func areANSICodesSupported() bool {
|
||||
enableVTPMutex.Lock()
|
||||
defer enableVTPMutex.Unlock()
|
||||
|
||||
outHandle := windows.Handle(os.Stdout.Fd())
|
||||
var outMode uint32
|
||||
if err := windows.GetConsoleMode(outHandle, &outMode); err == nil {
|
||||
if outMode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 {
|
||||
return true
|
||||
}
|
||||
if err := windows.SetConsoleMode(outHandle, outMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
colorsEnabled = areANSICodesSupported()
|
||||
)
|
||||
|
||||
// DisableColors (forcefully) disables color coding globally.
|
||||
func DisableColors() {
|
||||
colorsEnabled = false
|
||||
}
|
||||
|
||||
// EnableColors (forcefully) enables color coding globally.
|
||||
func EnableColors() {
|
||||
colorsEnabled = true
|
||||
}
|
||||
|
||||
// The logic here is inspired from github.com/fatih/color; the following is
|
||||
// the the bare minimum logic required to print Colored to the console.
|
||||
// The differences:
|
||||
// * This one caches the escape sequences for cases with multiple colors
|
||||
// * This one handles cases where the incoming already has colors in the
|
||||
// form of escape sequences; in which case, text that does not have any
|
||||
// escape sequences are colored/escaped
|
||||
|
||||
// Color represents a single color to render with.
|
||||
type Color int
|
||||
|
||||
// Base colors -- attributes in reality
|
||||
const (
|
||||
Reset Color = iota
|
||||
Bold
|
||||
Faint
|
||||
Italic
|
||||
Underline
|
||||
BlinkSlow
|
||||
BlinkRapid
|
||||
ReverseVideo
|
||||
Concealed
|
||||
CrossedOut
|
||||
)
|
||||
|
||||
// Foreground colors
|
||||
const (
|
||||
FgBlack Color = iota + 30
|
||||
FgRed
|
||||
FgGreen
|
||||
FgYellow
|
||||
FgBlue
|
||||
FgMagenta
|
||||
FgCyan
|
||||
FgWhite
|
||||
)
|
||||
|
||||
// Foreground Hi-Intensity colors
|
||||
const (
|
||||
FgHiBlack Color = iota + 90
|
||||
FgHiRed
|
||||
FgHiGreen
|
||||
FgHiYellow
|
||||
FgHiBlue
|
||||
FgHiMagenta
|
||||
FgHiCyan
|
||||
FgHiWhite
|
||||
)
|
||||
|
||||
// Background colors
|
||||
const (
|
||||
BgBlack Color = iota + 40
|
||||
BgRed
|
||||
BgGreen
|
||||
BgYellow
|
||||
BgBlue
|
||||
BgMagenta
|
||||
BgCyan
|
||||
BgWhite
|
||||
)
|
||||
|
||||
// Background Hi-Intensity colors
|
||||
const (
|
||||
BgHiBlack Color = iota + 100
|
||||
BgHiRed
|
||||
BgHiGreen
|
||||
BgHiYellow
|
||||
BgHiBlue
|
||||
BgHiMagenta
|
||||
BgHiCyan
|
||||
BgHiWhite
|
||||
)
|
||||
|
||||
// EscapeSeq returns the ANSI escape sequence for the color.
|
||||
func (c Color) EscapeSeq() string {
|
||||
return EscapeStart + strconv.Itoa(int(c)) + EscapeStop
|
||||
}
|
||||
|
||||
// HTMLProperty returns the "class" attribute for the color.
|
||||
func (c Color) HTMLProperty() string {
|
||||
out := ""
|
||||
if class, ok := colorCSSClassMap[c]; ok {
|
||||
out = fmt.Sprintf("class=\"%s\"", class)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Sprint colorizes and prints the given string(s).
|
||||
func (c Color) Sprint(a ...interface{}) string {
|
||||
return colorize(fmt.Sprint(a...), c.EscapeSeq())
|
||||
}
|
||||
|
||||
// Sprintf formats and colorizes and prints the given string(s).
|
||||
func (c Color) Sprintf(format string, a ...interface{}) string {
|
||||
return colorize(fmt.Sprintf(format, a...), c.EscapeSeq())
|
||||
}
|
||||
|
||||
// Colors represents an array of Color objects to render with.
|
||||
// Example: Colors{FgCyan, BgBlack}
|
||||
type Colors []Color
|
||||
|
||||
var (
|
||||
// colorsSeqMap caches the escape sequence for a set of colors
|
||||
colorsSeqMap = sync.Map{}
|
||||
)
|
||||
|
||||
// EscapeSeq returns the ANSI escape sequence for the colors set.
|
||||
func (c Colors) EscapeSeq() string {
|
||||
if len(c) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
colorsKey := fmt.Sprintf("%#v", c)
|
||||
escapeSeq, ok := colorsSeqMap.Load(colorsKey)
|
||||
if !ok || escapeSeq == "" {
|
||||
colorNums := make([]string, len(c))
|
||||
for idx, color := range c {
|
||||
colorNums[idx] = strconv.Itoa(int(color))
|
||||
}
|
||||
escapeSeq = EscapeStart + strings.Join(colorNums, ";") + EscapeStop
|
||||
colorsSeqMap.Store(colorsKey, escapeSeq)
|
||||
}
|
||||
return escapeSeq.(string)
|
||||
}
|
||||
|
||||
// HTMLProperty returns the "class" attribute for the colors.
|
||||
func (c Colors) HTMLProperty() string {
|
||||
if len(c) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var classes []string
|
||||
for _, color := range c {
|
||||
if class, ok := colorCSSClassMap[color]; ok {
|
||||
classes = append(classes, class)
|
||||
}
|
||||
}
|
||||
if len(classes) > 1 {
|
||||
sort.Strings(classes)
|
||||
}
|
||||
return fmt.Sprintf("class=\"%s\"", strings.Join(classes, " "))
|
||||
}
|
||||
|
||||
// Sprint colorizes and prints the given string(s).
|
||||
func (c Colors) Sprint(a ...interface{}) string {
|
||||
return colorize(fmt.Sprint(a...), c.EscapeSeq())
|
||||
}
|
||||
|
||||
// Sprintf formats and colorizes and prints the given string(s).
|
||||
func (c Colors) Sprintf(format string, a ...interface{}) string {
|
||||
return colorize(fmt.Sprintf(format, a...), c.EscapeSeq())
|
||||
}
|
||||
|
||||
func colorize(s string, escapeSeq string) string {
|
||||
if !colorsEnabled || escapeSeq == "" {
|
||||
return s
|
||||
}
|
||||
return Escape(s, escapeSeq)
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package text
|
||||
|
||||
var (
|
||||
// colorCSSClassMap contains the equivalent CSS-class for all colors
|
||||
colorCSSClassMap = map[Color]string{
|
||||
Bold: "bold",
|
||||
Faint: "faint",
|
||||
Italic: "italic",
|
||||
Underline: "underline",
|
||||
BlinkSlow: "blink-slow",
|
||||
BlinkRapid: "blink-rapid",
|
||||
ReverseVideo: "reverse-video",
|
||||
Concealed: "concealed",
|
||||
CrossedOut: "crossed-out",
|
||||
FgBlack: "fg-black",
|
||||
FgRed: "fg-red",
|
||||
FgGreen: "fg-green",
|
||||
FgYellow: "fg-yellow",
|
||||
FgBlue: "fg-blue",
|
||||
FgMagenta: "fg-magenta",
|
||||
FgCyan: "fg-cyan",
|
||||
FgWhite: "fg-white",
|
||||
FgHiBlack: "fg-hi-black",
|
||||
FgHiRed: "fg-hi-red",
|
||||
FgHiGreen: "fg-hi-green",
|
||||
FgHiYellow: "fg-hi-yellow",
|
||||
FgHiBlue: "fg-hi-blue",
|
||||
FgHiMagenta: "fg-hi-magenta",
|
||||
FgHiCyan: "fg-hi-cyan",
|
||||
FgHiWhite: "fg-hi-white",
|
||||
BgBlack: "bg-black",
|
||||
BgRed: "bg-red",
|
||||
BgGreen: "bg-green",
|
||||
BgYellow: "bg-yellow",
|
||||
BgBlue: "bg-blue",
|
||||
BgMagenta: "bg-magenta",
|
||||
BgCyan: "bg-cyan",
|
||||
BgWhite: "bg-white",
|
||||
BgHiBlack: "bg-hi-black",
|
||||
BgHiRed: "bg-hi-red",
|
||||
BgHiGreen: "bg-hi-green",
|
||||
BgHiYellow: "bg-hi-yellow",
|
||||
BgHiBlue: "bg-hi-blue",
|
||||
BgHiMagenta: "bg-hi-magenta",
|
||||
BgHiCyan: "bg-hi-cyan",
|
||||
BgHiWhite: "bg-hi-white",
|
||||
}
|
||||
)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Cursor helps move the cursor on the console in multiple directions.
|
||||
type Cursor rune
|
||||
|
||||
const (
|
||||
// CursorDown helps move the Cursor Down X lines
|
||||
CursorDown Cursor = 'B'
|
||||
|
||||
// CursorLeft helps move the Cursor Left X characters
|
||||
CursorLeft Cursor = 'D'
|
||||
|
||||
// CursorRight helps move the Cursor Right X characters
|
||||
CursorRight Cursor = 'C'
|
||||
|
||||
// CursorUp helps move the Cursor Up X lines
|
||||
CursorUp Cursor = 'A'
|
||||
|
||||
// EraseLine helps erase all characters to the Right of the Cursor in the
|
||||
// current line
|
||||
EraseLine Cursor = 'K'
|
||||
)
|
||||
|
||||
// Sprint prints the Escape Sequence to move the Cursor once.
|
||||
func (c Cursor) Sprint() string {
|
||||
return fmt.Sprintf("%s%c", EscapeStart, c)
|
||||
}
|
||||
|
||||
// Sprintn prints the Escape Sequence to move the Cursor "n" times.
|
||||
func (c Cursor) Sprintn(n int) string {
|
||||
if c == EraseLine {
|
||||
return c.Sprint()
|
||||
}
|
||||
return fmt.Sprintf("%s%d%c", EscapeStart, n, c)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package text
|
||||
|
||||
// Filter filters the slice 's' to items which return truth when passed to 'f'.
|
||||
func Filter(s []string, f func(string) bool) []string {
|
||||
var out []string
|
||||
for _, item := range s {
|
||||
if f(item) {
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Format lets you transform the text in supported methods while keeping escape
|
||||
// sequences in the string intact and untouched.
|
||||
type Format int
|
||||
|
||||
// Format enumerations
|
||||
const (
|
||||
FormatDefault Format = iota // default_Case
|
||||
FormatLower // lower
|
||||
FormatTitle // Title
|
||||
FormatUpper // UPPER
|
||||
)
|
||||
|
||||
// Apply converts the text as directed.
|
||||
func (tc Format) Apply(text string) string {
|
||||
switch tc {
|
||||
case FormatLower:
|
||||
return strings.ToLower(text)
|
||||
case FormatTitle:
|
||||
return toTitle(text)
|
||||
case FormatUpper:
|
||||
return toUpper(text)
|
||||
default:
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
func toTitle(text string) string {
|
||||
prev, inEscSeq := ' ', false
|
||||
return strings.Map(
|
||||
func(r rune) rune {
|
||||
if r == EscapeStartRune {
|
||||
inEscSeq = true
|
||||
}
|
||||
if !inEscSeq {
|
||||
if isSeparator(prev) {
|
||||
prev = r
|
||||
r = unicode.ToUpper(r)
|
||||
} else {
|
||||
prev = r
|
||||
}
|
||||
}
|
||||
if inEscSeq && r == EscapeStopRune {
|
||||
inEscSeq = false
|
||||
}
|
||||
return r
|
||||
},
|
||||
text,
|
||||
)
|
||||
}
|
||||
|
||||
func toUpper(text string) string {
|
||||
inEscSeq := false
|
||||
return strings.Map(
|
||||
func(r rune) rune {
|
||||
if r == EscapeStartRune {
|
||||
inEscSeq = true
|
||||
}
|
||||
if !inEscSeq {
|
||||
r = unicode.ToUpper(r)
|
||||
}
|
||||
if inEscSeq && r == EscapeStopRune {
|
||||
inEscSeq = false
|
||||
}
|
||||
return r
|
||||
},
|
||||
text,
|
||||
)
|
||||
}
|
||||
|
||||
// isSeparator returns true if the given rune is a separator. This function is
|
||||
// lifted straight out of the standard library @ strings/strings.go.
|
||||
func isSeparator(r rune) bool {
|
||||
// ASCII alphanumerics and underscore are not separators
|
||||
if r <= 0x7F {
|
||||
switch {
|
||||
case '0' <= r && r <= '9':
|
||||
return false
|
||||
case 'a' <= r && r <= 'z':
|
||||
return false
|
||||
case 'A' <= r && r <= 'Z':
|
||||
return false
|
||||
case r == '_':
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
// Letters and digits are not separators
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
return false
|
||||
}
|
||||
// Otherwise, all we can do for now is treat spaces as separators.
|
||||
return unicode.IsSpace(r)
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// Constants
|
||||
const (
|
||||
EscapeReset = EscapeStart + "0" + EscapeStop
|
||||
EscapeStart = "\x1b["
|
||||
EscapeStartRune = rune(27) // \x1b
|
||||
EscapeStop = "m"
|
||||
EscapeStopRune = 'm'
|
||||
)
|
||||
|
||||
// InsertEveryN inserts the rune every N characters in the string. For ex.:
|
||||
// InsertEveryN("Ghost", '-', 1) == "G-h-o-s-t"
|
||||
// InsertEveryN("Ghost", '-', 2) == "Gh-os-t"
|
||||
// InsertEveryN("Ghost", '-', 3) == "Gho-st"
|
||||
// InsertEveryN("Ghost", '-', 4) == "Ghos-t"
|
||||
// InsertEveryN("Ghost", '-', 5) == "Ghost"
|
||||
func InsertEveryN(str string, runeToInsert rune, n int) string {
|
||||
if n <= 0 {
|
||||
return str
|
||||
}
|
||||
|
||||
sLen := RuneCount(str)
|
||||
var out strings.Builder
|
||||
out.Grow(sLen + (sLen / n))
|
||||
outLen, isEscSeq := 0, false
|
||||
for idx, c := range str {
|
||||
if c == EscapeStartRune {
|
||||
isEscSeq = true
|
||||
}
|
||||
|
||||
if !isEscSeq && outLen > 0 && (outLen%n) == 0 && idx != sLen {
|
||||
out.WriteRune(runeToInsert)
|
||||
}
|
||||
out.WriteRune(c)
|
||||
if !isEscSeq {
|
||||
outLen += RuneWidth(c)
|
||||
}
|
||||
|
||||
if isEscSeq && c == EscapeStopRune {
|
||||
isEscSeq = false
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// LongestLineLen returns the length of the longest "line" within the
|
||||
// argument string. For ex.:
|
||||
// LongestLineLen("Ghost!\nCome back here!\nRight now!") == 15
|
||||
func LongestLineLen(str string) int {
|
||||
maxLength, currLength, isEscSeq := 0, 0, false
|
||||
for _, c := range str {
|
||||
if c == EscapeStartRune {
|
||||
isEscSeq = true
|
||||
} else if isEscSeq && c == EscapeStopRune {
|
||||
isEscSeq = false
|
||||
continue
|
||||
}
|
||||
|
||||
if c == '\n' {
|
||||
if currLength > maxLength {
|
||||
maxLength = currLength
|
||||
}
|
||||
currLength = 0
|
||||
} else if !isEscSeq {
|
||||
currLength += RuneWidth(c)
|
||||
}
|
||||
}
|
||||
if currLength > maxLength {
|
||||
maxLength = currLength
|
||||
}
|
||||
return maxLength
|
||||
}
|
||||
|
||||
// Pad pads the given string with as many characters as needed to make it as
|
||||
// long as specified (maxLen). This function does not count escape sequences
|
||||
// while calculating length of the string. Ex.:
|
||||
// Pad("Ghost", 0, ' ') == "Ghost"
|
||||
// Pad("Ghost", 3, ' ') == "Ghost"
|
||||
// Pad("Ghost", 5, ' ') == "Ghost"
|
||||
// Pad("Ghost", 7, ' ') == "Ghost "
|
||||
// Pad("Ghost", 10, '.') == "Ghost....."
|
||||
func Pad(str string, maxLen int, paddingChar rune) string {
|
||||
strLen := RuneCount(str)
|
||||
if strLen < maxLen {
|
||||
str += strings.Repeat(string(paddingChar), maxLen-strLen)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// RepeatAndTrim repeats the given string until it is as long as maxRunes.
|
||||
// For ex.:
|
||||
// RepeatAndTrim("", 5) == ""
|
||||
// RepeatAndTrim("Ghost", 0) == ""
|
||||
// RepeatAndTrim("Ghost", 5) == "Ghost"
|
||||
// RepeatAndTrim("Ghost", 7) == "GhostGh"
|
||||
// RepeatAndTrim("Ghost", 10) == "GhostGhost"
|
||||
func RepeatAndTrim(str string, maxRunes int) string {
|
||||
if str == "" || maxRunes == 0 {
|
||||
return ""
|
||||
} else if maxRunes == utf8.RuneCountInString(str) {
|
||||
return str
|
||||
}
|
||||
repeatedS := strings.Repeat(str, int(maxRunes/utf8.RuneCountInString(str))+1)
|
||||
return Trim(repeatedS, maxRunes)
|
||||
}
|
||||
|
||||
// RuneCount is similar to utf8.RuneCountInString, except for the fact that it
|
||||
// ignores escape sequences while counting. For ex.:
|
||||
// RuneCount("") == 0
|
||||
// RuneCount("Ghost") == 5
|
||||
// RuneCount("\x1b[33mGhost\x1b[0m") == 5
|
||||
// RuneCount("\x1b[33mGhost\x1b[0") == 5
|
||||
func RuneCount(str string) int {
|
||||
count, isEscSeq := 0, false
|
||||
for _, c := range str {
|
||||
if c == EscapeStartRune {
|
||||
isEscSeq = true
|
||||
} else if isEscSeq {
|
||||
if c == EscapeStopRune {
|
||||
isEscSeq = false
|
||||
}
|
||||
} else {
|
||||
count += RuneWidth(c)
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// RuneWidth returns the mostly accurate character-width of the rune. This is
|
||||
// not 100% accurate as the character width is usually dependant on the
|
||||
// typeface (font) used in the console/terminal. For ex.:
|
||||
// RuneWidth('A') == 1
|
||||
// RuneWidth('ツ') == 2
|
||||
// RuneWidth('⊙') == 1
|
||||
// RuneWidth('︿') == 2
|
||||
// RuneWidth(0x27) == 0
|
||||
func RuneWidth(r rune) int {
|
||||
return runewidth.RuneWidth(r)
|
||||
}
|
||||
|
||||
// Snip returns the given string with a fixed length. For ex.:
|
||||
// Snip("Ghost", 0, "~") == "Ghost"
|
||||
// Snip("Ghost", 1, "~") == "~"
|
||||
// Snip("Ghost", 3, "~") == "Gh~"
|
||||
// Snip("Ghost", 5, "~") == "Ghost"
|
||||
// Snip("Ghost", 7, "~") == "Ghost "
|
||||
// Snip("\x1b[33mGhost\x1b[0m", 7, "~") == "\x1b[33mGhost\x1b[0m "
|
||||
func Snip(str string, length int, snipIndicator string) string {
|
||||
if length > 0 {
|
||||
lenStr := RuneCount(str)
|
||||
if lenStr > length {
|
||||
lenStrFinal := length - RuneCount(snipIndicator)
|
||||
return Trim(str, lenStrFinal) + snipIndicator
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// Trim trims a string to the given length while ignoring escape sequences. For
|
||||
// ex.:
|
||||
// Trim("Ghost", 3) == "Gho"
|
||||
// Trim("Ghost", 6) == "Ghost"
|
||||
// Trim("\x1b[33mGhost\x1b[0m", 3) == "\x1b[33mGho\x1b[0m"
|
||||
// Trim("\x1b[33mGhost\x1b[0m", 6) == "\x1b[33mGhost\x1b[0m"
|
||||
func Trim(str string, maxLen int) string {
|
||||
if maxLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
out.Grow(maxLen)
|
||||
|
||||
outLen, isEscSeq, lastEscSeq := 0, false, strings.Builder{}
|
||||
for _, sChr := range str {
|
||||
out.WriteRune(sChr)
|
||||
if sChr == EscapeStartRune {
|
||||
isEscSeq = true
|
||||
lastEscSeq.Reset()
|
||||
lastEscSeq.WriteRune(sChr)
|
||||
} else if isEscSeq {
|
||||
lastEscSeq.WriteRune(sChr)
|
||||
if sChr == EscapeStopRune {
|
||||
isEscSeq = false
|
||||
}
|
||||
} else {
|
||||
outLen++
|
||||
if outLen == maxLen {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if lastEscSeq.Len() > 0 && lastEscSeq.String() != EscapeReset {
|
||||
out.WriteString(EscapeReset)
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Transformer related constants
|
||||
const (
|
||||
unixTimeMinMilliseconds = int64(10000000000)
|
||||
unixTimeMinMicroseconds = unixTimeMinMilliseconds * 1000
|
||||
unixTimeMinNanoSeconds = unixTimeMinMicroseconds * 1000
|
||||
)
|
||||
|
||||
// Transformer related variables
|
||||
var (
|
||||
colorsNumberPositive = Colors{FgHiGreen}
|
||||
colorsNumberNegative = Colors{FgHiRed}
|
||||
colorsNumberZero = Colors{}
|
||||
colorsURL = Colors{Underline, FgBlue}
|
||||
rfc3339Milli = "2006-01-02T15:04:05.000Z07:00"
|
||||
rfc3339Micro = "2006-01-02T15:04:05.000000Z07:00"
|
||||
|
||||
possibleTimeLayouts = []string{
|
||||
time.RFC3339,
|
||||
rfc3339Milli, // strfmt.DateTime.String()'s default layout
|
||||
rfc3339Micro,
|
||||
time.RFC3339Nano,
|
||||
}
|
||||
)
|
||||
|
||||
// Transformer helps format the contents of an object to the user's liking.
|
||||
type Transformer func(val interface{}) string
|
||||
|
||||
// NewNumberTransformer returns a number Transformer that:
|
||||
// * transforms the number as directed by 'format' (ex.: %.2f)
|
||||
// * colors negative values Red
|
||||
// * colors positive values Green
|
||||
func NewNumberTransformer(format string) Transformer {
|
||||
return func(val interface{}) string {
|
||||
if number, ok := val.(int); ok {
|
||||
return transformInt(format, int64(number))
|
||||
}
|
||||
if number, ok := val.(int8); ok {
|
||||
return transformInt(format, int64(number))
|
||||
}
|
||||
if number, ok := val.(int16); ok {
|
||||
return transformInt(format, int64(number))
|
||||
}
|
||||
if number, ok := val.(int32); ok {
|
||||
return transformInt(format, int64(number))
|
||||
}
|
||||
if number, ok := val.(int64); ok {
|
||||
return transformInt(format, int64(number))
|
||||
}
|
||||
if number, ok := val.(uint); ok {
|
||||
return transformUint(format, uint64(number))
|
||||
}
|
||||
if number, ok := val.(uint8); ok {
|
||||
return transformUint(format, uint64(number))
|
||||
}
|
||||
if number, ok := val.(uint16); ok {
|
||||
return transformUint(format, uint64(number))
|
||||
}
|
||||
if number, ok := val.(uint32); ok {
|
||||
return transformUint(format, uint64(number))
|
||||
}
|
||||
if number, ok := val.(uint64); ok {
|
||||
return transformUint(format, uint64(number))
|
||||
}
|
||||
if number, ok := val.(float32); ok {
|
||||
return transformFloat(format, float64(number))
|
||||
}
|
||||
if number, ok := val.(float64); ok {
|
||||
return transformFloat(format, float64(number))
|
||||
}
|
||||
return fmt.Sprint(val)
|
||||
}
|
||||
}
|
||||
|
||||
func transformInt(format string, val int64) string {
|
||||
if val < 0 {
|
||||
return colorsNumberNegative.Sprintf("-"+format, -val)
|
||||
}
|
||||
if val > 0 {
|
||||
return colorsNumberPositive.Sprintf(format, val)
|
||||
}
|
||||
return colorsNumberZero.Sprintf(format, val)
|
||||
}
|
||||
|
||||
func transformUint(format string, val uint64) string {
|
||||
if val > 0 {
|
||||
return colorsNumberPositive.Sprintf(format, val)
|
||||
}
|
||||
return colorsNumberZero.Sprintf(format, val)
|
||||
}
|
||||
|
||||
func transformFloat(format string, val float64) string {
|
||||
if val < 0 {
|
||||
return colorsNumberNegative.Sprintf("-"+format, -val)
|
||||
}
|
||||
if val > 0 {
|
||||
return colorsNumberPositive.Sprintf(format, val)
|
||||
}
|
||||
return colorsNumberZero.Sprintf(format, val)
|
||||
}
|
||||
|
||||
// NewJSONTransformer returns a Transformer that can format a JSON string or an
|
||||
// object into pretty-indented JSON-strings.
|
||||
func NewJSONTransformer(prefix string, indent string) Transformer {
|
||||
return func(val interface{}) string {
|
||||
if valStr, ok := val.(string); ok {
|
||||
var b bytes.Buffer
|
||||
if err := json.Indent(&b, []byte(strings.TrimSpace(valStr)), prefix, indent); err == nil {
|
||||
return string(b.Bytes())
|
||||
}
|
||||
} else if b, err := json.MarshalIndent(val, prefix, indent); err == nil {
|
||||
return string(b)
|
||||
}
|
||||
return fmt.Sprintf("%#v", val)
|
||||
}
|
||||
}
|
||||
|
||||
// NewTimeTransformer returns a Transformer that can format a timestamp (a
|
||||
// time.Time) into a well-defined time format defined using the provided layout
|
||||
// (ex.: time.RFC3339).
|
||||
//
|
||||
// If a non-nil location value is provided, the time will be localized to that
|
||||
// location (use time.Local to get localized timestamps).
|
||||
func NewTimeTransformer(layout string, location *time.Location) Transformer {
|
||||
return func(val interface{}) string {
|
||||
formatTime := func(t time.Time) string {
|
||||
rsp := ""
|
||||
if t.Unix() > 0 {
|
||||
if location != nil {
|
||||
t = t.In(location)
|
||||
}
|
||||
rsp = t.Format(layout)
|
||||
}
|
||||
return rsp
|
||||
}
|
||||
|
||||
rsp := fmt.Sprint(val)
|
||||
if valTime, ok := val.(time.Time); ok {
|
||||
rsp = formatTime(valTime)
|
||||
} else {
|
||||
// cycle through some supported layouts to see if the string form
|
||||
// of the object matches any of these layouts
|
||||
for _, possibleTimeLayout := range possibleTimeLayouts {
|
||||
if valTime, err := time.Parse(possibleTimeLayout, rsp); err == nil {
|
||||
rsp = formatTime(valTime)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return rsp
|
||||
}
|
||||
}
|
||||
|
||||
// NewUnixTimeTransformer returns a Transformer that can format a unix-timestamp
|
||||
// into a well-defined time format as defined by 'layout'. This can handle
|
||||
// unix-time in Seconds, MilliSeconds, Microseconds and Nanoseconds.
|
||||
//
|
||||
// If a non-nil location value is provided, the time will be localized to that
|
||||
// location (use time.Local to get localized timestamps).
|
||||
func NewUnixTimeTransformer(layout string, location *time.Location) Transformer {
|
||||
timeTransformer := NewTimeTransformer(layout, location)
|
||||
formatUnixTime := func(unixTime int64) string {
|
||||
if unixTime >= unixTimeMinNanoSeconds {
|
||||
unixTime = unixTime / time.Second.Nanoseconds()
|
||||
} else if unixTime >= unixTimeMinMicroseconds {
|
||||
unixTime = unixTime / (time.Second.Nanoseconds() / 1000)
|
||||
} else if unixTime >= unixTimeMinMilliseconds {
|
||||
unixTime = unixTime / (time.Second.Nanoseconds() / 1000000)
|
||||
}
|
||||
return timeTransformer(time.Unix(unixTime, 0))
|
||||
}
|
||||
|
||||
return func(val interface{}) string {
|
||||
if unixTime, ok := val.(int64); ok {
|
||||
return formatUnixTime(unixTime)
|
||||
} else if unixTimeStr, ok := val.(string); ok {
|
||||
if unixTime, err := strconv.ParseInt(unixTimeStr, 10, 64); err == nil {
|
||||
return formatUnixTime(unixTime)
|
||||
}
|
||||
}
|
||||
return fmt.Sprint(val)
|
||||
}
|
||||
}
|
||||
|
||||
// NewURLTransformer returns a Transformer that can format and pretty print a string
|
||||
// that contains an URL (the text is underlined and colored Blue).
|
||||
func NewURLTransformer() Transformer {
|
||||
return func(val interface{}) string {
|
||||
return colorsURL.Sprint(val)
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package text
|
||||
|
||||
import "strings"
|
||||
|
||||
// VAlign denotes how text is to be aligned vertically.
|
||||
type VAlign int
|
||||
|
||||
// VAlign enumerations
|
||||
const (
|
||||
VAlignDefault VAlign = iota // same as VAlignTop
|
||||
VAlignTop // "top\n\n"
|
||||
VAlignMiddle // "\nmiddle\n"
|
||||
VAlignBottom // "\n\nbottom"
|
||||
)
|
||||
|
||||
// Apply aligns the lines vertically. For ex.:
|
||||
// * VAlignTop.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"Game", "Of", "Thrones", "", ""}
|
||||
// * VAlignMiddle.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"", "Game", "Of", "Thrones", ""}
|
||||
// * VAlignBottom.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"", "", "Game", "Of", "Thrones"}
|
||||
func (va VAlign) Apply(lines []string, maxLines int) []string {
|
||||
if len(lines) == maxLines {
|
||||
return lines
|
||||
} else if len(lines) > maxLines {
|
||||
maxLines = len(lines)
|
||||
}
|
||||
|
||||
insertIdx := 0
|
||||
if va == VAlignMiddle {
|
||||
insertIdx = int(maxLines-len(lines)) / 2
|
||||
} else if va == VAlignBottom {
|
||||
insertIdx = maxLines - len(lines)
|
||||
}
|
||||
|
||||
linesOut := strings.Split(strings.Repeat("\n", maxLines-1), "\n")
|
||||
for idx, line := range lines {
|
||||
linesOut[idx+insertIdx] = line
|
||||
}
|
||||
return linesOut
|
||||
}
|
||||
|
||||
// ApplyStr aligns the string (of 1 or more lines) vertically. For ex.:
|
||||
// * VAlignTop.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"Game", "Of", "Thrones", "", ""}
|
||||
// * VAlignMiddle.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"", "Game", "Of", "Thrones", ""}
|
||||
// * VAlignBottom.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"", "", "Game", "Of", "Thrones"}
|
||||
func (va VAlign) ApplyStr(text string, maxLines int) []string {
|
||||
return va.Apply(strings.Split(text, "\n"), maxLines)
|
||||
}
|
||||
|
||||
// HTMLProperty returns the equivalent HTML vertical-align tag property.
|
||||
func (va VAlign) HTMLProperty() string {
|
||||
switch va {
|
||||
case VAlignTop:
|
||||
return "valign=\"top\""
|
||||
case VAlignMiddle:
|
||||
return "valign=\"middle\""
|
||||
case VAlignBottom:
|
||||
return "valign=\"bottom\""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// WrapHard wraps a string to the given length using a newline. Handles strings
|
||||
// with ANSI escape sequences (such as text color) without breaking the text
|
||||
// formatting. Breaks all words that go beyond the line boundary.
|
||||
//
|
||||
// For examples, refer to the unit-tests or GoDoc examples.
|
||||
func WrapHard(str string, wrapLen int) string {
|
||||
if wrapLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
str = strings.Replace(str, "\t", " ", -1)
|
||||
sLen := utf8.RuneCountInString(str)
|
||||
if sLen <= wrapLen {
|
||||
return str
|
||||
}
|
||||
|
||||
out := &strings.Builder{}
|
||||
out.Grow(sLen + (sLen / wrapLen))
|
||||
for idx, paragraph := range strings.Split(str, "\n\n") {
|
||||
if idx > 0 {
|
||||
out.WriteString("\n\n")
|
||||
}
|
||||
wrapHard(paragraph, wrapLen, out)
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// WrapSoft wraps a string to the given length using a newline. Handles strings
|
||||
// with ANSI escape sequences (such as text color) without breaking the text
|
||||
// formatting. Tries to move words that go beyond the line boundary to the next
|
||||
// line.
|
||||
//
|
||||
// For examples, refer to the unit-tests or GoDoc examples.
|
||||
func WrapSoft(str string, wrapLen int) string {
|
||||
if wrapLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
str = strings.Replace(str, "\t", " ", -1)
|
||||
sLen := utf8.RuneCountInString(str)
|
||||
if sLen <= wrapLen {
|
||||
return str
|
||||
}
|
||||
|
||||
out := &strings.Builder{}
|
||||
out.Grow(sLen + (sLen / wrapLen))
|
||||
for idx, paragraph := range strings.Split(str, "\n\n") {
|
||||
if idx > 0 {
|
||||
out.WriteString("\n\n")
|
||||
}
|
||||
wrapSoft(paragraph, wrapLen, out)
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// WrapText is very similar to WrapHard except for one minor difference. Unlike
|
||||
// WrapHard which discards line-breaks and respects only paragraph-breaks, this
|
||||
// function respects line-breaks too.
|
||||
//
|
||||
// For examples, refer to the unit-tests or GoDoc examples.
|
||||
func WrapText(str string, wrapLen int) string {
|
||||
if wrapLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
sLen := utf8.RuneCountInString(str)
|
||||
out.Grow(sLen + (sLen / wrapLen))
|
||||
lineIdx, isEscSeq, lastEscSeq := 0, false, ""
|
||||
for _, char := range str {
|
||||
if char == EscapeStartRune {
|
||||
isEscSeq = true
|
||||
lastEscSeq = ""
|
||||
}
|
||||
if isEscSeq {
|
||||
lastEscSeq += string(char)
|
||||
}
|
||||
|
||||
appendChar(char, wrapLen, &lineIdx, isEscSeq, lastEscSeq, &out)
|
||||
|
||||
if isEscSeq && char == EscapeStopRune {
|
||||
isEscSeq = false
|
||||
}
|
||||
if lastEscSeq == EscapeReset {
|
||||
lastEscSeq = ""
|
||||
}
|
||||
}
|
||||
if lastEscSeq != "" && lastEscSeq != EscapeReset {
|
||||
out.WriteString(EscapeReset)
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func appendChar(char rune, wrapLen int, lineLen *int, inEscSeq bool, lastSeenEscSeq string, out *strings.Builder) {
|
||||
// handle reaching the end of the line as dictated by wrapLen or by finding
|
||||
// a newline character
|
||||
if (*lineLen == wrapLen && !inEscSeq && char != '\n') || (char == '\n') {
|
||||
if lastSeenEscSeq != "" {
|
||||
// terminate escape sequence and the line; and restart the escape
|
||||
// sequence in the next line
|
||||
out.WriteString(EscapeReset)
|
||||
out.WriteRune('\n')
|
||||
out.WriteString(lastSeenEscSeq)
|
||||
} else {
|
||||
// just start a new line
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
// reset line index to 0th character
|
||||
*lineLen = 0
|
||||
}
|
||||
|
||||
// if the rune is not a new line, output it
|
||||
if char != '\n' {
|
||||
out.WriteRune(char)
|
||||
|
||||
// increment the line index if not in the middle of an escape sequence
|
||||
if !inEscSeq {
|
||||
*lineLen++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendWord(word string, lineIdx *int, lastSeenEscSeq string, wrapLen int, out *strings.Builder) {
|
||||
inEscSeq := false
|
||||
for _, char := range word {
|
||||
if char == EscapeStartRune {
|
||||
inEscSeq = true
|
||||
lastSeenEscSeq = ""
|
||||
}
|
||||
if inEscSeq {
|
||||
lastSeenEscSeq += string(char)
|
||||
}
|
||||
|
||||
appendChar(char, wrapLen, lineIdx, inEscSeq, lastSeenEscSeq, out)
|
||||
|
||||
if inEscSeq && char == EscapeStopRune {
|
||||
inEscSeq = false
|
||||
}
|
||||
if lastSeenEscSeq == EscapeReset {
|
||||
lastSeenEscSeq = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractOpenEscapeSeq(str string) string {
|
||||
escapeSeq, inEscSeq := "", false
|
||||
for _, char := range str {
|
||||
if char == EscapeStartRune {
|
||||
inEscSeq = true
|
||||
escapeSeq = ""
|
||||
}
|
||||
if inEscSeq {
|
||||
escapeSeq += string(char)
|
||||
}
|
||||
if char == EscapeStopRune {
|
||||
inEscSeq = false
|
||||
}
|
||||
}
|
||||
if escapeSeq == EscapeReset {
|
||||
escapeSeq = ""
|
||||
}
|
||||
return escapeSeq
|
||||
}
|
||||
|
||||
func terminateLine(wrapLen int, lineLen *int, lastSeenEscSeq string, out *strings.Builder) {
|
||||
if *lineLen < wrapLen {
|
||||
out.WriteString(strings.Repeat(" ", wrapLen-*lineLen))
|
||||
}
|
||||
// something is already on the line; terminate it
|
||||
if lastSeenEscSeq != "" {
|
||||
out.WriteString(EscapeReset)
|
||||
}
|
||||
out.WriteRune('\n')
|
||||
out.WriteString(lastSeenEscSeq)
|
||||
*lineLen = 0
|
||||
}
|
||||
|
||||
func terminateOutput(lastSeenEscSeq string, out *strings.Builder) {
|
||||
if lastSeenEscSeq != "" && lastSeenEscSeq != EscapeReset && !strings.HasSuffix(out.String(), EscapeReset) {
|
||||
out.WriteString(EscapeReset)
|
||||
}
|
||||
}
|
||||
|
||||
func wrapHard(paragraph string, wrapLen int, out *strings.Builder) {
|
||||
lineLen, lastSeenEscSeq := 0, ""
|
||||
words := strings.Fields(paragraph)
|
||||
for wordIdx, word := range words {
|
||||
escSeq := extractOpenEscapeSeq(word)
|
||||
if escSeq != "" {
|
||||
lastSeenEscSeq = escSeq
|
||||
}
|
||||
if lineLen > 0 {
|
||||
out.WriteRune(' ')
|
||||
lineLen++
|
||||
}
|
||||
|
||||
wordLen := RuneCount(word)
|
||||
if lineLen+wordLen <= wrapLen { // word fits within the line
|
||||
out.WriteString(word)
|
||||
lineLen += wordLen
|
||||
} else { // word doesn't fit within the line; hard-wrap
|
||||
appendWord(word, &lineLen, lastSeenEscSeq, wrapLen, out)
|
||||
}
|
||||
|
||||
// end of line; but more words incoming
|
||||
if lineLen == wrapLen && wordIdx < len(words)-1 {
|
||||
terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
|
||||
}
|
||||
}
|
||||
terminateOutput(lastSeenEscSeq, out)
|
||||
}
|
||||
|
||||
func wrapSoft(paragraph string, wrapLen int, out *strings.Builder) {
|
||||
lineLen, lastSeenEscSeq := 0, ""
|
||||
words := strings.Fields(paragraph)
|
||||
for wordIdx, word := range words {
|
||||
escSeq := extractOpenEscapeSeq(word)
|
||||
if escSeq != "" {
|
||||
lastSeenEscSeq = escSeq
|
||||
}
|
||||
spacing, spacingLen := "", 0
|
||||
if lineLen > 0 {
|
||||
spacing, spacingLen = " ", 1
|
||||
}
|
||||
|
||||
wordLen := RuneCount(word)
|
||||
if lineLen+spacingLen+wordLen <= wrapLen { // word fits within the line
|
||||
out.WriteString(spacing)
|
||||
out.WriteString(word)
|
||||
lineLen += spacingLen + wordLen
|
||||
} else { // word doesn't fit within the line
|
||||
if lineLen > 0 { // something is already on the line; terminate it
|
||||
terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
|
||||
}
|
||||
if wordLen <= wrapLen { // word fits within a single line
|
||||
out.WriteString(word)
|
||||
lineLen = wordLen
|
||||
} else { // word doesn't fit within a single line; hard-wrap
|
||||
appendWord(word, &lineLen, lastSeenEscSeq, wrapLen, out)
|
||||
}
|
||||
}
|
||||
|
||||
// end of line; but more words incoming
|
||||
if lineLen == wrapLen && wordIdx < len(words)-1 {
|
||||
terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
|
||||
}
|
||||
}
|
||||
terminateOutput(lastSeenEscSeq, out)
|
||||
}
|
||||
+11
-3
@@ -1,8 +1,16 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.13.x
|
||||
- tip
|
||||
|
||||
before_install:
|
||||
- go get github.com/mattn/goveralls
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
- go get -t -v ./...
|
||||
|
||||
script:
|
||||
- $HOME/gopath/bin/goveralls -repotoken lAKAWPzcGsD3A8yBX3BGGtRUdJ6CaGERL
|
||||
- go generate
|
||||
- git diff --cached --exit-code
|
||||
- ./go.test.sh
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
Generated
Vendored
+1
-1
@@ -2,7 +2,7 @@ go-runewidth
|
||||
============
|
||||
|
||||
[](https://travis-ci.org/mattn/go-runewidth)
|
||||
[](https://coveralls.io/r/mattn/go-runewidth?branch=HEAD)
|
||||
[](https://codecov.io/gh/mattn/go-runewidth)
|
||||
[](http://godoc.org/github.com/mattn/go-runewidth)
|
||||
[](https://goreportcard.com/report/github.com/mattn/go-runewidth)
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
for d in $(go list ./... | grep -v vendor); do
|
||||
go test -race -coverprofile=profile.out -covermode=atomic "$d"
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
fi
|
||||
done
|
||||
-1
@@ -50,7 +50,6 @@ func inTables(r rune, ts ...table) bool {
|
||||
}
|
||||
|
||||
func inTable(r rune, t table) bool {
|
||||
// func (t table) IncludesRune(r rune) bool {
|
||||
if r < t[0].first {
|
||||
return false
|
||||
}
|
||||
|
||||
+4
-1
@@ -62,7 +62,10 @@ func isEastAsian(locale string) bool {
|
||||
|
||||
// IsEastAsian return true if the current locale is CJK
|
||||
func IsEastAsian() bool {
|
||||
locale := os.Getenv("LC_CTYPE")
|
||||
locale := os.Getenv("LC_ALL")
|
||||
if locale == "" {
|
||||
locale = os.Getenv("LC_CTYPE")
|
||||
}
|
||||
if locale == "" {
|
||||
locale = os.Getenv("LANG")
|
||||
}
|
||||
|
||||
+215
-205
@@ -1,20 +1,23 @@
|
||||
// Code generated by script/generate.go. DO NOT EDIT.
|
||||
|
||||
package runewidth
|
||||
|
||||
var combining = table{
|
||||
{0x0300, 0x036F}, {0x0483, 0x0489}, {0x07EB, 0x07F3},
|
||||
{0x0C00, 0x0C00}, {0x0C04, 0x0C04}, {0x0D00, 0x0D01},
|
||||
{0x135D, 0x135F}, {0x1A7F, 0x1A7F}, {0x1AB0, 0x1ABE},
|
||||
{0x135D, 0x135F}, {0x1A7F, 0x1A7F}, {0x1AB0, 0x1AC0},
|
||||
{0x1B6B, 0x1B73}, {0x1DC0, 0x1DF9}, {0x1DFB, 0x1DFF},
|
||||
{0x20D0, 0x20F0}, {0x2CEF, 0x2CF1}, {0x2DE0, 0x2DFF},
|
||||
{0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D},
|
||||
{0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA8E0, 0xA8F1},
|
||||
{0xFE20, 0xFE2F}, {0x101FD, 0x101FD}, {0x10376, 0x1037A},
|
||||
{0x10F46, 0x10F50}, {0x11300, 0x11301}, {0x1133B, 0x1133C},
|
||||
{0x11366, 0x1136C}, {0x11370, 0x11374}, {0x16AF0, 0x16AF4},
|
||||
{0x1D165, 0x1D169}, {0x1D16D, 0x1D172}, {0x1D17B, 0x1D182},
|
||||
{0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244},
|
||||
{0x1E000, 0x1E006}, {0x1E008, 0x1E018}, {0x1E01B, 0x1E021},
|
||||
{0x1E023, 0x1E024}, {0x1E026, 0x1E02A}, {0x1E8D0, 0x1E8D6},
|
||||
{0x10EAB, 0x10EAC}, {0x10F46, 0x10F50}, {0x11300, 0x11301},
|
||||
{0x1133B, 0x1133C}, {0x11366, 0x1136C}, {0x11370, 0x11374},
|
||||
{0x16AF0, 0x16AF4}, {0x1D165, 0x1D169}, {0x1D16D, 0x1D172},
|
||||
{0x1D17B, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD},
|
||||
{0x1D242, 0x1D244}, {0x1E000, 0x1E006}, {0x1E008, 0x1E018},
|
||||
{0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, {0x1E026, 0x1E02A},
|
||||
{0x1E8D0, 0x1E8D6},
|
||||
}
|
||||
|
||||
var doublewidth = table{
|
||||
@@ -32,29 +35,30 @@ var doublewidth = table{
|
||||
{0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x2E80, 0x2E99},
|
||||
{0x2E9B, 0x2EF3}, {0x2F00, 0x2FD5}, {0x2FF0, 0x2FFB},
|
||||
{0x3000, 0x303E}, {0x3041, 0x3096}, {0x3099, 0x30FF},
|
||||
{0x3105, 0x312F}, {0x3131, 0x318E}, {0x3190, 0x31BA},
|
||||
{0x31C0, 0x31E3}, {0x31F0, 0x321E}, {0x3220, 0x3247},
|
||||
{0x3250, 0x4DBF}, {0x4E00, 0xA48C}, {0xA490, 0xA4C6},
|
||||
{0xA960, 0xA97C}, {0xAC00, 0xD7A3}, {0xF900, 0xFAFF},
|
||||
{0xFE10, 0xFE19}, {0xFE30, 0xFE52}, {0xFE54, 0xFE66},
|
||||
{0xFE68, 0xFE6B}, {0xFF01, 0xFF60}, {0xFFE0, 0xFFE6},
|
||||
{0x16FE0, 0x16FE3}, {0x17000, 0x187F7}, {0x18800, 0x18AF2},
|
||||
{0x1B000, 0x1B11E}, {0x1B150, 0x1B152}, {0x1B164, 0x1B167},
|
||||
{0x1B170, 0x1B2FB}, {0x1F004, 0x1F004}, {0x1F0CF, 0x1F0CF},
|
||||
{0x1F18E, 0x1F18E}, {0x1F191, 0x1F19A}, {0x1F200, 0x1F202},
|
||||
{0x1F210, 0x1F23B}, {0x1F240, 0x1F248}, {0x1F250, 0x1F251},
|
||||
{0x1F260, 0x1F265}, {0x1F300, 0x1F320}, {0x1F32D, 0x1F335},
|
||||
{0x1F337, 0x1F37C}, {0x1F37E, 0x1F393}, {0x1F3A0, 0x1F3CA},
|
||||
{0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0}, {0x1F3F4, 0x1F3F4},
|
||||
{0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, {0x1F442, 0x1F4FC},
|
||||
{0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E}, {0x1F550, 0x1F567},
|
||||
{0x1F57A, 0x1F57A}, {0x1F595, 0x1F596}, {0x1F5A4, 0x1F5A4},
|
||||
{0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5}, {0x1F6CC, 0x1F6CC},
|
||||
{0x1F6D0, 0x1F6D2}, {0x1F6D5, 0x1F6D5}, {0x1F6EB, 0x1F6EC},
|
||||
{0x1F6F4, 0x1F6FA}, {0x1F7E0, 0x1F7EB}, {0x1F90D, 0x1F971},
|
||||
{0x1F973, 0x1F976}, {0x1F97A, 0x1F9A2}, {0x1F9A5, 0x1F9AA},
|
||||
{0x1F9AE, 0x1F9CA}, {0x1F9CD, 0x1F9FF}, {0x1FA70, 0x1FA73},
|
||||
{0x1FA78, 0x1FA7A}, {0x1FA80, 0x1FA82}, {0x1FA90, 0x1FA95},
|
||||
{0x3105, 0x312F}, {0x3131, 0x318E}, {0x3190, 0x31E3},
|
||||
{0x31F0, 0x321E}, {0x3220, 0x3247}, {0x3250, 0x4DBF},
|
||||
{0x4E00, 0xA48C}, {0xA490, 0xA4C6}, {0xA960, 0xA97C},
|
||||
{0xAC00, 0xD7A3}, {0xF900, 0xFAFF}, {0xFE10, 0xFE19},
|
||||
{0xFE30, 0xFE52}, {0xFE54, 0xFE66}, {0xFE68, 0xFE6B},
|
||||
{0xFF01, 0xFF60}, {0xFFE0, 0xFFE6}, {0x16FE0, 0x16FE4},
|
||||
{0x16FF0, 0x16FF1}, {0x17000, 0x187F7}, {0x18800, 0x18CD5},
|
||||
{0x18D00, 0x18D08}, {0x1B000, 0x1B11E}, {0x1B150, 0x1B152},
|
||||
{0x1B164, 0x1B167}, {0x1B170, 0x1B2FB}, {0x1F004, 0x1F004},
|
||||
{0x1F0CF, 0x1F0CF}, {0x1F18E, 0x1F18E}, {0x1F191, 0x1F19A},
|
||||
{0x1F200, 0x1F202}, {0x1F210, 0x1F23B}, {0x1F240, 0x1F248},
|
||||
{0x1F250, 0x1F251}, {0x1F260, 0x1F265}, {0x1F300, 0x1F320},
|
||||
{0x1F32D, 0x1F335}, {0x1F337, 0x1F37C}, {0x1F37E, 0x1F393},
|
||||
{0x1F3A0, 0x1F3CA}, {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0},
|
||||
{0x1F3F4, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440},
|
||||
{0x1F442, 0x1F4FC}, {0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E},
|
||||
{0x1F550, 0x1F567}, {0x1F57A, 0x1F57A}, {0x1F595, 0x1F596},
|
||||
{0x1F5A4, 0x1F5A4}, {0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5},
|
||||
{0x1F6CC, 0x1F6CC}, {0x1F6D0, 0x1F6D2}, {0x1F6D5, 0x1F6D7},
|
||||
{0x1F6EB, 0x1F6EC}, {0x1F6F4, 0x1F6FC}, {0x1F7E0, 0x1F7EB},
|
||||
{0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1F978},
|
||||
{0x1F97A, 0x1F9CB}, {0x1F9CD, 0x1F9FF}, {0x1FA70, 0x1FA74},
|
||||
{0x1FA78, 0x1FA7A}, {0x1FA80, 0x1FA86}, {0x1FA90, 0x1FAA8},
|
||||
{0x1FAB0, 0x1FAB6}, {0x1FAC0, 0x1FAC2}, {0x1FAD0, 0x1FAD6},
|
||||
{0x20000, 0x2FFFD}, {0x30000, 0x3FFFD},
|
||||
}
|
||||
|
||||
@@ -151,7 +155,7 @@ var neutral = table{
|
||||
{0x0600, 0x061C}, {0x061E, 0x070D}, {0x070F, 0x074A},
|
||||
{0x074D, 0x07B1}, {0x07C0, 0x07FA}, {0x07FD, 0x082D},
|
||||
{0x0830, 0x083E}, {0x0840, 0x085B}, {0x085E, 0x085E},
|
||||
{0x0860, 0x086A}, {0x08A0, 0x08B4}, {0x08B6, 0x08BD},
|
||||
{0x0860, 0x086A}, {0x08A0, 0x08B4}, {0x08B6, 0x08C7},
|
||||
{0x08D3, 0x0983}, {0x0985, 0x098C}, {0x098F, 0x0990},
|
||||
{0x0993, 0x09A8}, {0x09AA, 0x09B0}, {0x09B2, 0x09B2},
|
||||
{0x09B6, 0x09B9}, {0x09BC, 0x09C4}, {0x09C7, 0x09C8},
|
||||
@@ -170,7 +174,7 @@ var neutral = table{
|
||||
{0x0B05, 0x0B0C}, {0x0B0F, 0x0B10}, {0x0B13, 0x0B28},
|
||||
{0x0B2A, 0x0B30}, {0x0B32, 0x0B33}, {0x0B35, 0x0B39},
|
||||
{0x0B3C, 0x0B44}, {0x0B47, 0x0B48}, {0x0B4B, 0x0B4D},
|
||||
{0x0B56, 0x0B57}, {0x0B5C, 0x0B5D}, {0x0B5F, 0x0B63},
|
||||
{0x0B55, 0x0B57}, {0x0B5C, 0x0B5D}, {0x0B5F, 0x0B63},
|
||||
{0x0B66, 0x0B77}, {0x0B82, 0x0B83}, {0x0B85, 0x0B8A},
|
||||
{0x0B8E, 0x0B90}, {0x0B92, 0x0B95}, {0x0B99, 0x0B9A},
|
||||
{0x0B9C, 0x0B9C}, {0x0B9E, 0x0B9F}, {0x0BA3, 0x0BA4},
|
||||
@@ -184,166 +188,169 @@ var neutral = table{
|
||||
{0x0C92, 0x0CA8}, {0x0CAA, 0x0CB3}, {0x0CB5, 0x0CB9},
|
||||
{0x0CBC, 0x0CC4}, {0x0CC6, 0x0CC8}, {0x0CCA, 0x0CCD},
|
||||
{0x0CD5, 0x0CD6}, {0x0CDE, 0x0CDE}, {0x0CE0, 0x0CE3},
|
||||
{0x0CE6, 0x0CEF}, {0x0CF1, 0x0CF2}, {0x0D00, 0x0D03},
|
||||
{0x0D05, 0x0D0C}, {0x0D0E, 0x0D10}, {0x0D12, 0x0D44},
|
||||
{0x0D46, 0x0D48}, {0x0D4A, 0x0D4F}, {0x0D54, 0x0D63},
|
||||
{0x0D66, 0x0D7F}, {0x0D82, 0x0D83}, {0x0D85, 0x0D96},
|
||||
{0x0D9A, 0x0DB1}, {0x0DB3, 0x0DBB}, {0x0DBD, 0x0DBD},
|
||||
{0x0DC0, 0x0DC6}, {0x0DCA, 0x0DCA}, {0x0DCF, 0x0DD4},
|
||||
{0x0DD6, 0x0DD6}, {0x0DD8, 0x0DDF}, {0x0DE6, 0x0DEF},
|
||||
{0x0DF2, 0x0DF4}, {0x0E01, 0x0E3A}, {0x0E3F, 0x0E5B},
|
||||
{0x0E81, 0x0E82}, {0x0E84, 0x0E84}, {0x0E86, 0x0E8A},
|
||||
{0x0E8C, 0x0EA3}, {0x0EA5, 0x0EA5}, {0x0EA7, 0x0EBD},
|
||||
{0x0EC0, 0x0EC4}, {0x0EC6, 0x0EC6}, {0x0EC8, 0x0ECD},
|
||||
{0x0ED0, 0x0ED9}, {0x0EDC, 0x0EDF}, {0x0F00, 0x0F47},
|
||||
{0x0F49, 0x0F6C}, {0x0F71, 0x0F97}, {0x0F99, 0x0FBC},
|
||||
{0x0FBE, 0x0FCC}, {0x0FCE, 0x0FDA}, {0x1000, 0x10C5},
|
||||
{0x10C7, 0x10C7}, {0x10CD, 0x10CD}, {0x10D0, 0x10FF},
|
||||
{0x1160, 0x1248}, {0x124A, 0x124D}, {0x1250, 0x1256},
|
||||
{0x1258, 0x1258}, {0x125A, 0x125D}, {0x1260, 0x1288},
|
||||
{0x128A, 0x128D}, {0x1290, 0x12B0}, {0x12B2, 0x12B5},
|
||||
{0x12B8, 0x12BE}, {0x12C0, 0x12C0}, {0x12C2, 0x12C5},
|
||||
{0x12C8, 0x12D6}, {0x12D8, 0x1310}, {0x1312, 0x1315},
|
||||
{0x1318, 0x135A}, {0x135D, 0x137C}, {0x1380, 0x1399},
|
||||
{0x13A0, 0x13F5}, {0x13F8, 0x13FD}, {0x1400, 0x169C},
|
||||
{0x16A0, 0x16F8}, {0x1700, 0x170C}, {0x170E, 0x1714},
|
||||
{0x1720, 0x1736}, {0x1740, 0x1753}, {0x1760, 0x176C},
|
||||
{0x176E, 0x1770}, {0x1772, 0x1773}, {0x1780, 0x17DD},
|
||||
{0x17E0, 0x17E9}, {0x17F0, 0x17F9}, {0x1800, 0x180E},
|
||||
{0x1810, 0x1819}, {0x1820, 0x1878}, {0x1880, 0x18AA},
|
||||
{0x18B0, 0x18F5}, {0x1900, 0x191E}, {0x1920, 0x192B},
|
||||
{0x1930, 0x193B}, {0x1940, 0x1940}, {0x1944, 0x196D},
|
||||
{0x1970, 0x1974}, {0x1980, 0x19AB}, {0x19B0, 0x19C9},
|
||||
{0x19D0, 0x19DA}, {0x19DE, 0x1A1B}, {0x1A1E, 0x1A5E},
|
||||
{0x1A60, 0x1A7C}, {0x1A7F, 0x1A89}, {0x1A90, 0x1A99},
|
||||
{0x1AA0, 0x1AAD}, {0x1AB0, 0x1ABE}, {0x1B00, 0x1B4B},
|
||||
{0x1B50, 0x1B7C}, {0x1B80, 0x1BF3}, {0x1BFC, 0x1C37},
|
||||
{0x1C3B, 0x1C49}, {0x1C4D, 0x1C88}, {0x1C90, 0x1CBA},
|
||||
{0x1CBD, 0x1CC7}, {0x1CD0, 0x1CFA}, {0x1D00, 0x1DF9},
|
||||
{0x1DFB, 0x1F15}, {0x1F18, 0x1F1D}, {0x1F20, 0x1F45},
|
||||
{0x1F48, 0x1F4D}, {0x1F50, 0x1F57}, {0x1F59, 0x1F59},
|
||||
{0x1F5B, 0x1F5B}, {0x1F5D, 0x1F5D}, {0x1F5F, 0x1F7D},
|
||||
{0x1F80, 0x1FB4}, {0x1FB6, 0x1FC4}, {0x1FC6, 0x1FD3},
|
||||
{0x1FD6, 0x1FDB}, {0x1FDD, 0x1FEF}, {0x1FF2, 0x1FF4},
|
||||
{0x1FF6, 0x1FFE}, {0x2000, 0x200F}, {0x2011, 0x2012},
|
||||
{0x2017, 0x2017}, {0x201A, 0x201B}, {0x201E, 0x201F},
|
||||
{0x2023, 0x2023}, {0x2028, 0x202F}, {0x2031, 0x2031},
|
||||
{0x2034, 0x2034}, {0x2036, 0x203A}, {0x203C, 0x203D},
|
||||
{0x203F, 0x2064}, {0x2066, 0x2071}, {0x2075, 0x207E},
|
||||
{0x2080, 0x2080}, {0x2085, 0x208E}, {0x2090, 0x209C},
|
||||
{0x20A0, 0x20A8}, {0x20AA, 0x20AB}, {0x20AD, 0x20BF},
|
||||
{0x20D0, 0x20F0}, {0x2100, 0x2102}, {0x2104, 0x2104},
|
||||
{0x2106, 0x2108}, {0x210A, 0x2112}, {0x2114, 0x2115},
|
||||
{0x2117, 0x2120}, {0x2123, 0x2125}, {0x2127, 0x212A},
|
||||
{0x212C, 0x2152}, {0x2155, 0x215A}, {0x215F, 0x215F},
|
||||
{0x216C, 0x216F}, {0x217A, 0x2188}, {0x218A, 0x218B},
|
||||
{0x219A, 0x21B7}, {0x21BA, 0x21D1}, {0x21D3, 0x21D3},
|
||||
{0x21D5, 0x21E6}, {0x21E8, 0x21FF}, {0x2201, 0x2201},
|
||||
{0x2204, 0x2206}, {0x2209, 0x220A}, {0x220C, 0x220E},
|
||||
{0x2210, 0x2210}, {0x2212, 0x2214}, {0x2216, 0x2219},
|
||||
{0x221B, 0x221C}, {0x2221, 0x2222}, {0x2224, 0x2224},
|
||||
{0x2226, 0x2226}, {0x222D, 0x222D}, {0x222F, 0x2233},
|
||||
{0x2238, 0x223B}, {0x223E, 0x2247}, {0x2249, 0x224B},
|
||||
{0x224D, 0x2251}, {0x2253, 0x225F}, {0x2262, 0x2263},
|
||||
{0x2268, 0x2269}, {0x226C, 0x226D}, {0x2270, 0x2281},
|
||||
{0x2284, 0x2285}, {0x2288, 0x2294}, {0x2296, 0x2298},
|
||||
{0x229A, 0x22A4}, {0x22A6, 0x22BE}, {0x22C0, 0x2311},
|
||||
{0x2313, 0x2319}, {0x231C, 0x2328}, {0x232B, 0x23E8},
|
||||
{0x23ED, 0x23EF}, {0x23F1, 0x23F2}, {0x23F4, 0x2426},
|
||||
{0x2440, 0x244A}, {0x24EA, 0x24EA}, {0x254C, 0x254F},
|
||||
{0x2574, 0x257F}, {0x2590, 0x2591}, {0x2596, 0x259F},
|
||||
{0x25A2, 0x25A2}, {0x25AA, 0x25B1}, {0x25B4, 0x25B5},
|
||||
{0x25B8, 0x25BB}, {0x25BE, 0x25BF}, {0x25C2, 0x25C5},
|
||||
{0x25C9, 0x25CA}, {0x25CC, 0x25CD}, {0x25D2, 0x25E1},
|
||||
{0x25E6, 0x25EE}, {0x25F0, 0x25FC}, {0x25FF, 0x2604},
|
||||
{0x2607, 0x2608}, {0x260A, 0x260D}, {0x2610, 0x2613},
|
||||
{0x2616, 0x261B}, {0x261D, 0x261D}, {0x261F, 0x263F},
|
||||
{0x2641, 0x2641}, {0x2643, 0x2647}, {0x2654, 0x265F},
|
||||
{0x2662, 0x2662}, {0x2666, 0x2666}, {0x266B, 0x266B},
|
||||
{0x266E, 0x266E}, {0x2670, 0x267E}, {0x2680, 0x2692},
|
||||
{0x2694, 0x269D}, {0x26A0, 0x26A0}, {0x26A2, 0x26A9},
|
||||
{0x26AC, 0x26BC}, {0x26C0, 0x26C3}, {0x26E2, 0x26E2},
|
||||
{0x26E4, 0x26E7}, {0x2700, 0x2704}, {0x2706, 0x2709},
|
||||
{0x270C, 0x2727}, {0x2729, 0x273C}, {0x273E, 0x274B},
|
||||
{0x274D, 0x274D}, {0x274F, 0x2752}, {0x2756, 0x2756},
|
||||
{0x2758, 0x2775}, {0x2780, 0x2794}, {0x2798, 0x27AF},
|
||||
{0x27B1, 0x27BE}, {0x27C0, 0x27E5}, {0x27EE, 0x2984},
|
||||
{0x2987, 0x2B1A}, {0x2B1D, 0x2B4F}, {0x2B51, 0x2B54},
|
||||
{0x2B5A, 0x2B73}, {0x2B76, 0x2B95}, {0x2B98, 0x2C2E},
|
||||
{0x2C30, 0x2C5E}, {0x2C60, 0x2CF3}, {0x2CF9, 0x2D25},
|
||||
{0x2D27, 0x2D27}, {0x2D2D, 0x2D2D}, {0x2D30, 0x2D67},
|
||||
{0x2D6F, 0x2D70}, {0x2D7F, 0x2D96}, {0x2DA0, 0x2DA6},
|
||||
{0x2DA8, 0x2DAE}, {0x2DB0, 0x2DB6}, {0x2DB8, 0x2DBE},
|
||||
{0x2DC0, 0x2DC6}, {0x2DC8, 0x2DCE}, {0x2DD0, 0x2DD6},
|
||||
{0x2DD8, 0x2DDE}, {0x2DE0, 0x2E4F}, {0x303F, 0x303F},
|
||||
{0x4DC0, 0x4DFF}, {0xA4D0, 0xA62B}, {0xA640, 0xA6F7},
|
||||
{0xA700, 0xA7BF}, {0xA7C2, 0xA7C6}, {0xA7F7, 0xA82B},
|
||||
{0xA830, 0xA839}, {0xA840, 0xA877}, {0xA880, 0xA8C5},
|
||||
{0xA8CE, 0xA8D9}, {0xA8E0, 0xA953}, {0xA95F, 0xA95F},
|
||||
{0xA980, 0xA9CD}, {0xA9CF, 0xA9D9}, {0xA9DE, 0xA9FE},
|
||||
{0xAA00, 0xAA36}, {0xAA40, 0xAA4D}, {0xAA50, 0xAA59},
|
||||
{0xAA5C, 0xAAC2}, {0xAADB, 0xAAF6}, {0xAB01, 0xAB06},
|
||||
{0xAB09, 0xAB0E}, {0xAB11, 0xAB16}, {0xAB20, 0xAB26},
|
||||
{0xAB28, 0xAB2E}, {0xAB30, 0xAB67}, {0xAB70, 0xABED},
|
||||
{0xABF0, 0xABF9}, {0xD7B0, 0xD7C6}, {0xD7CB, 0xD7FB},
|
||||
{0xD800, 0xDFFF}, {0xFB00, 0xFB06}, {0xFB13, 0xFB17},
|
||||
{0xFB1D, 0xFB36}, {0xFB38, 0xFB3C}, {0xFB3E, 0xFB3E},
|
||||
{0xFB40, 0xFB41}, {0xFB43, 0xFB44}, {0xFB46, 0xFBC1},
|
||||
{0xFBD3, 0xFD3F}, {0xFD50, 0xFD8F}, {0xFD92, 0xFDC7},
|
||||
{0xFDF0, 0xFDFD}, {0xFE20, 0xFE2F}, {0xFE70, 0xFE74},
|
||||
{0xFE76, 0xFEFC}, {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFC},
|
||||
{0x10000, 0x1000B}, {0x1000D, 0x10026}, {0x10028, 0x1003A},
|
||||
{0x1003C, 0x1003D}, {0x1003F, 0x1004D}, {0x10050, 0x1005D},
|
||||
{0x10080, 0x100FA}, {0x10100, 0x10102}, {0x10107, 0x10133},
|
||||
{0x10137, 0x1018E}, {0x10190, 0x1019B}, {0x101A0, 0x101A0},
|
||||
{0x101D0, 0x101FD}, {0x10280, 0x1029C}, {0x102A0, 0x102D0},
|
||||
{0x102E0, 0x102FB}, {0x10300, 0x10323}, {0x1032D, 0x1034A},
|
||||
{0x10350, 0x1037A}, {0x10380, 0x1039D}, {0x1039F, 0x103C3},
|
||||
{0x103C8, 0x103D5}, {0x10400, 0x1049D}, {0x104A0, 0x104A9},
|
||||
{0x104B0, 0x104D3}, {0x104D8, 0x104FB}, {0x10500, 0x10527},
|
||||
{0x10530, 0x10563}, {0x1056F, 0x1056F}, {0x10600, 0x10736},
|
||||
{0x10740, 0x10755}, {0x10760, 0x10767}, {0x10800, 0x10805},
|
||||
{0x10808, 0x10808}, {0x1080A, 0x10835}, {0x10837, 0x10838},
|
||||
{0x1083C, 0x1083C}, {0x1083F, 0x10855}, {0x10857, 0x1089E},
|
||||
{0x108A7, 0x108AF}, {0x108E0, 0x108F2}, {0x108F4, 0x108F5},
|
||||
{0x108FB, 0x1091B}, {0x1091F, 0x10939}, {0x1093F, 0x1093F},
|
||||
{0x10980, 0x109B7}, {0x109BC, 0x109CF}, {0x109D2, 0x10A03},
|
||||
{0x10A05, 0x10A06}, {0x10A0C, 0x10A13}, {0x10A15, 0x10A17},
|
||||
{0x10A19, 0x10A35}, {0x10A38, 0x10A3A}, {0x10A3F, 0x10A48},
|
||||
{0x10A50, 0x10A58}, {0x10A60, 0x10A9F}, {0x10AC0, 0x10AE6},
|
||||
{0x10AEB, 0x10AF6}, {0x10B00, 0x10B35}, {0x10B39, 0x10B55},
|
||||
{0x10B58, 0x10B72}, {0x10B78, 0x10B91}, {0x10B99, 0x10B9C},
|
||||
{0x10BA9, 0x10BAF}, {0x10C00, 0x10C48}, {0x10C80, 0x10CB2},
|
||||
{0x10CC0, 0x10CF2}, {0x10CFA, 0x10D27}, {0x10D30, 0x10D39},
|
||||
{0x10E60, 0x10E7E}, {0x10F00, 0x10F27}, {0x10F30, 0x10F59},
|
||||
{0x0CE6, 0x0CEF}, {0x0CF1, 0x0CF2}, {0x0D00, 0x0D0C},
|
||||
{0x0D0E, 0x0D10}, {0x0D12, 0x0D44}, {0x0D46, 0x0D48},
|
||||
{0x0D4A, 0x0D4F}, {0x0D54, 0x0D63}, {0x0D66, 0x0D7F},
|
||||
{0x0D81, 0x0D83}, {0x0D85, 0x0D96}, {0x0D9A, 0x0DB1},
|
||||
{0x0DB3, 0x0DBB}, {0x0DBD, 0x0DBD}, {0x0DC0, 0x0DC6},
|
||||
{0x0DCA, 0x0DCA}, {0x0DCF, 0x0DD4}, {0x0DD6, 0x0DD6},
|
||||
{0x0DD8, 0x0DDF}, {0x0DE6, 0x0DEF}, {0x0DF2, 0x0DF4},
|
||||
{0x0E01, 0x0E3A}, {0x0E3F, 0x0E5B}, {0x0E81, 0x0E82},
|
||||
{0x0E84, 0x0E84}, {0x0E86, 0x0E8A}, {0x0E8C, 0x0EA3},
|
||||
{0x0EA5, 0x0EA5}, {0x0EA7, 0x0EBD}, {0x0EC0, 0x0EC4},
|
||||
{0x0EC6, 0x0EC6}, {0x0EC8, 0x0ECD}, {0x0ED0, 0x0ED9},
|
||||
{0x0EDC, 0x0EDF}, {0x0F00, 0x0F47}, {0x0F49, 0x0F6C},
|
||||
{0x0F71, 0x0F97}, {0x0F99, 0x0FBC}, {0x0FBE, 0x0FCC},
|
||||
{0x0FCE, 0x0FDA}, {0x1000, 0x10C5}, {0x10C7, 0x10C7},
|
||||
{0x10CD, 0x10CD}, {0x10D0, 0x10FF}, {0x1160, 0x1248},
|
||||
{0x124A, 0x124D}, {0x1250, 0x1256}, {0x1258, 0x1258},
|
||||
{0x125A, 0x125D}, {0x1260, 0x1288}, {0x128A, 0x128D},
|
||||
{0x1290, 0x12B0}, {0x12B2, 0x12B5}, {0x12B8, 0x12BE},
|
||||
{0x12C0, 0x12C0}, {0x12C2, 0x12C5}, {0x12C8, 0x12D6},
|
||||
{0x12D8, 0x1310}, {0x1312, 0x1315}, {0x1318, 0x135A},
|
||||
{0x135D, 0x137C}, {0x1380, 0x1399}, {0x13A0, 0x13F5},
|
||||
{0x13F8, 0x13FD}, {0x1400, 0x169C}, {0x16A0, 0x16F8},
|
||||
{0x1700, 0x170C}, {0x170E, 0x1714}, {0x1720, 0x1736},
|
||||
{0x1740, 0x1753}, {0x1760, 0x176C}, {0x176E, 0x1770},
|
||||
{0x1772, 0x1773}, {0x1780, 0x17DD}, {0x17E0, 0x17E9},
|
||||
{0x17F0, 0x17F9}, {0x1800, 0x180E}, {0x1810, 0x1819},
|
||||
{0x1820, 0x1878}, {0x1880, 0x18AA}, {0x18B0, 0x18F5},
|
||||
{0x1900, 0x191E}, {0x1920, 0x192B}, {0x1930, 0x193B},
|
||||
{0x1940, 0x1940}, {0x1944, 0x196D}, {0x1970, 0x1974},
|
||||
{0x1980, 0x19AB}, {0x19B0, 0x19C9}, {0x19D0, 0x19DA},
|
||||
{0x19DE, 0x1A1B}, {0x1A1E, 0x1A5E}, {0x1A60, 0x1A7C},
|
||||
{0x1A7F, 0x1A89}, {0x1A90, 0x1A99}, {0x1AA0, 0x1AAD},
|
||||
{0x1AB0, 0x1AC0}, {0x1B00, 0x1B4B}, {0x1B50, 0x1B7C},
|
||||
{0x1B80, 0x1BF3}, {0x1BFC, 0x1C37}, {0x1C3B, 0x1C49},
|
||||
{0x1C4D, 0x1C88}, {0x1C90, 0x1CBA}, {0x1CBD, 0x1CC7},
|
||||
{0x1CD0, 0x1CFA}, {0x1D00, 0x1DF9}, {0x1DFB, 0x1F15},
|
||||
{0x1F18, 0x1F1D}, {0x1F20, 0x1F45}, {0x1F48, 0x1F4D},
|
||||
{0x1F50, 0x1F57}, {0x1F59, 0x1F59}, {0x1F5B, 0x1F5B},
|
||||
{0x1F5D, 0x1F5D}, {0x1F5F, 0x1F7D}, {0x1F80, 0x1FB4},
|
||||
{0x1FB6, 0x1FC4}, {0x1FC6, 0x1FD3}, {0x1FD6, 0x1FDB},
|
||||
{0x1FDD, 0x1FEF}, {0x1FF2, 0x1FF4}, {0x1FF6, 0x1FFE},
|
||||
{0x2000, 0x200F}, {0x2011, 0x2012}, {0x2017, 0x2017},
|
||||
{0x201A, 0x201B}, {0x201E, 0x201F}, {0x2023, 0x2023},
|
||||
{0x2028, 0x202F}, {0x2031, 0x2031}, {0x2034, 0x2034},
|
||||
{0x2036, 0x203A}, {0x203C, 0x203D}, {0x203F, 0x2064},
|
||||
{0x2066, 0x2071}, {0x2075, 0x207E}, {0x2080, 0x2080},
|
||||
{0x2085, 0x208E}, {0x2090, 0x209C}, {0x20A0, 0x20A8},
|
||||
{0x20AA, 0x20AB}, {0x20AD, 0x20BF}, {0x20D0, 0x20F0},
|
||||
{0x2100, 0x2102}, {0x2104, 0x2104}, {0x2106, 0x2108},
|
||||
{0x210A, 0x2112}, {0x2114, 0x2115}, {0x2117, 0x2120},
|
||||
{0x2123, 0x2125}, {0x2127, 0x212A}, {0x212C, 0x2152},
|
||||
{0x2155, 0x215A}, {0x215F, 0x215F}, {0x216C, 0x216F},
|
||||
{0x217A, 0x2188}, {0x218A, 0x218B}, {0x219A, 0x21B7},
|
||||
{0x21BA, 0x21D1}, {0x21D3, 0x21D3}, {0x21D5, 0x21E6},
|
||||
{0x21E8, 0x21FF}, {0x2201, 0x2201}, {0x2204, 0x2206},
|
||||
{0x2209, 0x220A}, {0x220C, 0x220E}, {0x2210, 0x2210},
|
||||
{0x2212, 0x2214}, {0x2216, 0x2219}, {0x221B, 0x221C},
|
||||
{0x2221, 0x2222}, {0x2224, 0x2224}, {0x2226, 0x2226},
|
||||
{0x222D, 0x222D}, {0x222F, 0x2233}, {0x2238, 0x223B},
|
||||
{0x223E, 0x2247}, {0x2249, 0x224B}, {0x224D, 0x2251},
|
||||
{0x2253, 0x225F}, {0x2262, 0x2263}, {0x2268, 0x2269},
|
||||
{0x226C, 0x226D}, {0x2270, 0x2281}, {0x2284, 0x2285},
|
||||
{0x2288, 0x2294}, {0x2296, 0x2298}, {0x229A, 0x22A4},
|
||||
{0x22A6, 0x22BE}, {0x22C0, 0x2311}, {0x2313, 0x2319},
|
||||
{0x231C, 0x2328}, {0x232B, 0x23E8}, {0x23ED, 0x23EF},
|
||||
{0x23F1, 0x23F2}, {0x23F4, 0x2426}, {0x2440, 0x244A},
|
||||
{0x24EA, 0x24EA}, {0x254C, 0x254F}, {0x2574, 0x257F},
|
||||
{0x2590, 0x2591}, {0x2596, 0x259F}, {0x25A2, 0x25A2},
|
||||
{0x25AA, 0x25B1}, {0x25B4, 0x25B5}, {0x25B8, 0x25BB},
|
||||
{0x25BE, 0x25BF}, {0x25C2, 0x25C5}, {0x25C9, 0x25CA},
|
||||
{0x25CC, 0x25CD}, {0x25D2, 0x25E1}, {0x25E6, 0x25EE},
|
||||
{0x25F0, 0x25FC}, {0x25FF, 0x2604}, {0x2607, 0x2608},
|
||||
{0x260A, 0x260D}, {0x2610, 0x2613}, {0x2616, 0x261B},
|
||||
{0x261D, 0x261D}, {0x261F, 0x263F}, {0x2641, 0x2641},
|
||||
{0x2643, 0x2647}, {0x2654, 0x265F}, {0x2662, 0x2662},
|
||||
{0x2666, 0x2666}, {0x266B, 0x266B}, {0x266E, 0x266E},
|
||||
{0x2670, 0x267E}, {0x2680, 0x2692}, {0x2694, 0x269D},
|
||||
{0x26A0, 0x26A0}, {0x26A2, 0x26A9}, {0x26AC, 0x26BC},
|
||||
{0x26C0, 0x26C3}, {0x26E2, 0x26E2}, {0x26E4, 0x26E7},
|
||||
{0x2700, 0x2704}, {0x2706, 0x2709}, {0x270C, 0x2727},
|
||||
{0x2729, 0x273C}, {0x273E, 0x274B}, {0x274D, 0x274D},
|
||||
{0x274F, 0x2752}, {0x2756, 0x2756}, {0x2758, 0x2775},
|
||||
{0x2780, 0x2794}, {0x2798, 0x27AF}, {0x27B1, 0x27BE},
|
||||
{0x27C0, 0x27E5}, {0x27EE, 0x2984}, {0x2987, 0x2B1A},
|
||||
{0x2B1D, 0x2B4F}, {0x2B51, 0x2B54}, {0x2B5A, 0x2B73},
|
||||
{0x2B76, 0x2B95}, {0x2B97, 0x2C2E}, {0x2C30, 0x2C5E},
|
||||
{0x2C60, 0x2CF3}, {0x2CF9, 0x2D25}, {0x2D27, 0x2D27},
|
||||
{0x2D2D, 0x2D2D}, {0x2D30, 0x2D67}, {0x2D6F, 0x2D70},
|
||||
{0x2D7F, 0x2D96}, {0x2DA0, 0x2DA6}, {0x2DA8, 0x2DAE},
|
||||
{0x2DB0, 0x2DB6}, {0x2DB8, 0x2DBE}, {0x2DC0, 0x2DC6},
|
||||
{0x2DC8, 0x2DCE}, {0x2DD0, 0x2DD6}, {0x2DD8, 0x2DDE},
|
||||
{0x2DE0, 0x2E52}, {0x303F, 0x303F}, {0x4DC0, 0x4DFF},
|
||||
{0xA4D0, 0xA62B}, {0xA640, 0xA6F7}, {0xA700, 0xA7BF},
|
||||
{0xA7C2, 0xA7CA}, {0xA7F5, 0xA82C}, {0xA830, 0xA839},
|
||||
{0xA840, 0xA877}, {0xA880, 0xA8C5}, {0xA8CE, 0xA8D9},
|
||||
{0xA8E0, 0xA953}, {0xA95F, 0xA95F}, {0xA980, 0xA9CD},
|
||||
{0xA9CF, 0xA9D9}, {0xA9DE, 0xA9FE}, {0xAA00, 0xAA36},
|
||||
{0xAA40, 0xAA4D}, {0xAA50, 0xAA59}, {0xAA5C, 0xAAC2},
|
||||
{0xAADB, 0xAAF6}, {0xAB01, 0xAB06}, {0xAB09, 0xAB0E},
|
||||
{0xAB11, 0xAB16}, {0xAB20, 0xAB26}, {0xAB28, 0xAB2E},
|
||||
{0xAB30, 0xAB6B}, {0xAB70, 0xABED}, {0xABF0, 0xABF9},
|
||||
{0xD7B0, 0xD7C6}, {0xD7CB, 0xD7FB}, {0xD800, 0xDFFF},
|
||||
{0xFB00, 0xFB06}, {0xFB13, 0xFB17}, {0xFB1D, 0xFB36},
|
||||
{0xFB38, 0xFB3C}, {0xFB3E, 0xFB3E}, {0xFB40, 0xFB41},
|
||||
{0xFB43, 0xFB44}, {0xFB46, 0xFBC1}, {0xFBD3, 0xFD3F},
|
||||
{0xFD50, 0xFD8F}, {0xFD92, 0xFDC7}, {0xFDF0, 0xFDFD},
|
||||
{0xFE20, 0xFE2F}, {0xFE70, 0xFE74}, {0xFE76, 0xFEFC},
|
||||
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFC}, {0x10000, 0x1000B},
|
||||
{0x1000D, 0x10026}, {0x10028, 0x1003A}, {0x1003C, 0x1003D},
|
||||
{0x1003F, 0x1004D}, {0x10050, 0x1005D}, {0x10080, 0x100FA},
|
||||
{0x10100, 0x10102}, {0x10107, 0x10133}, {0x10137, 0x1018E},
|
||||
{0x10190, 0x1019C}, {0x101A0, 0x101A0}, {0x101D0, 0x101FD},
|
||||
{0x10280, 0x1029C}, {0x102A0, 0x102D0}, {0x102E0, 0x102FB},
|
||||
{0x10300, 0x10323}, {0x1032D, 0x1034A}, {0x10350, 0x1037A},
|
||||
{0x10380, 0x1039D}, {0x1039F, 0x103C3}, {0x103C8, 0x103D5},
|
||||
{0x10400, 0x1049D}, {0x104A0, 0x104A9}, {0x104B0, 0x104D3},
|
||||
{0x104D8, 0x104FB}, {0x10500, 0x10527}, {0x10530, 0x10563},
|
||||
{0x1056F, 0x1056F}, {0x10600, 0x10736}, {0x10740, 0x10755},
|
||||
{0x10760, 0x10767}, {0x10800, 0x10805}, {0x10808, 0x10808},
|
||||
{0x1080A, 0x10835}, {0x10837, 0x10838}, {0x1083C, 0x1083C},
|
||||
{0x1083F, 0x10855}, {0x10857, 0x1089E}, {0x108A7, 0x108AF},
|
||||
{0x108E0, 0x108F2}, {0x108F4, 0x108F5}, {0x108FB, 0x1091B},
|
||||
{0x1091F, 0x10939}, {0x1093F, 0x1093F}, {0x10980, 0x109B7},
|
||||
{0x109BC, 0x109CF}, {0x109D2, 0x10A03}, {0x10A05, 0x10A06},
|
||||
{0x10A0C, 0x10A13}, {0x10A15, 0x10A17}, {0x10A19, 0x10A35},
|
||||
{0x10A38, 0x10A3A}, {0x10A3F, 0x10A48}, {0x10A50, 0x10A58},
|
||||
{0x10A60, 0x10A9F}, {0x10AC0, 0x10AE6}, {0x10AEB, 0x10AF6},
|
||||
{0x10B00, 0x10B35}, {0x10B39, 0x10B55}, {0x10B58, 0x10B72},
|
||||
{0x10B78, 0x10B91}, {0x10B99, 0x10B9C}, {0x10BA9, 0x10BAF},
|
||||
{0x10C00, 0x10C48}, {0x10C80, 0x10CB2}, {0x10CC0, 0x10CF2},
|
||||
{0x10CFA, 0x10D27}, {0x10D30, 0x10D39}, {0x10E60, 0x10E7E},
|
||||
{0x10E80, 0x10EA9}, {0x10EAB, 0x10EAD}, {0x10EB0, 0x10EB1},
|
||||
{0x10F00, 0x10F27}, {0x10F30, 0x10F59}, {0x10FB0, 0x10FCB},
|
||||
{0x10FE0, 0x10FF6}, {0x11000, 0x1104D}, {0x11052, 0x1106F},
|
||||
{0x1107F, 0x110C1}, {0x110CD, 0x110CD}, {0x110D0, 0x110E8},
|
||||
{0x110F0, 0x110F9}, {0x11100, 0x11134}, {0x11136, 0x11146},
|
||||
{0x11150, 0x11176}, {0x11180, 0x111CD}, {0x111D0, 0x111DF},
|
||||
{0x111E1, 0x111F4}, {0x11200, 0x11211}, {0x11213, 0x1123E},
|
||||
{0x11280, 0x11286}, {0x11288, 0x11288}, {0x1128A, 0x1128D},
|
||||
{0x1128F, 0x1129D}, {0x1129F, 0x112A9}, {0x112B0, 0x112EA},
|
||||
{0x112F0, 0x112F9}, {0x11300, 0x11303}, {0x11305, 0x1130C},
|
||||
{0x1130F, 0x11310}, {0x11313, 0x11328}, {0x1132A, 0x11330},
|
||||
{0x11332, 0x11333}, {0x11335, 0x11339}, {0x1133B, 0x11344},
|
||||
{0x11347, 0x11348}, {0x1134B, 0x1134D}, {0x11350, 0x11350},
|
||||
{0x11357, 0x11357}, {0x1135D, 0x11363}, {0x11366, 0x1136C},
|
||||
{0x11370, 0x11374}, {0x11400, 0x11459}, {0x1145B, 0x1145B},
|
||||
{0x1145D, 0x1145F}, {0x11480, 0x114C7}, {0x114D0, 0x114D9},
|
||||
{0x11580, 0x115B5}, {0x115B8, 0x115DD}, {0x11600, 0x11644},
|
||||
{0x11650, 0x11659}, {0x11660, 0x1166C}, {0x11680, 0x116B8},
|
||||
{0x116C0, 0x116C9}, {0x11700, 0x1171A}, {0x1171D, 0x1172B},
|
||||
{0x11730, 0x1173F}, {0x11800, 0x1183B}, {0x118A0, 0x118F2},
|
||||
{0x118FF, 0x118FF}, {0x119A0, 0x119A7}, {0x119AA, 0x119D7},
|
||||
{0x119DA, 0x119E4}, {0x11A00, 0x11A47}, {0x11A50, 0x11AA2},
|
||||
{0x11AC0, 0x11AF8}, {0x11C00, 0x11C08}, {0x11C0A, 0x11C36},
|
||||
{0x11C38, 0x11C45}, {0x11C50, 0x11C6C}, {0x11C70, 0x11C8F},
|
||||
{0x11C92, 0x11CA7}, {0x11CA9, 0x11CB6}, {0x11D00, 0x11D06},
|
||||
{0x11D08, 0x11D09}, {0x11D0B, 0x11D36}, {0x11D3A, 0x11D3A},
|
||||
{0x11D3C, 0x11D3D}, {0x11D3F, 0x11D47}, {0x11D50, 0x11D59},
|
||||
{0x11D60, 0x11D65}, {0x11D67, 0x11D68}, {0x11D6A, 0x11D8E},
|
||||
{0x11D90, 0x11D91}, {0x11D93, 0x11D98}, {0x11DA0, 0x11DA9},
|
||||
{0x11EE0, 0x11EF8}, {0x11FC0, 0x11FF1}, {0x11FFF, 0x12399},
|
||||
{0x110F0, 0x110F9}, {0x11100, 0x11134}, {0x11136, 0x11147},
|
||||
{0x11150, 0x11176}, {0x11180, 0x111DF}, {0x111E1, 0x111F4},
|
||||
{0x11200, 0x11211}, {0x11213, 0x1123E}, {0x11280, 0x11286},
|
||||
{0x11288, 0x11288}, {0x1128A, 0x1128D}, {0x1128F, 0x1129D},
|
||||
{0x1129F, 0x112A9}, {0x112B0, 0x112EA}, {0x112F0, 0x112F9},
|
||||
{0x11300, 0x11303}, {0x11305, 0x1130C}, {0x1130F, 0x11310},
|
||||
{0x11313, 0x11328}, {0x1132A, 0x11330}, {0x11332, 0x11333},
|
||||
{0x11335, 0x11339}, {0x1133B, 0x11344}, {0x11347, 0x11348},
|
||||
{0x1134B, 0x1134D}, {0x11350, 0x11350}, {0x11357, 0x11357},
|
||||
{0x1135D, 0x11363}, {0x11366, 0x1136C}, {0x11370, 0x11374},
|
||||
{0x11400, 0x1145B}, {0x1145D, 0x11461}, {0x11480, 0x114C7},
|
||||
{0x114D0, 0x114D9}, {0x11580, 0x115B5}, {0x115B8, 0x115DD},
|
||||
{0x11600, 0x11644}, {0x11650, 0x11659}, {0x11660, 0x1166C},
|
||||
{0x11680, 0x116B8}, {0x116C0, 0x116C9}, {0x11700, 0x1171A},
|
||||
{0x1171D, 0x1172B}, {0x11730, 0x1173F}, {0x11800, 0x1183B},
|
||||
{0x118A0, 0x118F2}, {0x118FF, 0x11906}, {0x11909, 0x11909},
|
||||
{0x1190C, 0x11913}, {0x11915, 0x11916}, {0x11918, 0x11935},
|
||||
{0x11937, 0x11938}, {0x1193B, 0x11946}, {0x11950, 0x11959},
|
||||
{0x119A0, 0x119A7}, {0x119AA, 0x119D7}, {0x119DA, 0x119E4},
|
||||
{0x11A00, 0x11A47}, {0x11A50, 0x11AA2}, {0x11AC0, 0x11AF8},
|
||||
{0x11C00, 0x11C08}, {0x11C0A, 0x11C36}, {0x11C38, 0x11C45},
|
||||
{0x11C50, 0x11C6C}, {0x11C70, 0x11C8F}, {0x11C92, 0x11CA7},
|
||||
{0x11CA9, 0x11CB6}, {0x11D00, 0x11D06}, {0x11D08, 0x11D09},
|
||||
{0x11D0B, 0x11D36}, {0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D},
|
||||
{0x11D3F, 0x11D47}, {0x11D50, 0x11D59}, {0x11D60, 0x11D65},
|
||||
{0x11D67, 0x11D68}, {0x11D6A, 0x11D8E}, {0x11D90, 0x11D91},
|
||||
{0x11D93, 0x11D98}, {0x11DA0, 0x11DA9}, {0x11EE0, 0x11EF8},
|
||||
{0x11FB0, 0x11FB0}, {0x11FC0, 0x11FF1}, {0x11FFF, 0x12399},
|
||||
{0x12400, 0x1246E}, {0x12470, 0x12474}, {0x12480, 0x12543},
|
||||
{0x13000, 0x1342E}, {0x13430, 0x13438}, {0x14400, 0x14646},
|
||||
{0x16800, 0x16A38}, {0x16A40, 0x16A5E}, {0x16A60, 0x16A69},
|
||||
@@ -382,20 +389,22 @@ var neutral = table{
|
||||
{0x1EEA5, 0x1EEA9}, {0x1EEAB, 0x1EEBB}, {0x1EEF0, 0x1EEF1},
|
||||
{0x1F000, 0x1F003}, {0x1F005, 0x1F02B}, {0x1F030, 0x1F093},
|
||||
{0x1F0A0, 0x1F0AE}, {0x1F0B1, 0x1F0BF}, {0x1F0C1, 0x1F0CE},
|
||||
{0x1F0D1, 0x1F0F5}, {0x1F10B, 0x1F10C}, {0x1F12E, 0x1F12F},
|
||||
{0x1F16A, 0x1F16C}, {0x1F1E6, 0x1F1FF}, {0x1F321, 0x1F32C},
|
||||
{0x1F336, 0x1F336}, {0x1F37D, 0x1F37D}, {0x1F394, 0x1F39F},
|
||||
{0x1F3CB, 0x1F3CE}, {0x1F3D4, 0x1F3DF}, {0x1F3F1, 0x1F3F3},
|
||||
{0x1F3F5, 0x1F3F7}, {0x1F43F, 0x1F43F}, {0x1F441, 0x1F441},
|
||||
{0x1F4FD, 0x1F4FE}, {0x1F53E, 0x1F54A}, {0x1F54F, 0x1F54F},
|
||||
{0x1F568, 0x1F579}, {0x1F57B, 0x1F594}, {0x1F597, 0x1F5A3},
|
||||
{0x1F5A5, 0x1F5FA}, {0x1F650, 0x1F67F}, {0x1F6C6, 0x1F6CB},
|
||||
{0x1F6CD, 0x1F6CF}, {0x1F6D3, 0x1F6D4}, {0x1F6E0, 0x1F6EA},
|
||||
{0x1F6F0, 0x1F6F3}, {0x1F700, 0x1F773}, {0x1F780, 0x1F7D8},
|
||||
{0x1F800, 0x1F80B}, {0x1F810, 0x1F847}, {0x1F850, 0x1F859},
|
||||
{0x1F860, 0x1F887}, {0x1F890, 0x1F8AD}, {0x1F900, 0x1F90B},
|
||||
{0x1FA00, 0x1FA53}, {0x1FA60, 0x1FA6D}, {0xE0001, 0xE0001},
|
||||
{0xE0020, 0xE007F},
|
||||
{0x1F0D1, 0x1F0F5}, {0x1F10B, 0x1F10F}, {0x1F12E, 0x1F12F},
|
||||
{0x1F16A, 0x1F16F}, {0x1F1AD, 0x1F1AD}, {0x1F1E6, 0x1F1FF},
|
||||
{0x1F321, 0x1F32C}, {0x1F336, 0x1F336}, {0x1F37D, 0x1F37D},
|
||||
{0x1F394, 0x1F39F}, {0x1F3CB, 0x1F3CE}, {0x1F3D4, 0x1F3DF},
|
||||
{0x1F3F1, 0x1F3F3}, {0x1F3F5, 0x1F3F7}, {0x1F43F, 0x1F43F},
|
||||
{0x1F441, 0x1F441}, {0x1F4FD, 0x1F4FE}, {0x1F53E, 0x1F54A},
|
||||
{0x1F54F, 0x1F54F}, {0x1F568, 0x1F579}, {0x1F57B, 0x1F594},
|
||||
{0x1F597, 0x1F5A3}, {0x1F5A5, 0x1F5FA}, {0x1F650, 0x1F67F},
|
||||
{0x1F6C6, 0x1F6CB}, {0x1F6CD, 0x1F6CF}, {0x1F6D3, 0x1F6D4},
|
||||
{0x1F6E0, 0x1F6EA}, {0x1F6F0, 0x1F6F3}, {0x1F700, 0x1F773},
|
||||
{0x1F780, 0x1F7D8}, {0x1F800, 0x1F80B}, {0x1F810, 0x1F847},
|
||||
{0x1F850, 0x1F859}, {0x1F860, 0x1F887}, {0x1F890, 0x1F8AD},
|
||||
{0x1F8B0, 0x1F8B1}, {0x1F900, 0x1F90B}, {0x1F93B, 0x1F93B},
|
||||
{0x1F946, 0x1F946}, {0x1FA00, 0x1FA53}, {0x1FA60, 0x1FA6D},
|
||||
{0x1FB00, 0x1FB92}, {0x1FB94, 0x1FBCA}, {0x1FBF0, 0x1FBF9},
|
||||
{0xE0001, 0xE0001}, {0xE0020, 0xE007F},
|
||||
}
|
||||
|
||||
var emoji = table{
|
||||
@@ -423,5 +432,6 @@ var emoji = table{
|
||||
{0x1F546, 0x1F64F}, {0x1F680, 0x1F6FF}, {0x1F774, 0x1F77F},
|
||||
{0x1F7D5, 0x1F7FF}, {0x1F80C, 0x1F80F}, {0x1F848, 0x1F84F},
|
||||
{0x1F85A, 0x1F85F}, {0x1F888, 0x1F88F}, {0x1F8AE, 0x1F8FF},
|
||||
{0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1FFFD},
|
||||
{0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1FAFF},
|
||||
{0x1FC00, 0x1FFFD},
|
||||
}
|
||||
+335
@@ -0,0 +1,335 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package encoding defines an interface for character encodings, such as Shift
|
||||
// JIS and Windows 1252, that can convert to and from UTF-8.
|
||||
//
|
||||
// Encoding implementations are provided in other packages, such as
|
||||
// golang.org/x/text/encoding/charmap and
|
||||
// golang.org/x/text/encoding/japanese.
|
||||
package encoding // import "golang.org/x/text/encoding"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/encoding/internal/identifier"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// TODO:
|
||||
// - There seems to be some inconsistency in when decoders return errors
|
||||
// and when not. Also documentation seems to suggest they shouldn't return
|
||||
// errors at all (except for UTF-16).
|
||||
// - Encoders seem to rely on or at least benefit from the input being in NFC
|
||||
// normal form. Perhaps add an example how users could prepare their output.
|
||||
|
||||
// Encoding is a character set encoding that can be transformed to and from
|
||||
// UTF-8.
|
||||
type Encoding interface {
|
||||
// NewDecoder returns a Decoder.
|
||||
NewDecoder() *Decoder
|
||||
|
||||
// NewEncoder returns an Encoder.
|
||||
NewEncoder() *Encoder
|
||||
}
|
||||
|
||||
// A Decoder converts bytes to UTF-8. It implements transform.Transformer.
|
||||
//
|
||||
// Transforming source bytes that are not of that encoding will not result in an
|
||||
// error per se. Each byte that cannot be transcoded will be represented in the
|
||||
// output by the UTF-8 encoding of '\uFFFD', the replacement rune.
|
||||
type Decoder struct {
|
||||
transform.Transformer
|
||||
|
||||
// This forces external creators of Decoders to use names in struct
|
||||
// initializers, allowing for future extendibility without having to break
|
||||
// code.
|
||||
_ struct{}
|
||||
}
|
||||
|
||||
// Bytes converts the given encoded bytes to UTF-8. It returns the converted
|
||||
// bytes or nil, err if any error occurred.
|
||||
func (d *Decoder) Bytes(b []byte) ([]byte, error) {
|
||||
b, _, err := transform.Bytes(d, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// String converts the given encoded string to UTF-8. It returns the converted
|
||||
// string or "", err if any error occurred.
|
||||
func (d *Decoder) String(s string) (string, error) {
|
||||
s, _, err := transform.String(d, s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Reader wraps another Reader to decode its bytes.
|
||||
//
|
||||
// The Decoder may not be used for any other operation as long as the returned
|
||||
// Reader is in use.
|
||||
func (d *Decoder) Reader(r io.Reader) io.Reader {
|
||||
return transform.NewReader(r, d)
|
||||
}
|
||||
|
||||
// An Encoder converts bytes from UTF-8. It implements transform.Transformer.
|
||||
//
|
||||
// Each rune that cannot be transcoded will result in an error. In this case,
|
||||
// the transform will consume all source byte up to, not including the offending
|
||||
// rune. Transforming source bytes that are not valid UTF-8 will be replaced by
|
||||
// `\uFFFD`. To return early with an error instead, use transform.Chain to
|
||||
// preprocess the data with a UTF8Validator.
|
||||
type Encoder struct {
|
||||
transform.Transformer
|
||||
|
||||
// This forces external creators of Encoders to use names in struct
|
||||
// initializers, allowing for future extendibility without having to break
|
||||
// code.
|
||||
_ struct{}
|
||||
}
|
||||
|
||||
// Bytes converts bytes from UTF-8. It returns the converted bytes or nil, err if
|
||||
// any error occurred.
|
||||
func (e *Encoder) Bytes(b []byte) ([]byte, error) {
|
||||
b, _, err := transform.Bytes(e, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// String converts a string from UTF-8. It returns the converted string or
|
||||
// "", err if any error occurred.
|
||||
func (e *Encoder) String(s string) (string, error) {
|
||||
s, _, err := transform.String(e, s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Writer wraps another Writer to encode its UTF-8 output.
|
||||
//
|
||||
// The Encoder may not be used for any other operation as long as the returned
|
||||
// Writer is in use.
|
||||
func (e *Encoder) Writer(w io.Writer) io.Writer {
|
||||
return transform.NewWriter(w, e)
|
||||
}
|
||||
|
||||
// ASCIISub is the ASCII substitute character, as recommended by
|
||||
// https://unicode.org/reports/tr36/#Text_Comparison
|
||||
const ASCIISub = '\x1a'
|
||||
|
||||
// Nop is the nop encoding. Its transformed bytes are the same as the source
|
||||
// bytes; it does not replace invalid UTF-8 sequences.
|
||||
var Nop Encoding = nop{}
|
||||
|
||||
type nop struct{}
|
||||
|
||||
func (nop) NewDecoder() *Decoder {
|
||||
return &Decoder{Transformer: transform.Nop}
|
||||
}
|
||||
func (nop) NewEncoder() *Encoder {
|
||||
return &Encoder{Transformer: transform.Nop}
|
||||
}
|
||||
|
||||
// Replacement is the replacement encoding. Decoding from the replacement
|
||||
// encoding yields a single '\uFFFD' replacement rune. Encoding from UTF-8 to
|
||||
// the replacement encoding yields the same as the source bytes except that
|
||||
// invalid UTF-8 is converted to '\uFFFD'.
|
||||
//
|
||||
// It is defined at http://encoding.spec.whatwg.org/#replacement
|
||||
var Replacement Encoding = replacement{}
|
||||
|
||||
type replacement struct{}
|
||||
|
||||
func (replacement) NewDecoder() *Decoder {
|
||||
return &Decoder{Transformer: replacementDecoder{}}
|
||||
}
|
||||
|
||||
func (replacement) NewEncoder() *Encoder {
|
||||
return &Encoder{Transformer: replacementEncoder{}}
|
||||
}
|
||||
|
||||
func (replacement) ID() (mib identifier.MIB, other string) {
|
||||
return identifier.Replacement, ""
|
||||
}
|
||||
|
||||
type replacementDecoder struct{ transform.NopResetter }
|
||||
|
||||
func (replacementDecoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
if len(dst) < 3 {
|
||||
return 0, 0, transform.ErrShortDst
|
||||
}
|
||||
if atEOF {
|
||||
const fffd = "\ufffd"
|
||||
dst[0] = fffd[0]
|
||||
dst[1] = fffd[1]
|
||||
dst[2] = fffd[2]
|
||||
nDst = 3
|
||||
}
|
||||
return nDst, len(src), nil
|
||||
}
|
||||
|
||||
type replacementEncoder struct{ transform.NopResetter }
|
||||
|
||||
func (replacementEncoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
r, size := rune(0), 0
|
||||
|
||||
for ; nSrc < len(src); nSrc += size {
|
||||
r = rune(src[nSrc])
|
||||
|
||||
// Decode a 1-byte rune.
|
||||
if r < utf8.RuneSelf {
|
||||
size = 1
|
||||
|
||||
} else {
|
||||
// Decode a multi-byte rune.
|
||||
r, size = utf8.DecodeRune(src[nSrc:])
|
||||
if size == 1 {
|
||||
// All valid runes of size 1 (those below utf8.RuneSelf) were
|
||||
// handled above. We have invalid UTF-8 or we haven't seen the
|
||||
// full character yet.
|
||||
if !atEOF && !utf8.FullRune(src[nSrc:]) {
|
||||
err = transform.ErrShortSrc
|
||||
break
|
||||
}
|
||||
r = '\ufffd'
|
||||
}
|
||||
}
|
||||
|
||||
if nDst+utf8.RuneLen(r) > len(dst) {
|
||||
err = transform.ErrShortDst
|
||||
break
|
||||
}
|
||||
nDst += utf8.EncodeRune(dst[nDst:], r)
|
||||
}
|
||||
return nDst, nSrc, err
|
||||
}
|
||||
|
||||
// HTMLEscapeUnsupported wraps encoders to replace source runes outside the
|
||||
// repertoire of the destination encoding with HTML escape sequences.
|
||||
//
|
||||
// This wrapper exists to comply to URL and HTML forms requiring a
|
||||
// non-terminating legacy encoder. The produced sequences may lead to data
|
||||
// loss as they are indistinguishable from legitimate input. To avoid this
|
||||
// issue, use UTF-8 encodings whenever possible.
|
||||
func HTMLEscapeUnsupported(e *Encoder) *Encoder {
|
||||
return &Encoder{Transformer: &errorHandler{e, errorToHTML}}
|
||||
}
|
||||
|
||||
// ReplaceUnsupported wraps encoders to replace source runes outside the
|
||||
// repertoire of the destination encoding with an encoding-specific
|
||||
// replacement.
|
||||
//
|
||||
// This wrapper is only provided for backwards compatibility and legacy
|
||||
// handling. Its use is strongly discouraged. Use UTF-8 whenever possible.
|
||||
func ReplaceUnsupported(e *Encoder) *Encoder {
|
||||
return &Encoder{Transformer: &errorHandler{e, errorToReplacement}}
|
||||
}
|
||||
|
||||
type errorHandler struct {
|
||||
*Encoder
|
||||
handler func(dst []byte, r rune, err repertoireError) (n int, ok bool)
|
||||
}
|
||||
|
||||
// TODO: consider making this error public in some form.
|
||||
type repertoireError interface {
|
||||
Replacement() byte
|
||||
}
|
||||
|
||||
func (h errorHandler) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
nDst, nSrc, err = h.Transformer.Transform(dst, src, atEOF)
|
||||
for err != nil {
|
||||
rerr, ok := err.(repertoireError)
|
||||
if !ok {
|
||||
return nDst, nSrc, err
|
||||
}
|
||||
r, sz := utf8.DecodeRune(src[nSrc:])
|
||||
n, ok := h.handler(dst[nDst:], r, rerr)
|
||||
if !ok {
|
||||
return nDst, nSrc, transform.ErrShortDst
|
||||
}
|
||||
err = nil
|
||||
nDst += n
|
||||
if nSrc += sz; nSrc < len(src) {
|
||||
var dn, sn int
|
||||
dn, sn, err = h.Transformer.Transform(dst[nDst:], src[nSrc:], atEOF)
|
||||
nDst += dn
|
||||
nSrc += sn
|
||||
}
|
||||
}
|
||||
return nDst, nSrc, err
|
||||
}
|
||||
|
||||
func errorToHTML(dst []byte, r rune, err repertoireError) (n int, ok bool) {
|
||||
buf := [8]byte{}
|
||||
b := strconv.AppendUint(buf[:0], uint64(r), 10)
|
||||
if n = len(b) + len("&#;"); n >= len(dst) {
|
||||
return 0, false
|
||||
}
|
||||
dst[0] = '&'
|
||||
dst[1] = '#'
|
||||
dst[copy(dst[2:], b)+2] = ';'
|
||||
return n, true
|
||||
}
|
||||
|
||||
func errorToReplacement(dst []byte, r rune, err repertoireError) (n int, ok bool) {
|
||||
if len(dst) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
dst[0] = err.Replacement()
|
||||
return 1, true
|
||||
}
|
||||
|
||||
// ErrInvalidUTF8 means that a transformer encountered invalid UTF-8.
|
||||
var ErrInvalidUTF8 = errors.New("encoding: invalid UTF-8")
|
||||
|
||||
// UTF8Validator is a transformer that returns ErrInvalidUTF8 on the first
|
||||
// input byte that is not valid UTF-8.
|
||||
var UTF8Validator transform.Transformer = utf8Validator{}
|
||||
|
||||
type utf8Validator struct{ transform.NopResetter }
|
||||
|
||||
func (utf8Validator) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
n := len(src)
|
||||
if n > len(dst) {
|
||||
n = len(dst)
|
||||
}
|
||||
for i := 0; i < n; {
|
||||
if c := src[i]; c < utf8.RuneSelf {
|
||||
dst[i] = c
|
||||
i++
|
||||
continue
|
||||
}
|
||||
_, size := utf8.DecodeRune(src[i:])
|
||||
if size == 1 {
|
||||
// All valid runes of size 1 (those below utf8.RuneSelf) were
|
||||
// handled above. We have invalid UTF-8 or we haven't seen the
|
||||
// full character yet.
|
||||
err = ErrInvalidUTF8
|
||||
if !atEOF && !utf8.FullRune(src[i:]) {
|
||||
err = transform.ErrShortSrc
|
||||
}
|
||||
return i, i, err
|
||||
}
|
||||
if i+size > len(dst) {
|
||||
return i, i, transform.ErrShortDst
|
||||
}
|
||||
for ; size > 0; size-- {
|
||||
dst[i] = src[i]
|
||||
i++
|
||||
}
|
||||
}
|
||||
if len(src) > len(dst) {
|
||||
err = transform.ErrShortDst
|
||||
}
|
||||
return n, n, err
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:generate go run gen.go
|
||||
|
||||
// Package identifier defines the contract between implementations of Encoding
|
||||
// and Index by defining identifiers that uniquely identify standardized coded
|
||||
// character sets (CCS) and character encoding schemes (CES), which we will
|
||||
// together refer to as encodings, for which Encoding implementations provide
|
||||
// converters to and from UTF-8. This package is typically only of concern to
|
||||
// implementers of Indexes and Encodings.
|
||||
//
|
||||
// One part of the identifier is the MIB code, which is defined by IANA and
|
||||
// uniquely identifies a CCS or CES. Each code is associated with data that
|
||||
// references authorities, official documentation as well as aliases and MIME
|
||||
// names.
|
||||
//
|
||||
// Not all CESs are covered by the IANA registry. The "other" string that is
|
||||
// returned by ID can be used to identify other character sets or versions of
|
||||
// existing ones.
|
||||
//
|
||||
// It is recommended that each package that provides a set of Encodings provide
|
||||
// the All and Common variables to reference all supported encodings and
|
||||
// commonly used subset. This allows Index implementations to include all
|
||||
// available encodings without explicitly referencing or knowing about them.
|
||||
package identifier
|
||||
|
||||
// Note: this package is internal, but could be made public if there is a need
|
||||
// for writing third-party Indexes and Encodings.
|
||||
|
||||
// References:
|
||||
// - http://source.icu-project.org/repos/icu/icu/trunk/source/data/mappings/convrtrs.txt
|
||||
// - http://www.iana.org/assignments/character-sets/character-sets.xhtml
|
||||
// - http://www.iana.org/assignments/ianacharset-mib/ianacharset-mib
|
||||
// - http://www.ietf.org/rfc/rfc2978.txt
|
||||
// - https://www.unicode.org/reports/tr22/
|
||||
// - http://www.w3.org/TR/encoding/
|
||||
// - https://encoding.spec.whatwg.org/
|
||||
// - https://encoding.spec.whatwg.org/encodings.json
|
||||
// - https://tools.ietf.org/html/rfc6657#section-5
|
||||
|
||||
// Interface can be implemented by Encodings to define the CCS or CES for which
|
||||
// it implements conversions.
|
||||
type Interface interface {
|
||||
// ID returns an encoding identifier. Exactly one of the mib and other
|
||||
// values should be non-zero.
|
||||
//
|
||||
// In the usual case it is only necessary to indicate the MIB code. The
|
||||
// other string can be used to specify encodings for which there is no MIB,
|
||||
// such as "x-mac-dingbat".
|
||||
//
|
||||
// The other string may only contain the characters a-z, A-Z, 0-9, - and _.
|
||||
ID() (mib MIB, other string)
|
||||
|
||||
// NOTE: the restrictions on the encoding are to allow extending the syntax
|
||||
// with additional information such as versions, vendors and other variants.
|
||||
}
|
||||
|
||||
// A MIB identifies an encoding. It is derived from the IANA MIB codes and adds
|
||||
// some identifiers for some encodings that are not covered by the IANA
|
||||
// standard.
|
||||
//
|
||||
// See http://www.iana.org/assignments/ianacharset-mib.
|
||||
type MIB uint16
|
||||
|
||||
// These additional MIB types are not defined in IANA. They are added because
|
||||
// they are common and defined within the text repo.
|
||||
const (
|
||||
// Unofficial marks the start of encodings not registered by IANA.
|
||||
Unofficial MIB = 10000 + iota
|
||||
|
||||
// Replacement is the WhatWG replacement encoding.
|
||||
Replacement
|
||||
|
||||
// XUserDefined is the code for x-user-defined.
|
||||
XUserDefined
|
||||
|
||||
// MacintoshCyrillic is the code for x-mac-cyrillic.
|
||||
MacintoshCyrillic
|
||||
)
|
||||
+1619
File diff suppressed because it is too large.
Load diff
+75
@@ -0,0 +1,75 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package internal contains code that is shared among encoding implementations.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/internal/identifier"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// Encoding is an implementation of the Encoding interface that adds the String
|
||||
// and ID methods to an existing encoding.
|
||||
type Encoding struct {
|
||||
encoding.Encoding
|
||||
Name string
|
||||
MIB identifier.MIB
|
||||
}
|
||||
|
||||
// _ verifies that Encoding implements identifier.Interface.
|
||||
var _ identifier.Interface = (*Encoding)(nil)
|
||||
|
||||
func (e *Encoding) String() string {
|
||||
return e.Name
|
||||
}
|
||||
|
||||
func (e *Encoding) ID() (mib identifier.MIB, other string) {
|
||||
return e.MIB, ""
|
||||
}
|
||||
|
||||
// SimpleEncoding is an Encoding that combines two Transformers.
|
||||
type SimpleEncoding struct {
|
||||
Decoder transform.Transformer
|
||||
Encoder transform.Transformer
|
||||
}
|
||||
|
||||
func (e *SimpleEncoding) NewDecoder() *encoding.Decoder {
|
||||
return &encoding.Decoder{Transformer: e.Decoder}
|
||||
}
|
||||
|
||||
func (e *SimpleEncoding) NewEncoder() *encoding.Encoder {
|
||||
return &encoding.Encoder{Transformer: e.Encoder}
|
||||
}
|
||||
|
||||
// FuncEncoding is an Encoding that combines two functions returning a new
|
||||
// Transformer.
|
||||
type FuncEncoding struct {
|
||||
Decoder func() transform.Transformer
|
||||
Encoder func() transform.Transformer
|
||||
}
|
||||
|
||||
func (e FuncEncoding) NewDecoder() *encoding.Decoder {
|
||||
return &encoding.Decoder{Transformer: e.Decoder()}
|
||||
}
|
||||
|
||||
func (e FuncEncoding) NewEncoder() *encoding.Encoder {
|
||||
return &encoding.Encoder{Transformer: e.Encoder()}
|
||||
}
|
||||
|
||||
// A RepertoireError indicates a rune is not in the repertoire of a destination
|
||||
// encoding. It is associated with an encoding-specific suggested replacement
|
||||
// byte.
|
||||
type RepertoireError byte
|
||||
|
||||
// Error implements the error interrface.
|
||||
func (r RepertoireError) Error() string {
|
||||
return "encoding: rune not supported by encoding."
|
||||
}
|
||||
|
||||
// Replacement returns the replacement string associated with this error.
|
||||
func (r RepertoireError) Replacement() byte { return byte(r) }
|
||||
|
||||
var ErrASCIIReplacement = RepertoireError(encoding.ASCIISub)
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package unicode
|
||||
|
||||
import (
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// BOMOverride returns a new decoder transformer that is identical to fallback,
|
||||
// except that the presence of a Byte Order Mark at the start of the input
|
||||
// causes it to switch to the corresponding Unicode decoding. It will only
|
||||
// consider BOMs for UTF-8, UTF-16BE, and UTF-16LE.
|
||||
//
|
||||
// This differs from using ExpectBOM by allowing a BOM to switch to UTF-8, not
|
||||
// just UTF-16 variants, and allowing falling back to any encoding scheme.
|
||||
//
|
||||
// This technique is recommended by the W3C for use in HTML 5: "For
|
||||
// compatibility with deployed content, the byte order mark (also known as BOM)
|
||||
// is considered more authoritative than anything else."
|
||||
// http://www.w3.org/TR/encoding/#specification-hooks
|
||||
//
|
||||
// Using BOMOverride is mostly intended for use cases where the first characters
|
||||
// of a fallback encoding are known to not be a BOM, for example, for valid HTML
|
||||
// and most encodings.
|
||||
func BOMOverride(fallback transform.Transformer) transform.Transformer {
|
||||
// TODO: possibly allow a variadic argument of unicode encodings to allow
|
||||
// specifying details of which fallbacks are supported as well as
|
||||
// specifying the details of the implementations. This would also allow for
|
||||
// support for UTF-32, which should not be supported by default.
|
||||
return &bomOverride{fallback: fallback}
|
||||
}
|
||||
|
||||
type bomOverride struct {
|
||||
fallback transform.Transformer
|
||||
current transform.Transformer
|
||||
}
|
||||
|
||||
func (d *bomOverride) Reset() {
|
||||
d.current = nil
|
||||
d.fallback.Reset()
|
||||
}
|
||||
|
||||
var (
|
||||
// TODO: we could use decode functions here, instead of allocating a new
|
||||
// decoder on every NewDecoder as IgnoreBOM decoders can be stateless.
|
||||
utf16le = UTF16(LittleEndian, IgnoreBOM)
|
||||
utf16be = UTF16(BigEndian, IgnoreBOM)
|
||||
)
|
||||
|
||||
const utf8BOM = "\ufeff"
|
||||
|
||||
func (d *bomOverride) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
if d.current != nil {
|
||||
return d.current.Transform(dst, src, atEOF)
|
||||
}
|
||||
if len(src) < 3 && !atEOF {
|
||||
return 0, 0, transform.ErrShortSrc
|
||||
}
|
||||
d.current = d.fallback
|
||||
bomSize := 0
|
||||
if len(src) >= 2 {
|
||||
if src[0] == 0xFF && src[1] == 0xFE {
|
||||
d.current = utf16le.NewDecoder()
|
||||
bomSize = 2
|
||||
} else if src[0] == 0xFE && src[1] == 0xFF {
|
||||
d.current = utf16be.NewDecoder()
|
||||
bomSize = 2
|
||||
} else if len(src) >= 3 &&
|
||||
src[0] == utf8BOM[0] &&
|
||||
src[1] == utf8BOM[1] &&
|
||||
src[2] == utf8BOM[2] {
|
||||
d.current = transform.Nop
|
||||
bomSize = 3
|
||||
}
|
||||
}
|
||||
if bomSize < len(src) {
|
||||
nDst, nSrc, err = d.current.Transform(dst, src[bomSize:], atEOF)
|
||||
}
|
||||
return nDst, nSrc + bomSize, err
|
||||
}
|
||||
+512
@@ -0,0 +1,512 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package unicode provides Unicode encodings such as UTF-16.
|
||||
package unicode // import "golang.org/x/text/encoding/unicode"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/internal"
|
||||
"golang.org/x/text/encoding/internal/identifier"
|
||||
"golang.org/x/text/internal/utf8internal"
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// TODO: I think the Transformers really should return errors on unmatched
|
||||
// surrogate pairs and odd numbers of bytes. This is not required by RFC 2781,
|
||||
// which leaves it open, but is suggested by WhatWG. It will allow for all error
|
||||
// modes as defined by WhatWG: fatal, HTML and Replacement. This would require
|
||||
// the introduction of some kind of error type for conveying the erroneous code
|
||||
// point.
|
||||
|
||||
// UTF8 is the UTF-8 encoding. It neither removes nor adds byte order marks.
|
||||
var UTF8 encoding.Encoding = utf8enc
|
||||
|
||||
// UTF8BOM is an UTF-8 encoding where the decoder strips a leading byte order
|
||||
// mark while the encoder adds one.
|
||||
//
|
||||
// Some editors add a byte order mark as a signature to UTF-8 files. Although
|
||||
// the byte order mark is not useful for detecting byte order in UTF-8, it is
|
||||
// sometimes used as a convention to mark UTF-8-encoded files. This relies on
|
||||
// the observation that the UTF-8 byte order mark is either an illegal or at
|
||||
// least very unlikely sequence in any other character encoding.
|
||||
var UTF8BOM encoding.Encoding = utf8bomEncoding{}
|
||||
|
||||
type utf8bomEncoding struct{}
|
||||
|
||||
func (utf8bomEncoding) String() string {
|
||||
return "UTF-8-BOM"
|
||||
}
|
||||
|
||||
func (utf8bomEncoding) ID() (identifier.MIB, string) {
|
||||
return identifier.Unofficial, "x-utf8bom"
|
||||
}
|
||||
|
||||
func (utf8bomEncoding) NewEncoder() *encoding.Encoder {
|
||||
return &encoding.Encoder{
|
||||
Transformer: &utf8bomEncoder{t: runes.ReplaceIllFormed()},
|
||||
}
|
||||
}
|
||||
|
||||
func (utf8bomEncoding) NewDecoder() *encoding.Decoder {
|
||||
return &encoding.Decoder{Transformer: &utf8bomDecoder{}}
|
||||
}
|
||||
|
||||
var utf8enc = &internal.Encoding{
|
||||
&internal.SimpleEncoding{utf8Decoder{}, runes.ReplaceIllFormed()},
|
||||
"UTF-8",
|
||||
identifier.UTF8,
|
||||
}
|
||||
|
||||
type utf8bomDecoder struct {
|
||||
checked bool
|
||||
}
|
||||
|
||||
func (t *utf8bomDecoder) Reset() {
|
||||
t.checked = false
|
||||
}
|
||||
|
||||
func (t *utf8bomDecoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
if !t.checked {
|
||||
if !atEOF && len(src) < len(utf8BOM) {
|
||||
if len(src) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
return 0, 0, transform.ErrShortSrc
|
||||
}
|
||||
if bytes.HasPrefix(src, []byte(utf8BOM)) {
|
||||
nSrc += len(utf8BOM)
|
||||
src = src[len(utf8BOM):]
|
||||
}
|
||||
t.checked = true
|
||||
}
|
||||
nDst, n, err := utf8Decoder.Transform(utf8Decoder{}, dst[nDst:], src, atEOF)
|
||||
nSrc += n
|
||||
return nDst, nSrc, err
|
||||
}
|
||||
|
||||
type utf8bomEncoder struct {
|
||||
written bool
|
||||
t transform.Transformer
|
||||
}
|
||||
|
||||
func (t *utf8bomEncoder) Reset() {
|
||||
t.written = false
|
||||
t.t.Reset()
|
||||
}
|
||||
|
||||
func (t *utf8bomEncoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
if !t.written {
|
||||
if len(dst) < len(utf8BOM) {
|
||||
return nDst, 0, transform.ErrShortDst
|
||||
}
|
||||
nDst = copy(dst, utf8BOM)
|
||||
t.written = true
|
||||
}
|
||||
n, nSrc, err := utf8Decoder.Transform(utf8Decoder{}, dst[nDst:], src, atEOF)
|
||||
nDst += n
|
||||
return nDst, nSrc, err
|
||||
}
|
||||
|
||||
type utf8Decoder struct{ transform.NopResetter }
|
||||
|
||||
func (utf8Decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
var pSrc int // point from which to start copy in src
|
||||
var accept utf8internal.AcceptRange
|
||||
|
||||
// The decoder can only make the input larger, not smaller.
|
||||
n := len(src)
|
||||
if len(dst) < n {
|
||||
err = transform.ErrShortDst
|
||||
n = len(dst)
|
||||
atEOF = false
|
||||
}
|
||||
for nSrc < n {
|
||||
c := src[nSrc]
|
||||
if c < utf8.RuneSelf {
|
||||
nSrc++
|
||||
continue
|
||||
}
|
||||
first := utf8internal.First[c]
|
||||
size := int(first & utf8internal.SizeMask)
|
||||
if first == utf8internal.FirstInvalid {
|
||||
goto handleInvalid // invalid starter byte
|
||||
}
|
||||
accept = utf8internal.AcceptRanges[first>>utf8internal.AcceptShift]
|
||||
if nSrc+size > n {
|
||||
if !atEOF {
|
||||
// We may stop earlier than necessary here if the short sequence
|
||||
// has invalid bytes. Not checking for this simplifies the code
|
||||
// and may avoid duplicate computations in certain conditions.
|
||||
if err == nil {
|
||||
err = transform.ErrShortSrc
|
||||
}
|
||||
break
|
||||
}
|
||||
// Determine the maximal subpart of an ill-formed subsequence.
|
||||
switch {
|
||||
case nSrc+1 >= n || src[nSrc+1] < accept.Lo || accept.Hi < src[nSrc+1]:
|
||||
size = 1
|
||||
case nSrc+2 >= n || src[nSrc+2] < utf8internal.LoCB || utf8internal.HiCB < src[nSrc+2]:
|
||||
size = 2
|
||||
default:
|
||||
size = 3 // As we are short, the maximum is 3.
|
||||
}
|
||||
goto handleInvalid
|
||||
}
|
||||
if c = src[nSrc+1]; c < accept.Lo || accept.Hi < c {
|
||||
size = 1
|
||||
goto handleInvalid // invalid continuation byte
|
||||
} else if size == 2 {
|
||||
} else if c = src[nSrc+2]; c < utf8internal.LoCB || utf8internal.HiCB < c {
|
||||
size = 2
|
||||
goto handleInvalid // invalid continuation byte
|
||||
} else if size == 3 {
|
||||
} else if c = src[nSrc+3]; c < utf8internal.LoCB || utf8internal.HiCB < c {
|
||||
size = 3
|
||||
goto handleInvalid // invalid continuation byte
|
||||
}
|
||||
nSrc += size
|
||||
continue
|
||||
|
||||
handleInvalid:
|
||||
// Copy the scanned input so far.
|
||||
nDst += copy(dst[nDst:], src[pSrc:nSrc])
|
||||
|
||||
// Append RuneError to the destination.
|
||||
const runeError = "\ufffd"
|
||||
if nDst+len(runeError) > len(dst) {
|
||||
return nDst, nSrc, transform.ErrShortDst
|
||||
}
|
||||
nDst += copy(dst[nDst:], runeError)
|
||||
|
||||
// Skip the maximal subpart of an ill-formed subsequence according to
|
||||
// the W3C standard way instead of the Go way. This Transform is
|
||||
// probably the only place in the text repo where it is warranted.
|
||||
nSrc += size
|
||||
pSrc = nSrc
|
||||
|
||||
// Recompute the maximum source length.
|
||||
if sz := len(dst) - nDst; sz < len(src)-nSrc {
|
||||
err = transform.ErrShortDst
|
||||
n = nSrc + sz
|
||||
atEOF = false
|
||||
}
|
||||
}
|
||||
return nDst + copy(dst[nDst:], src[pSrc:nSrc]), nSrc, err
|
||||
}
|
||||
|
||||
// UTF16 returns a UTF-16 Encoding for the given default endianness and byte
|
||||
// order mark (BOM) policy.
|
||||
//
|
||||
// When decoding from UTF-16 to UTF-8, if the BOMPolicy is IgnoreBOM then
|
||||
// neither BOMs U+FEFF nor noncharacters U+FFFE in the input stream will affect
|
||||
// the endianness used for decoding, and will instead be output as their
|
||||
// standard UTF-8 encodings: "\xef\xbb\xbf" and "\xef\xbf\xbe". If the BOMPolicy
|
||||
// is UseBOM or ExpectBOM a staring BOM is not written to the UTF-8 output.
|
||||
// Instead, it overrides the default endianness e for the remainder of the
|
||||
// transformation. Any subsequent BOMs U+FEFF or noncharacters U+FFFE will not
|
||||
// affect the endianness used, and will instead be output as their standard
|
||||
// UTF-8 encodings. For UseBOM, if there is no starting BOM, it will proceed
|
||||
// with the default Endianness. For ExpectBOM, in that case, the transformation
|
||||
// will return early with an ErrMissingBOM error.
|
||||
//
|
||||
// When encoding from UTF-8 to UTF-16, a BOM will be inserted at the start of
|
||||
// the output if the BOMPolicy is UseBOM or ExpectBOM. Otherwise, a BOM will not
|
||||
// be inserted. The UTF-8 input does not need to contain a BOM.
|
||||
//
|
||||
// There is no concept of a 'native' endianness. If the UTF-16 data is produced
|
||||
// and consumed in a greater context that implies a certain endianness, use
|
||||
// IgnoreBOM. Otherwise, use ExpectBOM and always produce and consume a BOM.
|
||||
//
|
||||
// In the language of https://www.unicode.org/faq/utf_bom.html#bom10, IgnoreBOM
|
||||
// corresponds to "Where the precise type of the data stream is known... the
|
||||
// BOM should not be used" and ExpectBOM corresponds to "A particular
|
||||
// protocol... may require use of the BOM".
|
||||
func UTF16(e Endianness, b BOMPolicy) encoding.Encoding {
|
||||
return utf16Encoding{config{e, b}, mibValue[e][b&bomMask]}
|
||||
}
|
||||
|
||||
// mibValue maps Endianness and BOMPolicy settings to MIB constants. Note that
|
||||
// some configurations map to the same MIB identifier. RFC 2781 has requirements
|
||||
// and recommendations. Some of the "configurations" are merely recommendations,
|
||||
// so multiple configurations could match.
|
||||
var mibValue = map[Endianness][numBOMValues]identifier.MIB{
|
||||
BigEndian: [numBOMValues]identifier.MIB{
|
||||
IgnoreBOM: identifier.UTF16BE,
|
||||
UseBOM: identifier.UTF16, // BigEnding default is preferred by RFC 2781.
|
||||
// TODO: acceptBOM | strictBOM would map to UTF16BE as well.
|
||||
},
|
||||
LittleEndian: [numBOMValues]identifier.MIB{
|
||||
IgnoreBOM: identifier.UTF16LE,
|
||||
UseBOM: identifier.UTF16, // LittleEndian default is allowed and preferred on Windows.
|
||||
// TODO: acceptBOM | strictBOM would map to UTF16LE as well.
|
||||
},
|
||||
// ExpectBOM is not widely used and has no valid MIB identifier.
|
||||
}
|
||||
|
||||
// All lists a configuration for each IANA-defined UTF-16 variant.
|
||||
var All = []encoding.Encoding{
|
||||
UTF8,
|
||||
UTF16(BigEndian, UseBOM),
|
||||
UTF16(BigEndian, IgnoreBOM),
|
||||
UTF16(LittleEndian, IgnoreBOM),
|
||||
}
|
||||
|
||||
// BOMPolicy is a UTF-16 encoding's byte order mark policy.
|
||||
type BOMPolicy uint8
|
||||
|
||||
const (
|
||||
writeBOM BOMPolicy = 0x01
|
||||
acceptBOM BOMPolicy = 0x02
|
||||
requireBOM BOMPolicy = 0x04
|
||||
bomMask BOMPolicy = 0x07
|
||||
|
||||
// HACK: numBOMValues == 8 triggers a bug in the 1.4 compiler (cannot have a
|
||||
// map of an array of length 8 of a type that is also used as a key or value
|
||||
// in another map). See golang.org/issue/11354.
|
||||
// TODO: consider changing this value back to 8 if the use of 1.4.* has
|
||||
// been minimized.
|
||||
numBOMValues = 8 + 1
|
||||
|
||||
// IgnoreBOM means to ignore any byte order marks.
|
||||
IgnoreBOM BOMPolicy = 0
|
||||
// Common and RFC 2781-compliant interpretation for UTF-16BE/LE.
|
||||
|
||||
// UseBOM means that the UTF-16 form may start with a byte order mark, which
|
||||
// will be used to override the default encoding.
|
||||
UseBOM BOMPolicy = writeBOM | acceptBOM
|
||||
// Common and RFC 2781-compliant interpretation for UTF-16.
|
||||
|
||||
// ExpectBOM means that the UTF-16 form must start with a byte order mark,
|
||||
// which will be used to override the default encoding.
|
||||
ExpectBOM BOMPolicy = writeBOM | acceptBOM | requireBOM
|
||||
// Used in Java as Unicode (not to be confused with Java's UTF-16) and
|
||||
// ICU's UTF-16,version=1. Not compliant with RFC 2781.
|
||||
|
||||
// TODO (maybe): strictBOM: BOM must match Endianness. This would allow:
|
||||
// - UTF-16(B|L)E,version=1: writeBOM | acceptBOM | requireBOM | strictBOM
|
||||
// (UnicodeBig and UnicodeLittle in Java)
|
||||
// - RFC 2781-compliant, but less common interpretation for UTF-16(B|L)E:
|
||||
// acceptBOM | strictBOM (e.g. assigned to CheckBOM).
|
||||
// This addition would be consistent with supporting ExpectBOM.
|
||||
)
|
||||
|
||||
// Endianness is a UTF-16 encoding's default endianness.
|
||||
type Endianness bool
|
||||
|
||||
const (
|
||||
// BigEndian is UTF-16BE.
|
||||
BigEndian Endianness = false
|
||||
// LittleEndian is UTF-16LE.
|
||||
LittleEndian Endianness = true
|
||||
)
|
||||
|
||||
// ErrMissingBOM means that decoding UTF-16 input with ExpectBOM did not find a
|
||||
// starting byte order mark.
|
||||
var ErrMissingBOM = errors.New("encoding: missing byte order mark")
|
||||
|
||||
type utf16Encoding struct {
|
||||
config
|
||||
mib identifier.MIB
|
||||
}
|
||||
|
||||
type config struct {
|
||||
endianness Endianness
|
||||
bomPolicy BOMPolicy
|
||||
}
|
||||
|
||||
func (u utf16Encoding) NewDecoder() *encoding.Decoder {
|
||||
return &encoding.Decoder{Transformer: &utf16Decoder{
|
||||
initial: u.config,
|
||||
current: u.config,
|
||||
}}
|
||||
}
|
||||
|
||||
func (u utf16Encoding) NewEncoder() *encoding.Encoder {
|
||||
return &encoding.Encoder{Transformer: &utf16Encoder{
|
||||
endianness: u.endianness,
|
||||
initialBOMPolicy: u.bomPolicy,
|
||||
currentBOMPolicy: u.bomPolicy,
|
||||
}}
|
||||
}
|
||||
|
||||
func (u utf16Encoding) ID() (mib identifier.MIB, other string) {
|
||||
return u.mib, ""
|
||||
}
|
||||
|
||||
func (u utf16Encoding) String() string {
|
||||
e, b := "B", ""
|
||||
if u.endianness == LittleEndian {
|
||||
e = "L"
|
||||
}
|
||||
switch u.bomPolicy {
|
||||
case ExpectBOM:
|
||||
b = "Expect"
|
||||
case UseBOM:
|
||||
b = "Use"
|
||||
case IgnoreBOM:
|
||||
b = "Ignore"
|
||||
}
|
||||
return "UTF-16" + e + "E (" + b + " BOM)"
|
||||
}
|
||||
|
||||
type utf16Decoder struct {
|
||||
initial config
|
||||
current config
|
||||
}
|
||||
|
||||
func (u *utf16Decoder) Reset() {
|
||||
u.current = u.initial
|
||||
}
|
||||
|
||||
func (u *utf16Decoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
if len(src) < 2 && atEOF && u.current.bomPolicy&requireBOM != 0 {
|
||||
return 0, 0, ErrMissingBOM
|
||||
}
|
||||
if len(src) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
if len(src) >= 2 && u.current.bomPolicy&acceptBOM != 0 {
|
||||
switch {
|
||||
case src[0] == 0xfe && src[1] == 0xff:
|
||||
u.current.endianness = BigEndian
|
||||
nSrc = 2
|
||||
case src[0] == 0xff && src[1] == 0xfe:
|
||||
u.current.endianness = LittleEndian
|
||||
nSrc = 2
|
||||
default:
|
||||
if u.current.bomPolicy&requireBOM != 0 {
|
||||
return 0, 0, ErrMissingBOM
|
||||
}
|
||||
}
|
||||
u.current.bomPolicy = IgnoreBOM
|
||||
}
|
||||
|
||||
var r rune
|
||||
var dSize, sSize int
|
||||
for nSrc < len(src) {
|
||||
if nSrc+1 < len(src) {
|
||||
x := uint16(src[nSrc+0])<<8 | uint16(src[nSrc+1])
|
||||
if u.current.endianness == LittleEndian {
|
||||
x = x>>8 | x<<8
|
||||
}
|
||||
r, sSize = rune(x), 2
|
||||
if utf16.IsSurrogate(r) {
|
||||
if nSrc+3 < len(src) {
|
||||
x = uint16(src[nSrc+2])<<8 | uint16(src[nSrc+3])
|
||||
if u.current.endianness == LittleEndian {
|
||||
x = x>>8 | x<<8
|
||||
}
|
||||
// Save for next iteration if it is not a high surrogate.
|
||||
if isHighSurrogate(rune(x)) {
|
||||
r, sSize = utf16.DecodeRune(r, rune(x)), 4
|
||||
}
|
||||
} else if !atEOF {
|
||||
err = transform.ErrShortSrc
|
||||
break
|
||||
}
|
||||
}
|
||||
if dSize = utf8.RuneLen(r); dSize < 0 {
|
||||
r, dSize = utf8.RuneError, 3
|
||||
}
|
||||
} else if atEOF {
|
||||
// Single trailing byte.
|
||||
r, dSize, sSize = utf8.RuneError, 3, 1
|
||||
} else {
|
||||
err = transform.ErrShortSrc
|
||||
break
|
||||
}
|
||||
if nDst+dSize > len(dst) {
|
||||
err = transform.ErrShortDst
|
||||
break
|
||||
}
|
||||
nDst += utf8.EncodeRune(dst[nDst:], r)
|
||||
nSrc += sSize
|
||||
}
|
||||
return nDst, nSrc, err
|
||||
}
|
||||
|
||||
func isHighSurrogate(r rune) bool {
|
||||
return 0xDC00 <= r && r <= 0xDFFF
|
||||
}
|
||||
|
||||
type utf16Encoder struct {
|
||||
endianness Endianness
|
||||
initialBOMPolicy BOMPolicy
|
||||
currentBOMPolicy BOMPolicy
|
||||
}
|
||||
|
||||
func (u *utf16Encoder) Reset() {
|
||||
u.currentBOMPolicy = u.initialBOMPolicy
|
||||
}
|
||||
|
||||
func (u *utf16Encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
if u.currentBOMPolicy&writeBOM != 0 {
|
||||
if len(dst) < 2 {
|
||||
return 0, 0, transform.ErrShortDst
|
||||
}
|
||||
dst[0], dst[1] = 0xfe, 0xff
|
||||
u.currentBOMPolicy = IgnoreBOM
|
||||
nDst = 2
|
||||
}
|
||||
|
||||
r, size := rune(0), 0
|
||||
for nSrc < len(src) {
|
||||
r = rune(src[nSrc])
|
||||
|
||||
// Decode a 1-byte rune.
|
||||
if r < utf8.RuneSelf {
|
||||
size = 1
|
||||
|
||||
} else {
|
||||
// Decode a multi-byte rune.
|
||||
r, size = utf8.DecodeRune(src[nSrc:])
|
||||
if size == 1 {
|
||||
// All valid runes of size 1 (those below utf8.RuneSelf) were
|
||||
// handled above. We have invalid UTF-8 or we haven't seen the
|
||||
// full character yet.
|
||||
if !atEOF && !utf8.FullRune(src[nSrc:]) {
|
||||
err = transform.ErrShortSrc
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r <= 0xffff {
|
||||
if nDst+2 > len(dst) {
|
||||
err = transform.ErrShortDst
|
||||
break
|
||||
}
|
||||
dst[nDst+0] = uint8(r >> 8)
|
||||
dst[nDst+1] = uint8(r)
|
||||
nDst += 2
|
||||
} else {
|
||||
if nDst+4 > len(dst) {
|
||||
err = transform.ErrShortDst
|
||||
break
|
||||
}
|
||||
r1, r2 := utf16.EncodeRune(r)
|
||||
dst[nDst+0] = uint8(r1 >> 8)
|
||||
dst[nDst+1] = uint8(r1)
|
||||
dst[nDst+2] = uint8(r2 >> 8)
|
||||
dst[nDst+3] = uint8(r2)
|
||||
nDst += 4
|
||||
}
|
||||
nSrc += size
|
||||
}
|
||||
|
||||
if u.endianness == LittleEndian {
|
||||
for i := 0; i < nDst; i += 2 {
|
||||
dst[i], dst[i+1] = dst[i+1], dst[i]
|
||||
}
|
||||
}
|
||||
return nDst, nSrc, err
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package utf8internal contains low-level utf8-related constants, tables, etc.
|
||||
// that are used internally by the text package.
|
||||
package utf8internal
|
||||
|
||||
// The default lowest and highest continuation byte.
|
||||
const (
|
||||
LoCB = 0x80 // 1000 0000
|
||||
HiCB = 0xBF // 1011 1111
|
||||
)
|
||||
|
||||
// Constants related to getting information of first bytes of UTF-8 sequences.
|
||||
const (
|
||||
// ASCII identifies a UTF-8 byte as ASCII.
|
||||
ASCII = as
|
||||
|
||||
// FirstInvalid indicates a byte is invalid as a first byte of a UTF-8
|
||||
// sequence.
|
||||
FirstInvalid = xx
|
||||
|
||||
// SizeMask is a mask for the size bits. Use use x&SizeMask to get the size.
|
||||
SizeMask = 7
|
||||
|
||||
// AcceptShift is the right-shift count for the first byte info byte to get
|
||||
// the index into the AcceptRanges table. See AcceptRanges.
|
||||
AcceptShift = 4
|
||||
|
||||
// The names of these constants are chosen to give nice alignment in the
|
||||
// table below. The first nibble is an index into acceptRanges or F for
|
||||
// special one-byte cases. The second nibble is the Rune length or the
|
||||
// Status for the special one-byte case.
|
||||
xx = 0xF1 // invalid: size 1
|
||||
as = 0xF0 // ASCII: size 1
|
||||
s1 = 0x02 // accept 0, size 2
|
||||
s2 = 0x13 // accept 1, size 3
|
||||
s3 = 0x03 // accept 0, size 3
|
||||
s4 = 0x23 // accept 2, size 3
|
||||
s5 = 0x34 // accept 3, size 4
|
||||
s6 = 0x04 // accept 0, size 4
|
||||
s7 = 0x44 // accept 4, size 4
|
||||
)
|
||||
|
||||
// First is information about the first byte in a UTF-8 sequence.
|
||||
var First = [256]uint8{
|
||||
// 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x00-0x0F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x10-0x1F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x20-0x2F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x30-0x3F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x40-0x4F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x50-0x5F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x60-0x6F
|
||||
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x70-0x7F
|
||||
// 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x80-0x8F
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x90-0x9F
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xA0-0xAF
|
||||
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xB0-0xBF
|
||||
xx, xx, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xC0-0xCF
|
||||
s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xD0-0xDF
|
||||
s2, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s4, s3, s3, // 0xE0-0xEF
|
||||
s5, s6, s6, s6, s7, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xF0-0xFF
|
||||
}
|
||||
|
||||
// AcceptRange gives the range of valid values for the second byte in a UTF-8
|
||||
// sequence for any value for First that is not ASCII or FirstInvalid.
|
||||
type AcceptRange struct {
|
||||
Lo uint8 // lowest value for second byte.
|
||||
Hi uint8 // highest value for second byte.
|
||||
}
|
||||
|
||||
// AcceptRanges is a slice of AcceptRange values. For a given byte sequence b
|
||||
//
|
||||
// AcceptRanges[First[b[0]]>>AcceptShift]
|
||||
//
|
||||
// will give the value of AcceptRange for the multi-byte UTF-8 sequence starting
|
||||
// at b[0].
|
||||
var AcceptRanges = [...]AcceptRange{
|
||||
0: {LoCB, HiCB},
|
||||
1: {0xA0, HiCB},
|
||||
2: {LoCB, 0x9F},
|
||||
3: {0x90, HiCB},
|
||||
4: {LoCB, 0x8F},
|
||||
}
|
||||
Vendored
+11
-1
@@ -151,6 +151,10 @@ github.com/jackc/pgtype
|
||||
github.com/jackc/pgx/v4
|
||||
github.com/jackc/pgx/v4/internal/sanitize
|
||||
github.com/jackc/pgx/v4/stdlib
|
||||
# github.com/jedib0t/go-pretty/v6 v6.2.4
|
||||
## explicit
|
||||
github.com/jedib0t/go-pretty/v6/table
|
||||
github.com/jedib0t/go-pretty/v6/text
|
||||
# github.com/jinzhu/inflection v1.0.0
|
||||
github.com/jinzhu/inflection
|
||||
# github.com/jinzhu/now v1.1.1
|
||||
@@ -179,7 +183,7 @@ github.com/mattn/go-colorable
|
||||
# github.com/mattn/go-isatty v0.0.13
|
||||
## explicit
|
||||
github.com/mattn/go-isatty
|
||||
# github.com/mattn/go-runewidth v0.0.7
|
||||
# github.com/mattn/go-runewidth v0.0.9
|
||||
github.com/mattn/go-runewidth
|
||||
# github.com/mattn/go-sqlite3 v1.14.5
|
||||
## explicit
|
||||
@@ -261,11 +265,17 @@ golang.org/x/sys/windows/svc/mgr
|
||||
# golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
|
||||
golang.org/x/term
|
||||
# golang.org/x/text v0.3.6
|
||||
## explicit
|
||||
golang.org/x/text/cases
|
||||
golang.org/x/text/encoding
|
||||
golang.org/x/text/encoding/internal
|
||||
golang.org/x/text/encoding/internal/identifier
|
||||
golang.org/x/text/encoding/unicode
|
||||
golang.org/x/text/internal
|
||||
golang.org/x/text/internal/language
|
||||
golang.org/x/text/internal/language/compact
|
||||
golang.org/x/text/internal/tag
|
||||
golang.org/x/text/internal/utf8internal
|
||||
golang.org/x/text/language
|
||||
golang.org/x/text/runes
|
||||
golang.org/x/text/secure/bidirule
|
||||
|
||||
Reference in new issue
Block a user