Use XOR instead of AES for literal obfuscation.

Implement a literal obfuscator interface,
to allow the easy addition of new encodings.

Add literal obfuscation for byte literals.

Choose a random obfuscator on literal obfuscation,
useful when multiple obfuscators are implemented.

Fixes #62
pull/69/head
lu4p 4 years ago committed by Daniel Martí
parent 9c4b7d5a44
commit d48bdbadae

@ -1,57 +0,0 @@
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
mathrand "math/rand"
"strings"
)
// If math/rand.Seed() is not called, the generator behaves as if seeded by rand.Seed(1),
// so the generator is deterministic.
// genAesKey generates a 128-bit AES Key.
func genAesKey() []byte {
return genRandBytes(16)
}
// genAesKey generates a 128-bit nonce.
func genNonce() []byte {
return genRandBytes(12)
}
// genRandBytes return a random []byte with the length of size.
func genRandBytes(size int) []byte {
buffer := make([]byte, size)
if strings.HasPrefix(envGarbleSeed, "random;") {
_, err := rand.Read(buffer)
if err != nil {
panic(fmt.Sprintf("couldn't generate random key: %v", err))
}
} else {
mathrand.Read(buffer) // error is always nil so save to ignore
}
return buffer
}
// encAES encrypt data with key in AES GCM mode.
func encAES(data, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
nonce := genNonce()
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
ciphertext := aesgcm.Seal(nil, nonce, data, nil)
encData := append(nonce, ciphertext...)
return encData, nil
}

@ -0,0 +1,207 @@
package literals
import (
"fmt"
"go/ast"
"go/token"
"go/types"
mathrand "math/rand"
"strconv"
"golang.org/x/tools/go/ast/astutil"
)
func callExpr(resultType ast.Expr, block *ast.BlockStmt) *ast.CallExpr {
return &ast.CallExpr{Fun: &ast.FuncLit{
Type: &ast.FuncType{
Params: &ast.FieldList{},
Results: &ast.FieldList{
List: []*ast.Field{
{Type: resultType},
},
},
},
Body: block,
}}
}
func randObfuscator() obfuscator {
randPos := mathrand.Intn(len(obfuscators))
return obfuscators[randPos]
}
func returnStmt(result ast.Expr) *ast.ReturnStmt {
return &ast.ReturnStmt{
Results: []ast.Expr{result},
}
}
// Obfuscate replace literals with obfuscated lambda functions
func Obfuscate(files []*ast.File, info *types.Info, fset *token.FileSet, blacklist map[types.Object]struct{}) []*ast.File {
pre := func(cursor *astutil.Cursor) bool {
switch x := cursor.Node().(type) {
case *ast.GenDecl:
if x.Tok != token.CONST {
return true
}
for _, spec := range x.Specs {
spec, ok := spec.(*ast.ValueSpec)
if !ok {
return false
}
for _, name := range spec.Names {
obj := info.ObjectOf(name)
// The object itself is blacklisted, e.g. a value that needs to be constant
if _, ok := blacklist[obj]; ok {
return false
}
}
for _, val := range spec.Values {
if _, ok := val.(*ast.BasicLit); !ok {
return false // skip the block if it contains non basic literals
}
}
}
x.Tok = token.VAR
// constants are not possible if we want to obfuscate literals, therefore
// move all constant blocks which only contain strings to variables
}
return true
}
post := func(cursor *astutil.Cursor) bool {
switch x := cursor.Node().(type) {
case *ast.CompositeLit:
byteType := types.Universe.Lookup("byte").Type()
switch x := info.TypeOf(x.Type).(type) {
case *types.Array:
if x.Elem() != byteType {
return true
}
case *types.Slice:
if x.Elem() != byteType {
return true
}
default:
return true
}
var data []byte
for _, el := range x.Elts {
lit, ok := el.(*ast.BasicLit)
if !ok {
return true
}
value, err := strconv.Atoi(lit.Value)
if err != nil {
return true
}
data = append(data, byte(value))
}
cursor.Replace(obfuscateBytes(data))
case *ast.BasicLit:
switch cursor.Name() {
case "Values", "Rhs", "Value", "Args", "X", "Y", "Results":
default:
return true // we don't want to obfuscate imports etc.
}
switch x.Kind {
case token.STRING:
typeInfo := info.TypeOf(x)
if typeInfo != types.Typ[types.String] && typeInfo != types.Typ[types.UntypedString] {
return true
}
value, err := strconv.Unquote(x.Value)
if err != nil {
panic(fmt.Sprintf("cannot unquote string: %v", err))
}
cursor.Replace(obfuscateString(value))
}
}
return true
}
for i := range files {
files[i] = astutil.Apply(files[i], pre, post).(*ast.File)
}
return files
}
func obfuscateString(data string) *ast.CallExpr {
obfuscator := randObfuscator()
block := obfuscator.obfuscate([]byte(data))
block.List = append(block.List, &ast.ReturnStmt{
Results: []ast.Expr{&ast.CallExpr{
Fun: &ast.Ident{Name: "string"},
Args: []ast.Expr{&ast.Ident{Name: "data"}},
}},
})
return callExpr(&ast.Ident{Name: "string"}, block)
}
func obfuscateBytes(data []byte) *ast.CallExpr {
obfuscator := randObfuscator()
block := obfuscator.obfuscate(data)
block.List = append(block.List, &ast.ReturnStmt{
Results: []ast.Expr{&ast.Ident{Name: "data"}},
})
return callExpr(&ast.ArrayType{Elt: &ast.Ident{Name: "byte"}}, block)
}
// ConstBlacklist blacklist identifieres used in constant expressions
func ConstBlacklist(node ast.Node, info *types.Info, blacklist map[types.Object]struct{}) {
blacklistObjects := func(node ast.Node) bool {
ident, ok := node.(*ast.Ident)
if !ok {
return true
}
obj := info.ObjectOf(ident)
blacklist[obj] = struct{}{}
return true
}
switch x := node.(type) {
// in a slice or array composite literal all explicit keys must be constant representable
case *ast.CompositeLit:
if _, ok := x.Type.(*ast.ArrayType); !ok {
break
}
for _, elt := range x.Elts {
if kv, ok := elt.(*ast.KeyValueExpr); ok {
ast.Inspect(kv.Key, blacklistObjects)
}
}
// in an array type the length must be a constant representable
case *ast.ArrayType:
if x.Len != nil {
ast.Inspect(x.Len, blacklistObjects)
}
// in a const declaration all values must be constant representable
case *ast.GenDecl:
if x.Tok != token.CONST {
break
}
for _, spec := range x.Specs {
spec := spec.(*ast.ValueSpec)
for _, val := range spec.Values {
ast.Inspect(val, blacklistObjects)
}
}
}
}

