Compare commits

...
27 Commits
Author SHA1 Message Date
moloch-- fd3df6a3f3 Implemented ioc rm cmd 2021-07-31 11:42:23 -07:00
moloch-- 3c6b60bc3e Hosts IOC tracking support 2021-07-31 08:25:27 -07:00
moloch-- d42373e545 Added hosts iocs cmd 2021-07-29 19:41:32 -05:00
moloch-- 76e4f430d0 Merged extensions 2021-07-29 09:32:42 -05:00
moloch-- ab8d431c39 implemented hosts rm 2021-07-29 09:28:18 -05:00
moloch-- e4ad404822 Fix host uuid 2021-07-29 06:57:19 -05:00
rkervella 9e6a771a1a Merge branch 'master' into extensions 2021-07-29 11:40:54 +02:00
Ronan Kervella 94eb13610b Merge pull request #485 from BishopFox/registry
Add registry list-subkeys and list-values sub commands
2021-07-29 11:37:29 +02:00
rkervella 69ebbcb728 Merge branch 'extensions' of bishopfox.github.com:BishopFox/sliver into extensions 2021-07-29 10:17:28 +02:00
rkervella 5c65da3567 Add loader and default manifest to client's assets 2021-07-29 10:16:25 +02:00
rkervella 1c67a11c29 Remove CLR DLL as we don't need it anymore 2021-07-29 08:34:27 +02:00
moloch-- 755bed951b Add ioc tracking 2021-07-28 23:37:49 -05:00
moloch-- 7ba4e5bec0 Sessions for hosts 2021-07-28 23:04:26 -05:00
moloch-- 5d8d49461c Outlined hosts db and cmd 2021-07-28 22:48:14 -05:00
Joe cadc717963 Merge pull request #484 from BishopFox/jd/extensions
Platform agnostic path manipulation in macros
2021-07-28 14:27:38 -05:00
moloch-- ce4a7c7dcc Platform agnostic path manipulation in macros 2021-07-28 14:06:22 -05:00
Ronan Kervella ab8ff6a06a Merge pull request #483 from BishopFox/jd/extensions
Platform agnostic path manipulation
2021-07-28 17:41:51 +02:00
moloch-- 3b93554df2 Platform agnostic path manipulation 2021-07-28 08:20:52 -05:00
rkervella b688b7b121 Only update session when necessary 2021-07-28 14:58:01 +02:00
rkervella 3ce603a3c7 Fix constructor call, add dynamic entrypoint resolution for BOFs 2021-07-28 11:48:17 +02:00
rkervella 3d9df17d43 Fix path 2021-07-27 15:49:35 +02:00
rkervella d53c646af0 Store extension dir in assets 2021-07-27 15:44:05 +02:00
rkervella 601c1eea64 Add support for init function 2021-07-27 15:27:31 +02:00
rkervella 12ee96451a Refactor BOF args parsing 2021-07-27 15:24:29 +02:00
rkervella cc304ba85a Update readme 2021-07-27 15:19:09 +02:00
rkervella a743a72500 vendor update 2021-07-27 15:14:26 +02:00
rkervella 6c75b08648 Extension rework 2021-07-27 15:14:13 +02:00
96 changed files with 12651 additions and 2015 deletions

No files matched your search

