mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-04 18:13:42 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			510 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			510 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
 * Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
 | 
						|
 *
 | 
						|
 * Permission to use, copy, modify, and distribute this software for any
 | 
						|
 * purpose with or without fee is hereby granted, provided that the above
 | 
						|
 * copyright notice and this permission notice appear in all copies.
 | 
						|
 *
 | 
						|
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
						|
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
						|
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
						|
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
						|
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
						|
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
						|
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
						|
 */
 | 
						|
 | 
						|
package spew
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"encoding/hex"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"os"
 | 
						|
	"reflect"
 | 
						|
	"regexp"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	// uint8Type is a reflect.Type representing a uint8.  It is used to
 | 
						|
	// convert cgo types to uint8 slices for hexdumping.
 | 
						|
	uint8Type = reflect.TypeOf(uint8(0))
 | 
						|
 | 
						|
	// cCharRE is a regular expression that matches a cgo char.
 | 
						|
	// It is used to detect character arrays to hexdump them.
 | 
						|
	cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`)
 | 
						|
 | 
						|
	// cUnsignedCharRE is a regular expression that matches a cgo unsigned
 | 
						|
	// char.  It is used to detect unsigned character arrays to hexdump
 | 
						|
	// them.
 | 
						|
	cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
 | 
						|
 | 
						|
	// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
 | 
						|
	// It is used to detect uint8_t arrays to hexdump them.
 | 
						|
	cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`)
 | 
						|
)
 | 
						|
 | 
						|
// dumpState contains information about the state of a dump operation.
 | 
						|
type dumpState struct {
 | 
						|
	w                io.Writer
 | 
						|
	depth            int
 | 
						|
	pointers         map[uintptr]int
 | 
						|
	ignoreNextType   bool
 | 
						|
	ignoreNextIndent bool
 | 
						|
	cs               *ConfigState
 | 
						|
}
 | 
						|
 | 
						|
// indent performs indentation according to the depth level and cs.Indent
 | 
						|
// option.
 | 
						|
func (d *dumpState) indent() {
 | 
						|
	if d.ignoreNextIndent {
 | 
						|
		d.ignoreNextIndent = false
 | 
						|
		return
 | 
						|
	}
 | 
						|
	d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth))
 | 
						|
}
 | 
						|
 | 
						|
// unpackValue returns values inside of non-nil interfaces when possible.
 | 
						|
// This is useful for data types like structs, arrays, slices, and maps which
 | 
						|
// can contain varying types packed inside an interface.
 | 
						|
func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
 | 
						|
	if v.Kind() == reflect.Interface && !v.IsNil() {
 | 
						|
		v = v.Elem()
 | 
						|
	}
 | 
						|
	return v
 | 
						|
}
 | 
						|
 | 
						|
// dumpPtr handles formatting of pointers by indirecting them as necessary.
 | 
						|