@ -0,0 +1,60 @@
package literals
import (
cryptrand "crypto/rand"
"fmt"
"go/ast"
"go/token"
mathrand "math/rand"
"os"
"strings"
)
// obfuscator takes a byte slice and converts it to a ast.BlockStmt
type obfuscator interface {
obfuscate(data []byte) *ast.BlockStmt
}
var (
// obfuscators contains all types which implement the obfuscator Interface
obfuscators = []obfuscator{
xor{},
}
envGarbleSeed = os.Getenv("GARBLE_SEED")
)
// dataToByteSlice turns a byte slice like []byte{1, 2, 3} into an AST
// expression
func dataToByteSlice(data []byte) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.ArrayType{
Elt: &ast.Ident{Name: "byte"},
},
Args: []ast.Expr{&ast.BasicLit{
Kind: token.STRING,
Value: fmt.Sprintf("%q", data),
}},
}
}
// If math/rand.Seed() is not called, the generator behaves as if seeded by rand.Seed(1),
// so the generator is deterministic.
// genRandBytes return a random []byte with the length of size.
func genRandBytes(buffer []byte) {
if strings.HasPrefix(envGarbleSeed, "random;") {
_, err := cryptrand.Read(buffer)
if err != nil {
panic(fmt.Sprintf("couldn't generate random key: %v", err))
}
} else {
_, err := mathrand.Read(buffer)
if err != nil {
panic(fmt.Sprintf("couldn't generate random key: %v", err))
}
}
}
func genRandInt() int {
return mathrand.Int()
}