+9
View File
@@ -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/
+100
View File
@@ -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")
manfiestPath := 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(manfiestPath)
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)
}
+16
View File
@@ -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"
}
}
]
}
]
+2 -1
View File
@@ -80,6 +80,7 @@ var rootCmd = &cobra.Command{
// StartClientConsole - Start the client console
func StartClientConsole() error {
assets.Setup(false, true)
configs := assets.GetConfigs()
if len(configs) == 0 {
fmt.Printf("No config files found at %s or -import\n", assets.GetConfigDir())
@@ -97,7 +98,7 @@ func StartClientConsole() error {
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
View File
@@ -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)
}
+60 -2
View File
@@ -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
View File
@@ -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
}
+2
View File
@@ -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
+1 -1
View File
@@ -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
}
}
+20 -12
View File
@@ -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:
+4
View File
@@ -0,0 +1,4 @@
Hosts
======
Command to access and manage host information.
+46
View File
@@ -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")
}
+107
View File
@@ -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
}
+41
View File
@@ -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")
}
+164
View File
@@ -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
}
+4
View File
@@ -0,0 +1,4 @@
Extensions
===========
Wraps external DLLs/tool in Sliver console commands.
+335
View File
@@ -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 := &macro{}
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)
}
+1 -1
View File
@@ -199,7 +199,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()
}
+7 -1
View File
@@ -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:"
)
+13
View File
@@ -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
+2
View File
@@ -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
+7 -1
View File
@@ -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=
+35
View File
@@ -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,113 @@
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)
}
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 somethig 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)
}
+23 -11
View File
@@ -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
View File
@@ -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,
},
+26
View File
@@ -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;
}
File diff suppressed because it is too large. Load diff
+10
View File
@@ -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);
+256
View File
@@ -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,
+17
View File
@@ -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)
File diff suppressed because it is too large. Load diff
+41
View File
@@ -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,45 @@ 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;
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
View File
@@ -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)
+1 -1
View File
@@ -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
)
+1 -1
View File
@@ -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
)
+1 -1
View File
@@ -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
)
+1 -1
View File
@@ -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.
-29
View File
@@ -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.
-1
View File
@@ -113,5 +113,4 @@ func serverOnlyCmds(console *clientconsole.SliverConsoleClient) {
},
HelpGroup: consts.MultiplayerHelpGroup,
})
}
+71
View File
@@ -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
}
}
}
+1
View File
@@ -72,6 +72,7 @@ type Session struct {
ProxyURL string
PollInterval uint32
Burned bool
Extensions []string
}
// ToProtobuf - Get the protobuf version of the object
+42
View File
@@ -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
}
+133
View File
@@ -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
}
+2
View File
@@ -36,6 +36,8 @@ type Loot struct {
FileType int
CredentialType int
Name string
OriginHost uuid.UUID `gorm:"type:uuid;"`
}
// BeforeCreate - GORM hook
+1 -1
View File
@@ -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
+3
View File
@@ -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{},
+3
View File
@@ -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",
+37
View File
@@ -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
}
+42
View File
@@ -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")
}
}
+80
View File
@@ -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
}
+3
View File
@@ -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
+1
View File
@@ -74,6 +74,7 @@ type GenericResponse interface {
// NewServer - Create new server instance
func NewServer() *Server {
core.StartEventAutomation()
return &Server{}
}
+5 -5
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,446 @@
# Table
[![Go Reference](https://pkg.go.dev/badge/github.com/jedib0t/go-pretty/v6/table.svg)](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: "&nbsp;",
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>&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<td align="right">1</td>
<td>Arya</td>
<td>Stark</td>
<td align="right">3000</td>
<td>&nbsp;</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>&nbsp;</td>
</tr>
</tbody>
<tfoot>
<tr>
<td align="right">&nbsp;</td>
<td>&nbsp;</td>
<td>Total</td>
<td align="right">10000</td>
<td>&nbsp;</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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>&nbsp;</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td align="right">1</td>
// <td>Arya</td>
// <td>Stark</td>
// <td align="right">3000</td>
// <td>&nbsp;</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>&nbsp;</td>
// </tr>
// </tbody>
// <tfoot>
// <tr>
// <td align="right">&nbsp;</td>
// <td>&nbsp;</td>
// <td>Total</td>
// <td align="right">10000</td>
// <td>&nbsp;</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
View File
@@ -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
View File
@@ -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
View File
@@ -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: "&nbsp;",
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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,7 @@
// +build !windows
package text
func areANSICodesSupported() bool {
return true
}
+31
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
@@ -2,7 +2,7 @@ go-runewidth
============
[![Build Status](https://travis-ci.org/mattn/go-runewidth.png?branch=master)](https://travis-ci.org/mattn/go-runewidth)
[![Coverage Status](https://coveralls.io/repos/mattn/go-runewidth/badge.png?branch=HEAD)](https://coveralls.io/r/mattn/go-runewidth?branch=HEAD)
[![Codecov](https://codecov.io/gh/mattn/go-runewidth/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-runewidth)
[![GoDoc](https://godoc.org/github.com/mattn/go-runewidth?status.svg)](http://godoc.org/github.com/mattn/go-runewidth)
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-runewidth)](https://goreportcard.com/report/github.com/mattn/go-runewidth)
+12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
)
File diff suppressed because it is too large. Load diff
+75
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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},
}
+11 -1
View File
@@ -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