func (d *dumpState) dumpPtr(v reflect.Value) {
 | 
						|
	// Remove pointers at or below the current depth from map used to detect
 | 
						|
	// circular refs.
 | 
						|
	for k, depth := range d.pointers {
 | 
						|
		if depth >= d.depth {
 | 
						|
			delete(d.pointers, k)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Keep list of all dereferenced pointers to show later.
 | 
						|
	pointerChain := make([]uintptr, 0)
 | 
						|
 | 
						|
	// Figure out how many levels of indirection there are by dereferencing
 | 
						|
	// pointers and unpacking interfaces down the chain while detecting circular
 | 
						|
	// references.
 | 
						|
	nilFound := false
 | 
						|
	cycleFound := false
 | 
						|
	indirects := 0
 | 
						|
	ve := v
 | 
						|
	for ve.Kind() == reflect.Ptr {
 | 
						|
		if ve.IsNil() {
 | 
						|
			nilFound = true
 | 
						|
			break
 | 
						|
		}
 | 
						|
		indirects++
 | 
						|
		addr := ve.Pointer()
 | 
						|
		pointerChain = append(pointerChain, addr)
 | 
						|
		if pd, ok := d.pointers[addr]; ok && pd < d.depth {
 | 
						|
			cycleFound = true
 | 
						|
			indirects--
 | 
						|
			break
 | 
						|
		}
 | 
						|
		d.pointers[addr] = d.depth
 | 
						|
 | 
						|
		ve = ve.Elem()
 | 
						|
		if ve.Kind() == reflect.Interface {
 | 
						|
			if ve.IsNil() {
 | 
						|
				nilFound = true
 | 
						|
				break
 | 
						|
			}
 | 
						|
			ve = ve.Elem()
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Display type information.
 | 
						|
	d.w.Write(openParenBytes)
 | 
						|
	d.w.Write(bytes.Repeat(asteriskBytes, indirects))
 | 
						|
	d.w.Write([]byte(ve.Type().String()))
 | 
						|
	d.w.Write(closeParenBytes)
 | 
						|
 | 
						|
	// Display pointer information.
 | 
						|
	if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
 | 
						|
		d.w.Write(openParenBytes)
 | 
						|
		for i, addr := range pointerChain {
 | 
						|
			if i > 0 {
 | 
						|
				d.w.Write(pointerChainBytes)
 | 
						|
			}
 | 
						|
			printHexPtr(d.w, addr)
 | 
						|
		}
 | 
						|
		d.w.Write(closeParenBytes)
 | 
						|
	}
 | 
						|
 | 
						|
	// Display dereferenced value.
 | 
						|
	d.w.Write(openParenBytes)
 | 
						|
	switch {
 | 
						|
	case nilFound:
 | 
						|
		d.w.Write(nilAngleBytes)
 | 
						|
 | 
						|
	case cycleFound:
 | 
						|
		d.w.Write(circularBytes)
 | 
						|
 | 
						|
	default:
 | 
						|
		d.ignoreNextType = true
 | 
						|
		d.dump(ve)
 | 
						|
	}
 | 
						|
	d.w.Write(closeParenBytes)
 | 
						|
}
 | 
						|
 | 
						|
// dumpSlice handles formatting of arrays and slices.  Byte (uint8 under
 | 
						|
// reflection) arrays and slices are dumped in hexdump -C fashion.
 | 
						|
func (d *dumpState) dumpSlice(v reflect.Value) {
 | 
						|
	// Determine whether this type should be hex dumped or not.  Also,
 | 
						|
	// for types which should be hexdumped, try to use the underlying data
 | 
						|
	// first, then fall back to trying to convert them to a uint8 slice.
 | 
						|
	var buf []uint8
 | 
						|
	doConvert := false
 | 
						|
	doHexDump := false
 | 
						|
	numEntries := v.Len()
 | 
						|
	if numEntries > 0 {
 | 
						|
		vt := v.Index(0).Type()
 | 
						|
		vts := vt.String()
 | 
						|
		switch {
 | 
						|
		// C types that need to be converted.
 | 
						|
		case cCharRE.MatchString(vts):
 | 
						|
			fallthrough
 | 
						|
		case cUnsignedCharRE.MatchString(vts):
 | 
						|
			fallthrough
 | 
						|
		case cUint8tCharRE.MatchString(vts):
 | 
						|
			doConvert = true
 | 
						|
 | 
						|
		// Try to use existing uint8 slices and fall back to converting
 | 
						|
		// and copying if that fails.
 | 
						|
		case vt.Kind() == reflect.Uint8:
 | 
						|
			// We need an addressable interface to convert the type
 | 
						|
			// to a byte slice.  However, the reflect package won't
 | 
						|
			// give us an interface on certain things like
 | 
						|
			// unexported struct fields in order to enforce
 | 
						|
			// visibility rules.  We use unsafe, when available, to
 | 
						|
			// bypass these restrictions since this package does not
 | 
						|
			// mutate the values.
 | 
						|
			vs := v
 | 
						|
			if !vs.CanInterface() || !vs.CanAddr() {
 | 
						|
				vs = unsafeReflectValue(vs)
 | 
						|
			}
 | 
						|
			if !UnsafeDisabled {
 | 
						|
				vs = vs.Slice(0, numEntries)
 | 
						|
 | 
						|
				// Use the existing uint8 slice if it can be
 | 
						|
				// type asserted.
 | 
						|
				iface := vs.Interface()
 | 
						|
				if slice, ok := iface.([]uint8); ok {
 | 
						|
					buf = slice
 | 
						|
					doHexDump = true
 | 
						|
					break
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			// The underlying data needs to be converted if it can't
 | 
						|
			// be type asserted to a uint8 slice.
 | 
						|
			doConvert = true
 | 
						|
		}
 | 
						|
 | 
						|
		// Copy and convert the underlying type if needed.
 | 
						|
		if doConvert && vt.ConvertibleTo(uint8Type) {
 | 
						|
			// Convert and copy each element into a uint8 byte
 | 
						|
			// slice.
 | 
						|
			buf = make([]uint8, numEntries)
 | 
						|
			for i := 0; i < numEntries; i++ {
 | 
						|
				vv := v.Index(i)
 | 
						|
				buf[i] = uint8(vv.Convert(uint8Type).Uint())
 | 
						|
			}
 | 
						|
			doHexDump = true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Hexdump the entire slice as needed.
 | 
						|
	if doHexDump {
 | 
						|
		indent := strings.Repeat(d.cs.Indent, d.depth)
 | 
						|
		str := indent + hex.Dump(buf)
 | 
						|
		str = strings.Replace(str, "\n", "\n"+indent, -1)
 | 
						|
		str = strings.TrimRight(str, d.cs.Indent)
 | 
						|
		d.w.Write([]byte(str))
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// Recursively call dump for each item.
 | 
						|
	for i := 0; i < numEntries; i++ {
 | 
						|
		d.dump(d.unpackValue(v.Index(i)))
 | 
						|
		if i < (numEntries - 1) {
 | 
						|
			d.w.Write(commaNewlineBytes)
 | 
						|
		} else {
 | 
						|
			d.w.Write(newlineBytes)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// dump is the main workhorse for dumping a value.  It uses the passed reflect
 | 
						|
// value to figure out what kind of object we are dealing with and formats it
 | 
						|
// appropriately.  It is a recursive function, however circular data structures
 | 
						|
// are detected and handled properly.
 | 
						|
func (d *dumpState) dump(v reflect.Value) {
 | 
						|
	// Handle invalid reflect values immediately.
 | 
						|
	kind := v.Kind()
 | 
						|
	if kind == reflect.Invalid {
 | 
						|
		d.w.Write(invalidAngleBytes)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// Handle pointers specially.
 | 
						|
	if kind == reflect.Ptr {
 | 
						|
		d.indent()
 | 
						|
		d.dumpPtr(v)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// Print type information unless already handled elsewhere.
 | 
						|
	if !d.ignoreNextType {
 | 
						|
		d.indent()
 | 
						|
		d.w.Write(openParenBytes)
 | 
						|
		d.w.Write([]byte(v.Type().String()))
 | 
						|
		d.w.Write(closeParenBytes)
 | 
						|
		d.w.Write(spaceBytes)
 | 
						|
	}
 | 
						|
	d.ignoreNextType = false
 | 
						|
 | 
						|
	// Display length and capacity if the built-in len and cap functions
 | 
						|
	// work with the value's kind and the len/cap itself is non-zero.
 | 
						|
	valueLen, valueCap := 0, 0
 | 
						|
	switch v.Kind() {
 | 
						|
	case reflect.Array, reflect.Slice, reflect.Chan:
 | 
						|
		valueLen, valueCap = v.Len(), v.Cap()
 | 
						|
	case reflect.Map, reflect.String:
 | 
						|
		valueLen = v.Len()
 | 
						|
	}
 | 
						|
	if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
 | 
						|
		d.w.Write(openParenBytes)
 | 
						|
		if valueLen != 0 {
 | 
						|
			d.w.Write(lenEqualsBytes)
 | 
						|
			printInt(d.w, int64(valueLen), 10)
 | 
						|
		}
 | 
						|
		if !d.cs.DisableCapacities && valueCap != 0 {
 | 
						|
			if valueLen != 0 {
 | 
						|
				d.w.Write(spaceBytes)
 | 
						|
			}
 | 
						|
			d.w.Write(capEqualsBytes)
 | 
						|
			printInt(d.w, int64(valueCap), 10)
 | 
						|
		}
 | 
						|
		d.w.Write(closeParenBytes)
 | 
						|
		d.w.Write(spaceBytes)
 | 
						|
	}
 | 
						|
 | 
						|
	// Call Stringer/error interfaces if they exist and the handle methods flag
 | 
						|
	// is enabled
 | 
						|
	if !d.cs.DisableMethods {
 | 
						|
		if (kind != reflect.Invalid) && (kind != reflect.Interface) {
 | 
						|
			if handled := handleMethods(d.cs, d.w, v); handled {
 | 
						|
				return
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	switch kind {
 | 
						|
	case reflect.Invalid:
 | 
						|
		// Do nothing.  We should never get here since invalid has already
 | 
						|
		// been handled above.
 | 
						|
 | 
						|
	case reflect.Bool:
 | 
						|
		printBool(d.w, v.Bool())
 | 
						|
 | 
						|
	case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
 | 
						|
		printInt(d.w, v.Int(), 10)
 | 
						|
 | 
						|
	case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
 | 
						|
		printUint(d.w, v.Uint(), 10)
 | 
						|
 | 
						|
	case reflect.Float32:
 | 
						|
		printFloat(d.w, v.Float(), 32)
 | 
						|
 | 
						|
	case reflect.Float64:
 | 
						|
		printFloat(d.w, v.Float(), 64)
 | 
						|
 | 
						|
	case reflect.Complex64:
 | 
						|
		printComplex(d.w, v.Complex(), 32)
 | 
						|
 | 
						|
	case reflect.Complex128:
 | 
						|
		printComplex(d.w, v.Complex(), 64)
 | 
						|
 | 
						|
	case reflect.Slice:
 | 
						|
		if v.IsNil() {
 | 
						|
			d.w.Write(nilAngleBytes)
 | 
						|
			break
 | 
						|
		}
 | 
						|
		fallthrough
 | 
						|
 | 
						|
	case reflect.Array:
 | 
						|
		d.w.Write(openBraceNewlineBytes)
 | 
						|
		d.depth++
 | 
						|
		if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
 | 
						|
			d.indent()
 | 
						|
			d.w.Write(maxNewlineBytes)
 | 
						|
		} else {
 | 
						|
			d.dumpSlice(v)
 | 
						|
		}
 | 
						|
		d.depth--
 | 
						|
		d.indent()
 | 
						|
		d.w.Write(closeBraceBytes)
 | 
						|
 | 
						|
	case reflect.String:
 | 
						|
		d.w.Write([]byte(strconv.Quote(v.String())))
 | 
						|
 | 
						|
	case reflect.Interface:
 | 
						|
		// The only time we should get here is for nil interfaces due to
 | 
						|
		// unpackValue calls.
 | 
						|
		if v.IsNil() {
 | 
						|
			d.w.Write(nilAngleBytes)
 | 
						|
		}
 | 
						|
 | 
						|
	case reflect.Ptr:
 | 
						|
		// Do nothing.  We should never get here since pointers have already
 | 
						|
		// been handled above.
 | 
						|
 | 
						|
	case reflect.Map:
 | 
						|
		// nil maps should be indicated as different than empty maps
 | 
						|
		if v.IsNil() {
 | 
						|
			d.w.Write(nilAngleBytes)
 | 
						|
			break
 | 
						|
		}
 | 
						|
 | 
						|
		d.w.Write(openBraceNewlineBytes)
 | 
						|
		d.depth++
 | 
						|
		if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
 | 
						|
			d.indent()
 | 
						|
			d.w.Write(maxNewlineBytes)
 | 
						|
		} else {
 | 
						|
			numEntries := v.Len()
 | 
						|
			keys := v.MapKeys()
 | 
						|
			if d.cs.SortKeys {
 | 
						|
				sortValues(keys, d.cs)
 | 
						|
			}
 | 
						|
			for i, key := range keys {
 | 
						|
				d.dump(d.unpackValue(key))
 | 
						|
				d.w.Write(colonSpaceBytes)
 | 
						|
				d.ignoreNextIndent = true
 | 
						|
				d.dump(d.unpackValue(v.MapIndex(key)))
 | 
						|
				if i < (numEntries - 1) {
 | 
						|
					d.w.Write(commaNewlineBytes)
 | 
						|
				} else {
 | 
						|
					d.w.Write(newlineBytes)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		d.depth--
 | 
						|
		d.indent()
 | 
						|
		d.w.Write(closeBraceBytes)
 | 
						|
 | 
						|
	case reflect.Struct:
 | 
						|
		d.w.Write(openBraceNewlineBytes)
 | 
						|
		d.depth++
 | 
						|
		if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
 | 
						|
			d.indent()
 | 
						|
			d.w.Write(maxNewlineBytes)
 | 
						|
		} else {
 | 
						|
			vt := v.Type()
 | 
						|
			numFields := v.NumField()
 | 
						|
			for i := 0; i < numFields; i++ {
 | 
						|
				d.indent()
 | 
						|
				vtf := vt.Field(i)
 | 
						|
				d.w.Write([]byte(vtf.Name))
 | 
						|
				d.w.Write(colonSpaceBytes)
 | 
						|
				d.ignoreNextIndent = true
 | 
						|
				d.dump(d.unpackValue(v.Field(i)))
 | 
						|
				if i < (numFields - 1) {
 | 
						|
					d.w.Write(commaNewlineBytes)
 | 
						|
				} else {
 | 
						|
					d.w.Write(newlineBytes)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		d.depth--
 | 
						|
		d.indent()
 | 
						|
		d.w.Write(closeBraceBytes)
 | 
						|
 | 
						|
	case reflect.Uintptr:
 | 
						|
		printHexPtr(d.w, uintptr(v.Uint()))
 | 
						|
 | 
						|
	case reflect.UnsafePointer, reflect.Chan, reflect.Func:
 | 
						|
		printHexPtr(d.w, v.Pointer())
 | 
						|
 | 
						|
	// There were not any other types at the time this code was written, but
 | 
						|
	// fall back to letting the default fmt package handle it in case any new
 | 
						|
	// types are added.
 | 
						|
	default:
 | 
						|
		if v.CanInterface() {
 | 
						|
			fmt.Fprintf(d.w, "%v", v.Interface())
 | 
						|
		} else {
 | 
						|
			fmt.Fprintf(d.w, "%v", v.String())
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// fdump is a helper function to consolidate the logic from the various public
 | 
						|
// methods which take varying writers and config states.
 | 
						|
func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
 | 
						|
	for _, arg := range a {
 | 
						|
		if arg == nil {
 | 
						|
			w.Write(interfaceBytes)
 | 
						|
			w.Write(spaceBytes)
 | 
						|
			w.Write(nilAngleBytes)
 | 
						|
			w.Write(newlineBytes)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		d := dumpState{w: w, cs: cs}
 | 
						|
		d.pointers = make(map[uintptr]int)
 | 
						|
		d.dump(reflect.ValueOf(arg))
 | 
						|
		d.w.Write(newlineBytes)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Fdump formats and displays the passed arguments to io.Writer w.  It formats
 | 
						|
// exactly the same as Dump.
 | 
						|
func Fdump(w io.Writer, a ...interface{}) {
 | 
						|
	fdump(&Config, w, a...)
 | 
						|
}
 | 
						|
 | 
						|
// Sdump returns a string with the passed arguments formatted exactly the same
 | 
						|
// as Dump.
 | 
						|
func Sdump(a ...interface{}) string {
 | 
						|
	var buf bytes.Buffer
 | 
						|
	fdump(&Config, &buf, a...)
 | 
						|
	return buf.String()
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
Dump displays the passed parameters to standard out with newlines, customizable
 | 
						|
indentation, and additional debug information such as complete types and all
 | 
						|
pointer addresses used to indirect to the final value.  It provides the
 | 
						|
following features over the built-in printing facilities provided by the fmt
 | 
						|
package:
 | 
						|
 | 
						|
	* Pointers are dereferenced and followed
 | 
						|
	* Circular data structures are detected and handled properly
 | 
						|
	* Custom Stringer/error interfaces are optionally invoked, including
 | 
						|
	  on unexported types
 | 
						|
	* Custom types which only implement the Stringer/error interfaces via
 | 
						|
	  a pointer receiver are optionally invoked when passing non-pointer
 | 
						|
	  variables
 | 
						|
	* Byte arrays and slices are dumped like the hexdump -C command which
 | 
						|
	  includes offsets, byte values in hex, and ASCII output
 | 
						|
 | 
						|
The configuration options are controlled by an exported package global,
 | 
						|
spew.Config.  See ConfigState for options documentation.
 | 
						|
 | 
						|
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
 | 
						|
get the formatted result as a string.
 | 
						|
*/
 | 
						|
func Dump(a ...interface{}) {
 | 
						|
	fdump(&Config, os.Stdout, a...)
 | 
						|
}
 |