@ -0,0 +1,56 @@
package literals
import (
"go/ast"
"go/token"
)
type xor struct{}
// check that the obfuscator interface is implemented
var _ obfuscator = xor{}
func (x xor) obfuscate(data []byte) *ast.BlockStmt {
key := make([]byte, len(data))
genRandBytes(key)
for i, b := range key {
data[i] = data[i] ^ b
}
return &ast.BlockStmt{List: []ast.Stmt{
&ast.AssignStmt{
Lhs: []ast.Expr{&ast.Ident{Name: "key"}},
Tok: token.DEFINE,
Rhs: []ast.Expr{dataToByteSlice(key)},
},
&ast.AssignStmt{
Lhs: []ast.Expr{&ast.Ident{Name: "data"}},
Tok: token.DEFINE,
Rhs: []ast.Expr{dataToByteSlice(data)},
},
&ast.RangeStmt{
Key: &ast.Ident{Name: "i"},
Value: &ast.Ident{Name: "b"},
Tok: token.DEFINE,
X: &ast.Ident{Name: "key"},
Body: &ast.BlockStmt{List: []ast.Stmt{
&ast.AssignStmt{
Lhs: []ast.Expr{&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.Ident{Name: "i"},
}},
Tok: token.ASSIGN,
Rhs: []ast.Expr{&ast.BinaryExpr{
X: &ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.Ident{Name: "i"},
},
Op: token.XOR,
Y: &ast.Ident{Name: "b"},
}},
},
}},
},
}}
}

