github.com/diadata-org/diadata@v1.4.593/pkg/utils/typeddata.go (about) 1 package utils 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "math/big" 8 "reflect" 9 "regexp" 10 "sort" 11 "strconv" 12 "strings" 13 "unicode" 14 "unicode/utf8" 15 16 "github.com/ethereum/go-ethereum/common" 17 "github.com/ethereum/go-ethereum/common/hexutil" 18 "github.com/ethereum/go-ethereum/common/math" 19 "github.com/ethereum/go-ethereum/crypto" 20 ) 21 22 var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`) 23 24 type TypedData struct { 25 Types Types `json:"types"` 26 PrimaryType string `json:"primaryType"` 27 Domain TypedDataDomain `json:"domain"` 28 Message TypedDataMessage `json:"message"` 29 } 30 31 // Type is the inner type of an EIP-712 message 32 type Type struct { 33 Name string `json:"name"` 34 Type string `json:"type"` 35 } 36 37 func (t *Type) isArray() bool { 38 return strings.HasSuffix(t.Type, "[]") 39 } 40 41 // typeName returns the canonical name of the type. If the type is 'Person[]', then 42 // this method returns 'Person' 43 func (t *Type) typeName() string { 44 if strings.HasSuffix(t.Type, "[]") { 45 return strings.TrimSuffix(t.Type, "[]") 46 } 47 return t.Type 48 } 49 50 func (t *Type) isReferenceType() bool { 51 if len(t.Type) == 0 { 52 return false 53 } 54 // Reference types must have a leading uppercase character 55 r, _ := utf8.DecodeRuneInString(t.Type) 56 return unicode.IsUpper(r) 57 } 58 59 type Types map[string][]Type 60 61 type TypePriority struct { 62 Type string 63 Value uint 64 } 65 66 type TypedDataMessage = map[string]interface{} 67 68 // TypedDataDomain represents the domain part of an EIP-712 message. 69 type TypedDataDomain struct { 70 Name string `json:"name"` 71 Version string `json:"version"` 72 ChainId *math.HexOrDecimal256 `json:"chainId"` 73 VerifyingContract string `json:"verifyingContract"` 74 Salt string `json:"salt"` 75 } 76 77 // TypedDataAndHash is a helper function that calculates a hash for typed data conforming to EIP-712. 78 // This hash can then be safely used to calculate a signature. 79 // 80 // See https://eips.ethereum.org/EIPS/eip-712 for the full specification. 81 // 82 // This gives context to the signed typed data and prevents signing of transactions. 83 func TypedDataAndHash(typedData TypedData) ([]byte, string, error) { 84 domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) 85 if err != nil { 86 return nil, "", err 87 } 88 typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) 89 if err != nil { 90 return nil, "", err 91 } 92 rawData := fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)) 93 return crypto.Keccak256([]byte(rawData)), rawData, nil 94 } 95 96 // HashStruct generates a keccak256 hash of the encoding of the provided data 97 func (typedData *TypedData) HashStruct(primaryType string, data TypedDataMessage) (hexutil.Bytes, error) { 98 encodedData, err := typedData.EncodeData(primaryType, data, 1) 99 if err != nil { 100 return nil, err 101 } 102 return crypto.Keccak256(encodedData), nil 103 } 104 105 // Dependencies returns an array of custom types ordered by their hierarchical reference tree 106 func (typedData *TypedData) Dependencies(primaryType string, found []string) []string { 107 primaryType = strings.TrimSuffix(primaryType, "[]") 108 includes := func(arr []string, str string) bool { 109 for _, obj := range arr { 110 if obj == str { 111 return true 112 } 113 } 114 return false 115 } 116 117 if includes(found, primaryType) { 118 return found 119 } 120 if typedData.Types[primaryType] == nil { 121 return found 122 } 123 found = append(found, primaryType) 124 for _, field := range typedData.Types[primaryType] { 125 for _, dep := range typedData.Dependencies(field.Type, found) { 126 if !includes(found, dep) { 127 found = append(found, dep) 128 } 129 } 130 } 131 return found 132 } 133 134 // EncodeType generates the following encoding: 135 // `name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")"` 136 // 137 // each member is written as `type ‖ " " ‖ name` encodings cascade down and are sorted by name 138 func (typedData *TypedData) EncodeType(primaryType string) hexutil.Bytes { 139 // Get dependencies primary first, then alphabetical 140 deps := typedData.Dependencies(primaryType, []string{}) 141 if len(deps) > 0 { 142 slicedDeps := deps[1:] 143 sort.Strings(slicedDeps) 144 deps = append([]string{primaryType}, slicedDeps...) 145 } 146 147 // Format as a string with fields 148 var buffer bytes.Buffer 149 for _, dep := range deps { 150 buffer.WriteString(dep) 151 buffer.WriteString("(") 152 for _, obj := range typedData.Types[dep] { 153 buffer.WriteString(obj.Type) 154 buffer.WriteString(" ") 155 buffer.WriteString(obj.Name) 156 buffer.WriteString(",") 157 } 158 buffer.Truncate(buffer.Len() - 1) 159 buffer.WriteString(")") 160 } 161 return buffer.Bytes() 162 } 163 164 // TypeHash creates the keccak256 hash of the data 165 func (typedData *TypedData) TypeHash(primaryType string) hexutil.Bytes { 166 return crypto.Keccak256(typedData.EncodeType(primaryType)) 167 } 168 169 // EncodeData generates the following encoding: 170 // `enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)` 171 // 172 // each encoded member is 32-byte long 173 func (typedData *TypedData) EncodeData(primaryType string, data map[string]interface{}, depth int) (hexutil.Bytes, error) { 174 if err := typedData.validate(); err != nil { 175 return nil, err 176 } 177 178 buffer := bytes.Buffer{} 179 180 // Verify extra data 181 if exp, got := len(typedData.Types[primaryType]), len(data); exp < got { 182 return nil, fmt.Errorf("there is extra data provided in the message (%d < %d)", exp, got) 183 } 184 185 // Add typehash 186 buffer.Write(typedData.TypeHash(primaryType)) 187 188 // Add field contents. Structs and arrays have special handlers. 189 for _, field := range typedData.Types[primaryType] { 190 encType := field.Type 191 encValue := data[field.Name] 192 if encType[len(encType)-1:] == "]" { 193 arrayValue, ok := encValue.([]interface{}) 194 if !ok { 195 return nil, dataMismatchError(encType, encValue) 196 } 197 198 arrayBuffer := bytes.Buffer{} 199 parsedType := strings.Split(encType, "[")[0] 200 for _, item := range arrayValue { 201 if typedData.Types[parsedType] != nil { 202 mapValue, ok := item.(map[string]interface{}) 203 if !ok { 204 return nil, dataMismatchError(parsedType, item) 205 } 206 encodedData, err := typedData.EncodeData(parsedType, mapValue, depth+1) 207 if err != nil { 208 return nil, err 209 } 210 arrayBuffer.Write(crypto.Keccak256(encodedData)) 211 } else { 212 bytesValue, err := typedData.EncodePrimitiveValue(parsedType, item, depth) 213 if err != nil { 214 return nil, err 215 } 216 arrayBuffer.Write(bytesValue) 217 } 218 } 219 220 buffer.Write(crypto.Keccak256(arrayBuffer.Bytes())) 221 } else if typedData.Types[field.Type] != nil { 222 mapValue, ok := encValue.(map[string]interface{}) 223 if !ok { 224 return nil, dataMismatchError(encType, encValue) 225 } 226 encodedData, err := typedData.EncodeData(field.Type, mapValue, depth+1) 227 if err != nil { 228 return nil, err 229 } 230 buffer.Write(crypto.Keccak256(encodedData)) 231 } else { 232 byteValue, err := typedData.EncodePrimitiveValue(encType, encValue, depth) 233 if err != nil { 234 return nil, err 235 } 236 buffer.Write(byteValue) 237 } 238 } 239 return buffer.Bytes(), nil 240 } 241 242 // Attempt to parse bytes in different formats: byte array, hex string, hexutil.Bytes. 243 func parseBytes(encType interface{}) ([]byte, bool) { 244 switch v := encType.(type) { 245 case []byte: 246 return v, true 247 case hexutil.Bytes: 248 return v, true 249 case string: 250 bytes, err := hexutil.Decode(v) 251 if err != nil { 252 return nil, false 253 } 254 return bytes, true 255 default: 256 return nil, false 257 } 258 } 259 260 func parseInteger(encType string, encValue interface{}) (*big.Int, error) { 261 var ( 262 length int 263 signed = strings.HasPrefix(encType, "int") 264 b *big.Int 265 ) 266 if encType == "int" || encType == "uint" { 267 length = 256 268 } else { 269 lengthStr := "" 270 if strings.HasPrefix(encType, "uint") { 271 lengthStr = strings.TrimPrefix(encType, "uint") 272 } else { 273 lengthStr = strings.TrimPrefix(encType, "int") 274 } 275 atoiSize, err := strconv.Atoi(lengthStr) 276 if err != nil { 277 return nil, fmt.Errorf("invalid size on integer: %v", lengthStr) 278 } 279 length = atoiSize 280 } 281 switch v := encValue.(type) { 282 case *math.HexOrDecimal256: 283 b = (*big.Int)(v) 284 case string: 285 var hexIntValue math.HexOrDecimal256 286 if err := hexIntValue.UnmarshalText([]byte(v)); err != nil { 287 return nil, err 288 } 289 b = (*big.Int)(&hexIntValue) 290 case float64: 291 // JSON parses non-strings as float64. Fail if we cannot 292 // convert it losslessly 293 if float64(int64(v)) == v { 294 b = big.NewInt(int64(v)) 295 } else { 296 return nil, fmt.Errorf("invalid float value %v for type %v", v, encType) 297 } 298 } 299 if b == nil { 300 return nil, fmt.Errorf("invalid integer value %v/%v for type %v", encValue, reflect.TypeOf(encValue), encType) 301 } 302 if b.BitLen() > length { 303 return nil, fmt.Errorf("integer larger than '%v'", encType) 304 } 305 if !signed && b.Sign() == -1 { 306 return nil, fmt.Errorf("invalid negative value for unsigned type %v", encType) 307 } 308 return b, nil 309 } 310 311 // EncodePrimitiveValue deals with the primitive values found 312 // while searching through the typed data 313 func (typedData *TypedData) EncodePrimitiveValue(encType string, encValue interface{}, depth int) ([]byte, error) { 314 switch encType { 315 case "address": 316 stringValue, ok := encValue.(string) 317 if !ok || !common.IsHexAddress(stringValue) { 318 return nil, dataMismatchError(encType, encValue) 319 } 320 retval := make([]byte, 32) 321 copy(retval[12:], common.HexToAddress(stringValue).Bytes()) 322 return retval, nil 323 case "bool": 324 boolValue, ok := encValue.(bool) 325 if !ok { 326 return nil, dataMismatchError(encType, encValue) 327 } 328 if boolValue { 329 return math.PaddedBigBytes(common.Big1, 32), nil 330 } 331 return math.PaddedBigBytes(common.Big0, 32), nil 332 case "string": 333 strVal, ok := encValue.(string) 334 if !ok { 335 return nil, dataMismatchError(encType, encValue) 336 } 337 return crypto.Keccak256([]byte(strVal)), nil 338 case "bytes": 339 bytesValue, ok := parseBytes(encValue) 340 if !ok { 341 return nil, dataMismatchError(encType, encValue) 342 } 343 return crypto.Keccak256(bytesValue), nil 344 } 345 if strings.HasPrefix(encType, "bytes") { 346 lengthStr := strings.TrimPrefix(encType, "bytes") 347 length, err := strconv.Atoi(lengthStr) 348 if err != nil { 349 return nil, fmt.Errorf("invalid size on bytes: %v", lengthStr) 350 } 351 if length < 0 || length > 32 { 352 return nil, fmt.Errorf("invalid size on bytes: %d", length) 353 } 354 if byteValue, ok := parseBytes(encValue); !ok || len(byteValue) != length { 355 return nil, dataMismatchError(encType, encValue) 356 } else { 357 // Right-pad the bits 358 dst := make([]byte, 32) 359 copy(dst, byteValue) 360 return dst, nil 361 } 362 } 363 if strings.HasPrefix(encType, "int") || strings.HasPrefix(encType, "uint") { 364 b, err := parseInteger(encType, encValue) 365 if err != nil { 366 return nil, err 367 } 368 return math.U256Bytes(b), nil 369 } 370 return nil, fmt.Errorf("unrecognized type '%s'", encType) 371 } 372 373 // dataMismatchError generates an error for a mismatch between 374 // the provided type and data 375 func dataMismatchError(encType string, encValue interface{}) error { 376 return fmt.Errorf("provided data '%v' doesn't match type '%s'", encValue, encType) 377 } 378 379 // validate makes sure the types are sound 380 func (typedData *TypedData) validate() error { 381 if err := typedData.Types.validate(); err != nil { 382 return err 383 } 384 if err := typedData.Domain.validate(); err != nil { 385 return err 386 } 387 return nil 388 } 389 390 // Map generates a map version of the typed data 391 func (typedData *TypedData) Map() map[string]interface{} { 392 dataMap := map[string]interface{}{ 393 "types": typedData.Types, 394 "domain": typedData.Domain.Map(), 395 "primaryType": typedData.PrimaryType, 396 "message": typedData.Message, 397 } 398 return dataMap 399 } 400 401 // Format returns a representation of typedData, which can be easily displayed by a user-interface 402 // without in-depth knowledge about 712 rules 403 func (typedData *TypedData) Format() ([]*NameValueType, error) { 404 domain, err := typedData.formatData("EIP712Domain", typedData.Domain.Map()) 405 if err != nil { 406 return nil, err 407 } 408 ptype, err := typedData.formatData(typedData.PrimaryType, typedData.Message) 409 if err != nil { 410 return nil, err 411 } 412 var nvts []*NameValueType 413 nvts = append(nvts, &NameValueType{ 414 Name: "EIP712Domain", 415 Value: domain, 416 Typ: "domain", 417 }) 418 nvts = append(nvts, &NameValueType{ 419 Name: typedData.PrimaryType, 420 Value: ptype, 421 Typ: "primary type", 422 }) 423 return nvts, nil 424 } 425 426 func (typedData *TypedData) formatData(primaryType string, data map[string]interface{}) ([]*NameValueType, error) { 427 var output []*NameValueType 428 429 // Add field contents. Structs and arrays have special handlers. 430 for _, field := range typedData.Types[primaryType] { 431 encName := field.Name 432 encValue := data[encName] 433 item := &NameValueType{ 434 Name: encName, 435 Typ: field.Type, 436 } 437 if field.isArray() { 438 arrayValue, _ := encValue.([]interface{}) 439 parsedType := field.typeName() 440 for _, v := range arrayValue { 441 if typedData.Types[parsedType] != nil { 442 mapValue, _ := v.(map[string]interface{}) 443 mapOutput, err := typedData.formatData(parsedType, mapValue) 444 if err != nil { 445 return nil, err 446 } 447 item.Value = mapOutput 448 } else { 449 primitiveOutput, err := formatPrimitiveValue(field.Type, encValue) 450 if err != nil { 451 return nil, err 452 } 453 item.Value = primitiveOutput 454 } 455 } 456 } else if typedData.Types[field.Type] != nil { 457 if mapValue, ok := encValue.(map[string]interface{}); ok { 458 mapOutput, err := typedData.formatData(field.Type, mapValue) 459 if err != nil { 460 return nil, err 461 } 462 item.Value = mapOutput 463 } else { 464 item.Value = "<nil>" 465 } 466 } else { 467 primitiveOutput, err := formatPrimitiveValue(field.Type, encValue) 468 if err != nil { 469 return nil, err 470 } 471 item.Value = primitiveOutput 472 } 473 output = append(output, item) 474 } 475 return output, nil 476 } 477 478 func formatPrimitiveValue(encType string, encValue interface{}) (string, error) { 479 switch encType { 480 case "address": 481 if stringValue, ok := encValue.(string); !ok { 482 return "", fmt.Errorf("could not format value %v as address", encValue) 483 } else { 484 return common.HexToAddress(stringValue).String(), nil 485 } 486 case "bool": 487 if boolValue, ok := encValue.(bool); !ok { 488 return "", fmt.Errorf("could not format value %v as bool", encValue) 489 } else { 490 return fmt.Sprintf("%t", boolValue), nil 491 } 492 case "bytes", "string": 493 return fmt.Sprintf("%s", encValue), nil 494 } 495 if strings.HasPrefix(encType, "bytes") { 496 return fmt.Sprintf("%s", encValue), nil 497 } 498 if strings.HasPrefix(encType, "uint") || strings.HasPrefix(encType, "int") { 499 if b, err := parseInteger(encType, encValue); err != nil { 500 return "", err 501 } else { 502 return fmt.Sprintf("%d (%#x)", b, b), nil 503 } 504 } 505 return "", fmt.Errorf("unhandled type %v", encType) 506 } 507 508 // Validate checks if the types object is conformant to the specs 509 func (t Types) validate() error { 510 for typeKey, typeArr := range t { 511 if len(typeKey) == 0 { 512 return fmt.Errorf("empty type key") 513 } 514 for i, typeObj := range typeArr { 515 if len(typeObj.Type) == 0 { 516 return fmt.Errorf("type %q:%d: empty Type", typeKey, i) 517 } 518 if len(typeObj.Name) == 0 { 519 return fmt.Errorf("type %q:%d: empty Name", typeKey, i) 520 } 521 if typeKey == typeObj.Type { 522 return fmt.Errorf("type %q cannot reference itself", typeObj.Type) 523 } 524 if typeObj.isReferenceType() { 525 if _, exist := t[typeObj.typeName()]; !exist { 526 return fmt.Errorf("reference type %q is undefined", typeObj.Type) 527 } 528 if !typedDataReferenceTypeRegexp.MatchString(typeObj.Type) { 529 return fmt.Errorf("unknown reference type %q", typeObj.Type) 530 } 531 } else if !isPrimitiveTypeValid(typeObj.Type) { 532 return fmt.Errorf("unknown type %q", typeObj.Type) 533 } 534 } 535 } 536 return nil 537 } 538 539 // Checks if the primitive value is valid 540 func isPrimitiveTypeValid(primitiveType string) bool { 541 if primitiveType == "address" || 542 primitiveType == "address[]" || 543 primitiveType == "bool" || 544 primitiveType == "bool[]" || 545 primitiveType == "string" || 546 primitiveType == "string[]" { 547 return true 548 } 549 if primitiveType == "bytes" || 550 primitiveType == "bytes[]" || 551 primitiveType == "bytes1" || 552 primitiveType == "bytes1[]" || 553 primitiveType == "bytes2" || 554 primitiveType == "bytes2[]" || 555 primitiveType == "bytes3" || 556 primitiveType == "bytes3[]" || 557 primitiveType == "bytes4" || 558 primitiveType == "bytes4[]" || 559 primitiveType == "bytes5" || 560 primitiveType == "bytes5[]" || 561 primitiveType == "bytes6" || 562 primitiveType == "bytes6[]" || 563 primitiveType == "bytes7" || 564 primitiveType == "bytes7[]" || 565 primitiveType == "bytes8" || 566 primitiveType == "bytes8[]" || 567 primitiveType == "bytes9" || 568 primitiveType == "bytes9[]" || 569 primitiveType == "bytes10" || 570 primitiveType == "bytes10[]" || 571 primitiveType == "bytes11" || 572 primitiveType == "bytes11[]" || 573 primitiveType == "bytes12" || 574 primitiveType == "bytes12[]" || 575 primitiveType == "bytes13" || 576 primitiveType == "bytes13[]" || 577 primitiveType == "bytes14" || 578 primitiveType == "bytes14[]" || 579 primitiveType == "bytes15" || 580 primitiveType == "bytes15[]" || 581 primitiveType == "bytes16" || 582 primitiveType == "bytes16[]" || 583 primitiveType == "bytes17" || 584 primitiveType == "bytes17[]" || 585 primitiveType == "bytes18" || 586 primitiveType == "bytes18[]" || 587 primitiveType == "bytes19" || 588 primitiveType == "bytes19[]" || 589 primitiveType == "bytes20" || 590 primitiveType == "bytes20[]" || 591 primitiveType == "bytes21" || 592 primitiveType == "bytes21[]" || 593 primitiveType == "bytes22" || 594 primitiveType == "bytes22[]" || 595 primitiveType == "bytes23" || 596 primitiveType == "bytes23[]" || 597 primitiveType == "bytes24" || 598 primitiveType == "bytes24[]" || 599 primitiveType == "bytes25" || 600 primitiveType == "bytes25[]" || 601 primitiveType == "bytes26" || 602 primitiveType == "bytes26[]" || 603 primitiveType == "bytes27" || 604 primitiveType == "bytes27[]" || 605 primitiveType == "bytes28" || 606 primitiveType == "bytes28[]" || 607 primitiveType == "bytes29" || 608 primitiveType == "bytes29[]" || 609 primitiveType == "bytes30" || 610 primitiveType == "bytes30[]" || 611 primitiveType == "bytes31" || 612 primitiveType == "bytes31[]" || 613 primitiveType == "bytes32" || 614 primitiveType == "bytes32[]" { 615 return true 616 } 617 if primitiveType == "int" || 618 primitiveType == "int[]" || 619 primitiveType == "int8" || 620 primitiveType == "int8[]" || 621 primitiveType == "int16" || 622 primitiveType == "int16[]" || 623 primitiveType == "int32" || 624 primitiveType == "int32[]" || 625 primitiveType == "int64" || 626 primitiveType == "int64[]" || 627 primitiveType == "int96" || 628 primitiveType == "int96[]" || 629 primitiveType == "int128" || 630 primitiveType == "int128[]" || 631 primitiveType == "int256" || 632 primitiveType == "int256[]" { 633 return true 634 } 635 if primitiveType == "uint" || 636 primitiveType == "uint[]" || 637 primitiveType == "uint8" || 638 primitiveType == "uint8[]" || 639 primitiveType == "uint16" || 640 primitiveType == "uint16[]" || 641 primitiveType == "uint32" || 642 primitiveType == "uint32[]" || 643 primitiveType == "uint64" || 644 primitiveType == "uint64[]" || 645 primitiveType == "uint96" || 646 primitiveType == "uint96[]" || 647 primitiveType == "uint128" || 648 primitiveType == "uint128[]" || 649 primitiveType == "uint256" || 650 primitiveType == "uint256[]" { 651 return true 652 } 653 return false 654 } 655 656 // validate checks if the given domain is valid, i.e. contains at least 657 // the minimum viable keys and values 658 func (domain *TypedDataDomain) validate() error { 659 if domain.ChainId == nil && len(domain.Name) == 0 && len(domain.Version) == 0 && len(domain.VerifyingContract) == 0 && len(domain.Salt) == 0 { 660 return errors.New("domain is undefined") 661 } 662 663 return nil 664 } 665 666 // Map is a helper function to generate a map version of the domain 667 func (domain *TypedDataDomain) Map() map[string]interface{} { 668 dataMap := map[string]interface{}{} 669 670 if domain.ChainId != nil { 671 dataMap["chainId"] = domain.ChainId 672 } 673 674 if len(domain.Name) > 0 { 675 dataMap["name"] = domain.Name 676 } 677 678 if len(domain.Version) > 0 { 679 dataMap["version"] = domain.Version 680 } 681 682 if len(domain.VerifyingContract) > 0 { 683 dataMap["verifyingContract"] = domain.VerifyingContract 684 } 685 686 if len(domain.Salt) > 0 { 687 dataMap["salt"] = domain.Salt 688 } 689 return dataMap 690 } 691 692 // NameValueType is a very simple struct with Name, Value and Type. It's meant for simple 693 // json structures used to communicate signing-info about typed data with the UI 694 type NameValueType struct { 695 Name string `json:"name"` 696 Value interface{} `json:"value"` 697 Typ string `json:"type"` 698 } 699 700 // Pprint returns a pretty-printed version of nvt 701 func (nvt *NameValueType) Pprint(depth int) string { 702 output := bytes.Buffer{} 703 output.WriteString(strings.Repeat("\u00a0", depth*2)) 704 output.WriteString(fmt.Sprintf("%s [%s]: ", nvt.Name, nvt.Typ)) 705 if nvts, ok := nvt.Value.([]*NameValueType); ok { 706 output.WriteString("\n") 707 for _, next := range nvts { 708 sublevel := next.Pprint(depth + 1) 709 output.WriteString(sublevel) 710 } 711 } else { 712 if nvt.Value != nil { 713 output.WriteString(fmt.Sprintf("%q\n", nvt.Value)) 714 } else { 715 output.WriteString("\n") 716 } 717 } 718 return output.String() 719 }