@ -29,6 +29,7 @@ import (
"strings"
"golang.org/x/tools/go/ast/astutil"
"mvdan.cc/garble/internal/literals"
)
var flagSet = flag.NewFlagSet("garble", flag.ContinueOnError)
@ -383,8 +384,9 @@ func transformCompile(args []string) ([]string, error) {
}
info := &types.Info{
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
}
pkg, err := origTypesConfig.Check(pkgPath, fset, files, info)
if err != nil {
@ -394,7 +396,7 @@ func transformCompile(args []string) ([]string, error) {
blacklist := buildBlacklist(files, info, pkg)
if envGarbleLiterals {
files = obfuscateLiterals(files, info, blacklist)
files = literals.Obfuscate(files, info, fset, blacklist)
// ast changed so we need to typecheck again
pkg, err = origTypesConfig.Check(pkgPath, fset, files, info)
if err != nil {
@ -575,38 +577,6 @@ func readBuildIDs(flags []string) error {
}
// log.Printf("%#v", buildInfo)
// Since string obfuscation adds crypto dependencies, ensure they are
// also part of the importcfg. Otherwise, the compiler or linker might
// error when trying to locate them.
// TODO: this means these packages can't be garbled. never garble std?
if envGarbleLiterals {
toAdd := []string{
"crypto/aes",
"crypto/cipher",
}
for len(toAdd) > 0 {
// Use a stack, to reuse memory.
path := toAdd[len(toAdd)-1]
toAdd = toAdd[:len(toAdd)-1]
if _, ok := buildInfo.imports[path]; ok {
continue
}
pkg, err := listPackage(path)
if err != nil {
return err
}
if pkg.Export == "" {
continue // e.g. unsafe
}
if _, err := fmt.Fprintf(f, "packagefile %s=%s\n", path, pkg.Export); err != nil {
return err
}
// Add their dependencies too, without adding duplicates.
buildInfo.imports[path] = importedPkg{packagefile: pkg.Export}
toAdd = append(toAdd, pkg.Deps...)
}
}
if err := f.Close(); err != nil {
return err
}
@ -682,7 +652,7 @@ func buildBlacklist(files []*ast.File, info *types.Info, pkg *types.Package) map
visit := func(node ast.Node) bool {
if envGarbleLiterals {
constBlacklist(node, info, blacklist)
literals.ConstBlacklist(node, info, blacklist)
}
call, ok := node.(*ast.CallExpr)

@ -1,362 +0,0 @@
package main
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"strconv"
"golang.org/x/tools/go/ast/astutil"
)
func isTypeDefStr(typ types.Type) bool {
strType := types.Typ[types.String]
if named, ok := typ.(*types.Named); ok {
return types.Identical(named.Underlying(), strType)
}
return false
}
func containsTypeDefStr(expr ast.Expr, info *types.Info) bool {
typ := info.TypeOf(expr)
// log.Println(expr, typ, reflect.TypeOf(expr), reflect.TypeOf(typ))
if sig, ok := typ.(*types.Signature); ok {
for i := 0; i < sig.Params().Len(); i++ {
if isTypeDefStr(sig.Params().At(i).Type()) {
return true
}
}
}
if mapT, ok := typ.(*types.Map); ok {
return isTypeDefStr(mapT.Elem()) || isTypeDefStr(mapT.Key())
}
if named, ok := typ.(*types.Named); ok {
return isTypeDefStr(named)
}
return false
}
func obfuscateLiterals(files []*ast.File, info *types.Info, blacklist map[types.Object]struct{}) []*ast.File {
pre := func(cursor *astutil.Cursor) bool {
switch x := cursor.Node().(type) {
case *ast.ValueSpec:
return !containsTypeDefStr(x.Type, info)
case *ast.AssignStmt:
for _, expr := range x.Lhs {
if index, ok := expr.(*ast.IndexExpr); ok {
return !containsTypeDefStr(index.X, info)
}
if ident, ok := expr.(*ast.Ident); ok {
return !containsTypeDefStr(ident, info)
}
}
case *ast.CallExpr:
return !containsTypeDefStr(x.Fun, info)
case *ast.CompositeLit:
if t, ok := x.Type.(*ast.MapType); ok {
return !(containsTypeDefStr(t.Key, info) || containsTypeDefStr(t.Value, info))
}
case *ast.FuncDecl:
if x.Type.Results == nil {
return true
}
for _, result := range x.Type.Results.List {
for _, name := range result.Names {
return !containsTypeDefStr(name, info)
}
}
case *ast.KeyValueExpr:
if ident, ok := x.Key.(*ast.Ident); ok {
return !containsTypeDefStr(ident, info)
}
case *ast.GenDecl:
if x.Tok != token.CONST {
return true
}
for _, spec := range x.Specs {
spec, ok := spec.(*ast.ValueSpec)
if !ok {
return false
}
for _, name := range spec.Names {
obj := info.ObjectOf(name)
// The object itself is blacklisted, e.g. a value that needs to be constant
if _, ok := blacklist[obj]; ok {
return false
}
}
for _, val := range spec.Values {
if v, ok := val.(*ast.BasicLit); !ok || v.Kind != token.STRING {
return false // skip the block if it contains non basic literals
}
}
}
x.Tok = token.VAR
// constants are not possible if we want to obfuscate literals, therefore
// move all constant blocks which only contain strings to variables
}
return true
}
key := genAesKey()
addedToPkg := false // we only want to inject the code and imports once
post := func(cursor *astutil.Cursor) bool {
switch x := cursor.Node().(type) {
case *ast.File:
if addedToPkg {
break
}
x.Decls = append(x.Decls, funcStmt)
x.Decls = append(x.Decls, keyStmt(key))
astutil.AddImport(fset, x, "crypto/aes")
astutil.AddImport(fset, x, "crypto/cipher")
addedToPkg = true
case *ast.BasicLit:
switch cursor.Name() {
case "Values", "Rhs", "Value", "Args":
default:
return true // we don't want to obfuscate imports etc.
}
if x.Kind != token.STRING {
return true // TODO: garble literals other than strings
}
value, err := strconv.Unquote(x.Value)
if err != nil {
panic(fmt.Sprintf("cannot unquote string: %v", err))
}
ciphertext, err := encAES([]byte(value), key)
if err != nil {
panic(fmt.Sprintf("cannot encrypt string: %v", err))
}
cursor.Replace(ciphertextStmt(ciphertext))
}
return true
}
for i := range files {
files[i] = astutil.Apply(files[i], pre, post).(*ast.File)
}
return files
}
// AST definitions for injection
var (
aesCipherStmt = &ast.AssignStmt{
Lhs: []ast.Expr{
&ast.Ident{Name: "block"},
&ast.Ident{Name: "err"},
},
Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "aes"},
Sel: &ast.Ident{Name: "NewCipher"},
},
Args: []ast.Expr{&ast.Ident{Name: "garbleKey"}},
}},
}
aesGcmCipherStmt = &ast.AssignStmt{
Lhs: []ast.Expr{
&ast.Ident{Name: "aesgcm"},
&ast.Ident{Name: "err"},
},
Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "cipher"},
Sel: &ast.Ident{Name: "NewGCM"},
},
Args: []ast.Expr{&ast.Ident{Name: "block"}},
}},
}
plaintextStmt = &ast.AssignStmt{
Lhs: []ast.Expr{
&ast.Ident{Name: "plaintext"},
&ast.Ident{Name: "err"},
},
Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "aesgcm"},
Sel: &ast.Ident{Name: "Open"},
},
Args: []ast.Expr{
&ast.Ident{Name: "nil"},
&ast.SliceExpr{
X: &ast.Ident{Name: "ciphertext"},
High: &ast.BasicLit{
Kind: token.INT,
Value: "12",
},
},
&ast.SliceExpr{
X: &ast.Ident{Name: "ciphertext"},
Low: &ast.BasicLit{
Kind: token.INT,
Value: "12",
},
},
&ast.Ident{Name: "nil"},
},
}},
}
returnStmt = &ast.ReturnStmt{Results: []ast.Expr{
&ast.CallExpr{
Fun: &ast.Ident{Name: "string"},
Args: []ast.Expr{&ast.Ident{Name: "plaintext"}},
},
}}
)
func decErrStmt() *ast.IfStmt {
return &ast.IfStmt{
Cond: &ast.BinaryExpr{
X: &ast.Ident{Name: "err"},
Op: token.NEQ,
Y: &ast.Ident{Name: "nil"},
},
Body: &ast.BlockStmt{List: []ast.Stmt{
&ast.ExprStmt{X: &ast.CallExpr{
Fun: &ast.Ident{Name: "panic"},
Args: []ast.Expr{&ast.BinaryExpr{
X: &ast.BasicLit{
Kind: token.STRING,
Value: `"garble: literal couldn't be decrypted: "`,
},
Op: token.ADD,
Y: &ast.CallExpr{Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "err"},
Sel: &ast.Ident{Name: "Error"},
}},
}},
}},
}},
}
}
var funcStmt = &ast.FuncDecl{
Name: &ast.Ident{Name: "garbleDecrypt"},
Type: &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{{
Names: []*ast.Ident{{Name: "ciphertext"}},
Type: &ast.ArrayType{
Elt: &ast.Ident{Name: "byte"},
},
}}},
Results: &ast.FieldList{List: []*ast.Field{{
Type: &ast.Ident{Name: "string"},
}}},
},
Body: &ast.BlockStmt{List: []ast.Stmt{
aesCipherStmt,
decErrStmt(),
aesGcmCipherStmt,
decErrStmt(),
plaintextStmt,
decErrStmt(),
returnStmt,
}},
}
func ciphertextStmt(ciphertext []byte) *ast.CallExpr {
ciphertextLit := dataToByteSlice(ciphertext)
return &ast.CallExpr{
Fun: &ast.Ident{Name: "garbleDecrypt"},
Args: []ast.Expr{ciphertextLit},
}
}
// dataToByteSlice turns a byte slice like []byte{1, 2, 3} into an AST
// expression
func dataToByteSlice(data []byte) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.ArrayType{
Elt: &ast.Ident{Name: "byte"},
},
Args: []ast.Expr{&ast.BasicLit{
Kind: token.STRING,
Value: fmt.Sprintf("%q", data),
}},
}
}
func keyStmt(key []byte) *ast.GenDecl {
keyLit := dataToByteSlice(key)
return &ast.GenDecl{
Tok: token.VAR,
Specs: []ast.Spec{&ast.ValueSpec{
Names: []*ast.Ident{{Name: "garbleKey"}},
Values: []ast.Expr{keyLit},
}},
}
}
func constBlacklist(node ast.Node, info *types.Info, blacklist map[types.Object]struct{}) {
blacklistObjects := func(node ast.Node) bool {
ident, ok := node.(*ast.Ident)
if !ok {
return true
}
obj := info.ObjectOf(ident)
blacklist[obj] = struct{}{}
return true
}
switch x := node.(type) {
// in a slice or array composite literal all explicit keys must be constant representable
case *ast.CompositeLit:
if _, ok := x.Type.(*ast.ArrayType); !ok {
break
}
for _, elt := range x.Elts {
if kv, ok := elt.(*ast.KeyValueExpr); ok {
ast.Inspect(kv.Key, blacklistObjects)
}
}
// in an array type the length must be a constant representable
case *ast.ArrayType:
if x.Len != nil {
ast.Inspect(x.Len, blacklistObjects)
}
// in a const declaration all values must be constant representable
case *ast.GenDecl:
if x.Tok != token.CONST {
break
}
for _, spec := range x.Specs {
spec := spec.(*ast.ValueSpec)
for _, val := range spec.Values {
ast.Inspect(val, blacklistObjects)
}
}
}
}

@ -1,10 +1,13 @@
go run .
cmp stderr main.stderr
cp stderr normal.stderr
garble -literals build
exec ./main$exe
cmp stderr main.stderr
cmp stderr normal.stderr
! binsubstr main$exe 'garbleDecrypt' 'Lorem' 'ipsum' 'dolor' 'first assign' 'second assign' 'First Line' 'Second Line' 'map value' 'to obfuscate' 'also obfuscate' 'stringTypeField String' 'stringTypeStruct'
binsubstr main$exe 'Skip this block,' 'also skip this' 'skip typed const' 'skip typed var' 'skip typed var assign' 'stringTypeField strType' 'stringType lambda func return' 'testMap1 key' 'testMap2 key' 'testMap3 key' 'testMap1 value' 'testMap2 value' 'testMap3 value' 'testMap1 new value' 'testMap2 new value' 'testMap3 new value' 'stringType func param' 'stringType return'
binsubstr main$exe 'also skip this' 'skip typed const' 'skip typed var' 'skip typed var assign' 'stringTypeField strType' 'stringType lambda func return' 'testMap1 key' 'testMap2 key' 'testMap3 key' 'testMap1 value' 'testMap3 value' 'testMap1 new value' 'testMap3 new value' 'stringType func param' 'stringType return'
[short] stop # checking that the build is reproducible is slow
# Also check that the binary is reproducible.
@ -18,9 +21,6 @@ module test/main
-- main.go --
package main
// The lack of imports is an edge case, since we have to add imports like
// crypto/aes.
type strucTest struct {
field string
anotherfield string
@ -33,8 +33,8 @@ Second Line`
)
const (
skip1 = "Skip this block,"
i = 1
i = 1
boolean = true
)
const (
@ -58,8 +58,10 @@ func main() {
reassign := "first assign"
reassign = "second assign"
println(cnst)
println(multiline)
add := "total" + " string"
println(cnst, boolean)
println(multiline, add)
println(localVar)
println(reassign)
println(empty)
@ -68,6 +70,11 @@ func main() {
field: "to obfuscate",
anotherfield: "also obfuscate",
}
lambda := func() string {
return "😅 😅"
}()
println(lambda)
println(x.field, x.anotherfield)
@ -75,10 +82,11 @@ func main() {
testMap["map key"] = "new value"
println(testMap["map key"])
println("another literal")
println(skip1, skip2)
println(skip2)
println(i, foo, bar)
typedTest()
constantTest()
byteTest()
}
type stringType string
@ -112,8 +120,8 @@ func typedTest() {
testMap1 := map[string]stringType{"testMap1 key": "testMap1 value"} // skip
testMap1["testMap1 key"] = "testMap1 new value" // skip
testMap2 := map[stringType]string{"testMap2 key": "testMap2 value"} // skip
testMap2["testMap2 key"] = "testMap2 new value" // skip
testMap2 := map[stringType]string{"testMap2 key": "testMap2 value"} // skip key
testMap2["testMap2 key"] = "testMap2 new value" // skip key
testMap3 := map[stringType]stringType{"testMap3 key": "testMap3 value"} // skip
testMap3["testMap3 key"] = "testMap3 new value" // skip
@ -152,22 +160,42 @@ func constantTest() {
const i = length + len(f)
}
func byteTest() {
a := []byte{12, 13}
for _, elm := range a {
print(elm, ", ")
}
println()
var b = []byte{12, 13}
for _, elm := range b {
print(elm, ", ")
}
println()
var c = [2]byte{12, 13}
for _, elm := range c {
print(elm, ", ")
}
println()
}
func stringTypeFunc(s stringType) stringType {
println(s)
return "stringType return" // skip
}
-- main.stderr --
Lorem
Lorem true
First Line
Second Line
Second Line total string
dolor
second assign
😅 😅
to obfuscate also obfuscate
new value
another literal
Skip this block, also skip this
also skip this
1 0 1
skip typed const skip typed var skip typed var assign
stringTypeField String stringTypeField strType
@ -176,3 +204,6 @@ stringType func param
stringType return
foo
foo
12, 13,
12, 13,
12, 13,
Loading…
Cancel
Save