summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--util/board_status/go/src/cbfs/cbfs.go239
-rw-r--r--util/board_status/go/src/cbtables/cbtables.go391
-rw-r--r--util/board_status/go/src/kconfig/kconfig.go30
-rw-r--r--util/board_status/go/src/main/board_status.go134
4 files changed, 794 insertions, 0 deletions
diff --git a/util/board_status/go/src/cbfs/cbfs.go b/util/board_status/go/src/cbfs/cbfs.go
new file mode 100644
index 000000000000..042d35fa7205
--- /dev/null
+++ b/util/board_status/go/src/cbfs/cbfs.go
@@ -0,0 +1,239 @@
+package cbfs
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "os"
+ "sort"
+ "strings"
+ "text/tabwriter"
+)
+
+type CBFSReader interface {
+ GetFile(name string) ([]byte, error)
+ ListFiles() ([]string, error)
+}
+
+type ArchType uint32
+type FileType uint32
+
+type CBFSHeader struct {
+ Magic uint32
+ Version uint32
+ ROMSize uint32
+ BootBlockSize uint32
+ Align uint32
+ Offset uint32
+ Architecture ArchType
+ Pad [1]uint32
+}
+
+func (a ArchType) String() string {
+ switch a {
+ case 0xFFFFFFFF:
+ return "unknown"
+ case 0x00000001:
+ return "x86"
+ case 0x00000010:
+ return "arm"
+ default:
+ return fmt.Sprintf("0x%x", a)
+ }
+}
+
+func (f FileType) String() string {
+ switch f {
+ case 0xffffffff:
+ return "null"
+ case 0x10:
+ return "stage"
+ case 0x20:
+ return "payload"
+ case 0x30:
+ return "optionrom"
+ case 0x40:
+ return "bootsplash"
+ case 0x50:
+ return "raw"
+ case 0x51:
+ return "vsa"
+ case 0x52:
+ return "mbi"
+ case 0x53:
+ return "microcode"
+ case 0xaa:
+ return "cmos_default"
+ case 0x1aa:
+ return "cmos_layout"
+ default:
+ return fmt.Sprintf("0x%x", uint32(f))
+ }
+}
+
+func (c CBFSHeader) String() (ret string) {
+ ret = fmt.Sprintf("bootblocksize: %d\n", c.BootBlockSize)
+ ret += fmt.Sprintf("romsize: %d\n", c.ROMSize)
+ ret += fmt.Sprintf("offset: 0x%x\n", c.Offset)
+ ret += fmt.Sprintf("alignment: %d bytes\n", c.Align)
+ ret += fmt.Sprintf("architecture: %v\n", c.Architecture)
+ ret += fmt.Sprintf("version: 0x%x\n", c.Version)
+ return ret
+}
+
+const sizeofFileHeader = 24
+const CBFSHeaderMagic = 0x4F524243
+
+type CBFSFileHeader struct {
+ Magic [8]byte
+ Len uint32
+ Type FileType
+ CheckSum uint32
+ Offset uint32
+}
+
+type cBFSFile struct {
+ headerOffset uint64
+ header CBFSFileHeader
+ name string
+}
+
+type cBFSDesc struct {
+ file *os.File
+ end uint64
+ headerPos uint64
+ rOMStart uint64
+ fileNames map[string]cBFSFile
+ files []cBFSFile
+ header CBFSHeader
+}
+
+func (c cBFSDesc) align(offset uint32) uint32 {
+ a := uint32(c.header.Align)
+ return (a + offset - 1) & ^(a - 1)
+}
+
+func (c cBFSDesc) ListFiles() (files []string, err error) {
+ for name, _ := range c.fileNames {
+ files = append(files, name)
+ }
+ sort.Strings(files)
+ return files, nil
+}
+
+func (c cBFSDesc) GetFile(name string) ([]byte, error) {
+ file, ok := c.fileNames[name]
+ if !ok {
+ return nil, fmt.Errorf("file not found: %s", name)
+ }
+ _, err := c.file.Seek(int64(file.headerOffset)+int64(file.header.Offset), 0)
+ if err != nil {
+ return nil, err
+ }
+ ret := make([]byte, file.header.Len, file.header.Len)
+ r, err := c.file.Read(ret)
+ if err != nil {
+ return nil, err
+ }
+ if r != len(ret) {
+ return nil, fmt.Errorf("incomplete read")
+ }
+ return ret, nil
+}
+
+func (c cBFSDesc) String() (ret string) {
+ ret = c.header.String()
+ ret += "\n"
+ buf := bytes.NewBuffer([]byte{})
+ w := new(tabwriter.Writer)
+ w.Init(buf, 15, 0, 1, ' ', 0)
+ fmt.Fprintln(w, "Name\tOffset\tType\tSize\t")
+ for _, file := range c.files {
+ name := file.name
+ if file.header.Type == 0xffffffff {
+ name = "(empty)"
+ }
+ fmt.Fprintf(w, "%s\t0x%x\t%v\t%d\t\n",
+ name, file.headerOffset-c.rOMStart,
+ file.header.Type, file.header.Len)
+ }
+ w.Flush()
+ ret += buf.String()
+ return ret
+}
+
+func openGeneric(cbfs *cBFSDesc) (CBFSReader, error) {
+ _, err := cbfs.file.Seek(int64(cbfs.end-4), 0)
+ if err != nil {
+ return nil, err
+ }
+ headerPos := int32(0)
+ binary.Read(cbfs.file, binary.LittleEndian, &headerPos)
+ if headerPos < 0 {
+ cbfs.headerPos = cbfs.end - uint64(-headerPos)
+ } else {
+ cbfs.headerPos = uint64(headerPos)
+ }
+ _, err = cbfs.file.Seek(int64(cbfs.headerPos), 0)
+ if err != nil {
+ return nil, err
+ }
+ err = binary.Read(cbfs.file, binary.BigEndian, &cbfs.header)
+ if err != nil {
+ return nil, err
+ }
+ if cbfs.header.Magic != CBFSHeaderMagic {
+ return nil, fmt.Errorf("invalid header magic")
+ }
+
+ cbfs.fileNames = map[string]cBFSFile{}
+
+ curptr := cbfs.end - uint64(cbfs.header.ROMSize) + uint64(cbfs.header.Offset)
+ cbfs.rOMStart = cbfs.end - uint64(cbfs.header.ROMSize)
+ for {
+ file := cBFSFile{headerOffset: curptr}
+ _, err = cbfs.file.Seek(int64(curptr), 0)
+ if err != nil {
+ return nil, err
+ }
+ err = binary.Read(cbfs.file, binary.BigEndian, &file.header)
+ if err != nil {
+ return nil, err
+ }
+ if string(file.header.Magic[:]) != "LARCHIVE" {
+ return *cbfs, nil
+ }
+ name := make([]byte, file.header.Offset-sizeofFileHeader, file.header.Offset-sizeofFileHeader)
+ _, err = cbfs.file.Read(name)
+ if err != nil {
+ return nil, err
+ }
+ nameStr := string(name)
+ idx := strings.Index(nameStr, "\000")
+ if idx >= 0 {
+ nameStr = nameStr[0:idx]
+ }
+ file.name = nameStr
+ cbfs.fileNames[nameStr] = file
+ cbfs.files = append(cbfs.files, file)
+ curptr += uint64(cbfs.align(file.header.Offset + file.header.Len))
+ }
+}
+
+func OpenFile(file *os.File) (CBFSReader, error) {
+ stat, err := file.Stat()
+ if err != nil {
+ return nil, err
+ }
+ cbfs := cBFSDesc{file: file, end: uint64(stat.Size())}
+ return openGeneric(&cbfs)
+}
+
+func OpenROM() (CBFSReader, error) {
+ file, err := os.Open("/dev/mem")
+ if err != nil {
+ return nil, err
+ }
+ cbfs := cBFSDesc{file: file, end: 0x100000000}
+ return openGeneric(&cbfs)
+}
diff --git a/util/board_status/go/src/cbtables/cbtables.go b/util/board_status/go/src/cbtables/cbtables.go
new file mode 100644
index 000000000000..291400617dca
--- /dev/null
+++ b/util/board_status/go/src/cbtables/cbtables.go
@@ -0,0 +1,391 @@
+package cbtables
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "os"
+ "runtime"
+ "strings"
+ "time"
+)
+
+type Header struct {
+ Signature [4]uint8 /* LBIO */
+ HeaderBytes uint32
+ HeaderChecksum uint32
+ TableBytes uint32
+ TableChecksum uint32
+ TableEntries uint32
+}
+
+type Record struct {
+ Tag uint32
+ Size uint32
+}
+
+type rawTable struct {
+ record Record
+ payload []byte
+}
+
+type parsedTables struct {
+ mem *os.File
+ raw []rawTable
+ typeMap map[uint32][]byte
+}
+
+var headerSignature [4]byte = [4]byte{'L', 'B', 'I', 'O'}
+
+const HeaderSize = 24
+const (
+ TagVersion = 0x0004
+ TagForward = 0x0011
+ TagTimestamps = 0x0016
+ TagConsole = 0x0017
+ TagVersionTimestamp = 0x0026
+)
+
+type CBTablesReader interface {
+ GetConsole() (cons []byte, lost uint32, err error)
+ GetTimestamps() (*TimeStamps, error)
+ GetVersion() (string, error)
+ GetVersionTimestamp() (time.Time, error)
+}
+
+type CBMemConsole struct {
+ Size uint32
+ Cursor uint32
+}
+
+type TimeStampEntry struct {
+ EntryID uint32
+ EntryStamp uint64
+}
+
+type TimeStampHeader struct {
+ BaseTime uint64
+ MaxEntries uint32
+ NumEntries uint32
+}
+
+type TimeStamps struct {
+ Head TimeStampHeader
+ Entries []TimeStampEntry
+ FrequencyMHZ uint32
+}
+
+var timeStampNames map[uint32]string = map[uint32]string{
+ 1: "start of rom stage",
+ 2: "before ram initialization",
+ 3: "after ram initialization",
+ 4: "end of romstage",
+ 5: "start of verified boot",
+ 6: "end of verified boot",
+ 8: "start of copying ram stage",
+ 9: "end of copying ram stage",
+ 10: "start of ramstage",
+ 30: "device enumeration",
+ 40: "device configuration",
+ 50: "device enable",
+ 60: "device initialization",
+ 70: "device setup done",
+ 75: "cbmem post",
+ 80: "write tables",
+ 90: "load payload",
+ 98: "ACPI wake jump",
+ 99: "selfboot jump",
+ 1000: "depthcharge start",
+ 1001: "RO parameter init",
+ 1002: "RO vboot init",
+ 1003: "RO vboot select firmware",
+ 1004: "RO vboot select&load kernel",
+ 1010: "RW vboot select&load kernel",
+ 1020: "vboot select&load kernel",
+ 1100: "crossystem data",
+ 1101: "start kernel",
+}
+
+func formatSep(val uint64) string {
+ ret := ""
+ for val > 1000 {
+ ret = fmt.Sprintf(",%03d", val%1000) + ret
+ val /= 1000
+ }
+ ret = fmt.Sprintf("%d", val) + ret
+ return ret
+}
+
+func formatElapsedTime(ticks uint64, frequency uint32) string {
+ if frequency == 0 {
+ return formatSep(ticks) + " cycles"
+ }
+ us := ticks / uint64(frequency)
+ return formatSep(us) + " us"
+}
+
+func (t TimeStamps) String() string {
+ ret := fmt.Sprintf("%d entries total\n\n", len(t.Entries))
+ for i, e := range t.Entries {
+ name, ok := timeStampNames[e.EntryID]
+ if !ok {
+ name = "<unknown>"
+ }
+ ret += fmt.Sprintf("%4d:%-30s %s", e.EntryID, name, formatElapsedTime(e.EntryStamp, t.FrequencyMHZ))
+ if i != 0 {
+ ret += fmt.Sprintf(" (%s)", formatElapsedTime(e.EntryStamp-t.Entries[i-1].EntryStamp, t.FrequencyMHZ))
+ }
+ ret += "\n"
+ }
+ return ret
+}
+
+func getFrequency() uint32 {
+ /* On non-x86 platforms the timestamp entries are in usecs */
+ if runtime.GOARCH != "386" && runtime.GOARCH != "amd64" {
+ return 1
+ }
+
+ cpuf, err := os.Open("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq")
+ if err != nil {
+ return 0
+ }
+
+ freq := uint64(0)
+ fmt.Fscanf(cpuf, "%d", &freq)
+ return uint32(freq / 1000)
+}
+
+func (p parsedTables) GetVersion() (string, error) {
+ str, ok := p.typeMap[TagVersion]
+ if !ok {
+ return "", fmt.Errorf("no coreboot version")
+ }
+ s := string(str)
+ idx := strings.Index(s, "\000")
+ if idx >= 0 {
+ s = s[0:idx]
+ }
+ return s, nil
+}
+
+func (p parsedTables) GetVersionTimestamp() (time.Time, error) {
+ raw, ok := p.typeMap[TagVersionTimestamp]
+ if !ok {
+ return time.Time{}, fmt.Errorf("no coreboot version timestamp")
+ }
+ ts := uint32(0)
+ err := binary.Read(bytes.NewReader(raw), binary.LittleEndian, &ts)
+ if err != nil {
+ return time.Time{}, err
+ }
+ return time.Unix(int64(ts), 0), nil
+}
+
+func (p parsedTables) GetTimestamps() (*TimeStamps, error) {
+ addr := uint64(0)
+ addrRaw, ok := p.typeMap[TagTimestamps]
+ if !ok {
+ return nil, fmt.Errorf("no coreboot console")
+ }
+ err := binary.Read(bytes.NewReader(addrRaw), binary.LittleEndian, &addr)
+ if err != nil {
+ return nil, err
+ }
+ mem := p.mem
+ _, err = mem.Seek(int64(addr), 0)
+ if err != nil {
+ return nil, err
+ }
+ var head TimeStampHeader
+ err = binary.Read(mem, binary.LittleEndian, &head)
+ if err != nil {
+ return nil, err
+ }
+
+ entries := make([]TimeStampEntry, head.NumEntries, head.NumEntries)
+ err = binary.Read(mem, binary.LittleEndian, &entries)
+ if err != nil {
+ return nil, err
+ }
+
+ return &TimeStamps{Head: head, Entries: entries, FrequencyMHZ: getFrequency()}, nil
+}
+
+func (p parsedTables) GetConsole() (console []byte, lost uint32, err error) {
+ addr := uint64(0)
+ addrRaw, ok := p.typeMap[TagConsole]
+ if !ok {
+ return nil, 0, fmt.Errorf("no coreboot console")
+ }
+ err = binary.Read(bytes.NewReader(addrRaw), binary.LittleEndian, &addr)
+ if err != nil {
+ return nil, 0, err
+ }
+ mem := p.mem
+ _, err = mem.Seek(int64(addr), 0)
+ if err != nil {
+ return nil, 0, err
+ }
+ var consDesc CBMemConsole
+ err = binary.Read(mem, binary.LittleEndian, &consDesc)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ readSize := consDesc.Cursor
+ lost = 0
+ if readSize > consDesc.Size {
+ lost = readSize - consDesc.Size
+ readSize = consDesc.Size
+ }
+
+ cons := make([]byte, readSize, readSize)
+ mem.Read(cons)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return cons, lost, nil
+}
+
+func IPChecksum(b []byte) uint16 {
+ sum := uint32(0)
+ /* Oh boy: coreboot really does is little-endian way. */
+ for i := 0; i < len(b); i += 2 {
+ sum += uint32(b[i])
+ }
+ for i := 1; i < len(b); i += 2 {
+ sum += uint32(b[i]) << 8
+ }
+
+ sum = (sum >> 16) + (sum & 0xffff)
+ sum += (sum >> 16)
+ return uint16(^sum & 0xffff)
+}
+
+func readFromBase(mem *os.File, base uint64) ([]byte, error) {
+ _, err := mem.Seek(int64(base), 0)
+ if err != nil {
+ return nil, err
+ }
+ var headRaw [HeaderSize]byte
+ var head Header
+ _, err = mem.Read(headRaw[:])
+ if err != nil {
+ return nil, err
+ }
+
+ err = binary.Read(bytes.NewReader(headRaw[:]), binary.LittleEndian, &head)
+ if err != nil {
+ return nil, err
+ }
+ if bytes.Compare(head.Signature[:], headerSignature[:]) != 0 || head.HeaderBytes == 0 {
+ return nil, nil
+ }
+ if IPChecksum(headRaw[:]) != 0 {
+ return nil, nil
+ }
+ table := make([]byte, head.TableBytes, head.TableBytes)
+ _, err = mem.Seek(int64(base)+int64(head.HeaderBytes), 0)
+ if err != nil {
+ return nil, err
+ }
+ _, err = mem.Read(table)
+ if err != nil {
+ return nil, err
+ }
+
+ if uint32(IPChecksum(table)) != head.TableChecksum {
+ return nil, nil
+ }
+ return table, nil
+}
+
+func scanFromBase(mem *os.File, base uint64) ([]byte, error) {
+ for i := uint64(0); i < 0x1000; i += 0x10 {
+ b, err := readFromBase(mem, base+i)
+ if err != nil {
+ return nil, err
+ }
+ if b != nil {
+ return b, nil
+ }
+ }
+ return nil, fmt.Errorf("no coreboot table found")
+}
+
+func readTables(mem *os.File) ([]byte, error) {
+ switch runtime.GOARCH {
+ case "arm":
+ dt, err := os.Open("/proc/device-tree/firmware/coreboot/coreboot-table")
+ defer dt.Close()
+ if err != nil {
+ return nil, err
+ }
+ var base uint32
+ err = binary.Read(dt, binary.BigEndian, &base)
+ if err != nil {
+ return nil, err
+ }
+ return scanFromBase(mem, uint64(base))
+ case "386", "amd64":
+ tbl, err := scanFromBase(mem, 0)
+ if err == nil {
+ return tbl, nil
+ }
+ return scanFromBase(mem, 0xf0000)
+ default:
+ return nil, fmt.Errorf("unsuppurted arch: %s", runtime.GOARCH)
+ }
+}
+
+func parseTables(mem *os.File, raw []byte) (p parsedTables, err error) {
+ reader := bytes.NewBuffer(raw)
+ p.typeMap = map[uint32][]byte{}
+ for {
+ record := Record{}
+ err = binary.Read(reader, binary.LittleEndian, &record)
+ if err == io.EOF {
+ p.mem = mem
+ return p, nil
+ }
+ if err != nil {
+ return p, err
+ }
+ payload := make([]byte, record.Size-8, record.Size-8)
+ reader.Read(payload)
+ p.raw = append(p.raw, rawTable{record: record, payload: payload})
+ p.typeMap[record.Tag] = payload
+ if record.Tag == TagForward {
+ base := uint64(0)
+ err = binary.Read(bytes.NewBuffer(payload), binary.LittleEndian, &base)
+ if err != nil {
+ return p, err
+ }
+ raw, err := readFromBase(mem, base)
+ if err != nil {
+ return p, err
+ }
+ if raw == nil {
+ return p, fmt.Errorf("no coreboot table found")
+ }
+ reader = bytes.NewBuffer(raw)
+ }
+ }
+}
+
+func Open() (reader CBTablesReader, err error) {
+ mem, err := os.Open("/dev/mem")
+ if err != nil {
+ return nil, err
+ }
+
+ tables, err := readTables(mem)
+ if err != nil {
+ return nil, err
+ }
+
+ return parseTables(mem, tables)
+}
diff --git a/util/board_status/go/src/kconfig/kconfig.go b/util/board_status/go/src/kconfig/kconfig.go
new file mode 100644
index 000000000000..6ce308e61d70
--- /dev/null
+++ b/util/board_status/go/src/kconfig/kconfig.go
@@ -0,0 +1,30 @@
+package kconfig
+
+import (
+ "bufio"
+ "bytes"
+ "strings"
+)
+
+func ParseKConfig(raw []byte) map[string]string {
+ buffer := bytes.NewBuffer(raw)
+
+ scanner := bufio.NewScanner(buffer)
+ ret := map[string]string{}
+ for scanner.Scan() {
+ line := scanner.Text()
+ if line[0] == '#' {
+ continue
+ }
+ idx := strings.Index(line, "=")
+ if idx < 0 {
+ continue
+ }
+ ret[line[0:idx]] = line[idx+1:]
+ }
+ return ret
+}
+
+func UnQuote(in string) string {
+ return in[1 : len(in)-1]
+}
diff --git a/util/board_status/go/src/main/board_status.go b/util/board_status/go/src/main/board_status.go
new file mode 100644
index 000000000000..c52b60f8f7b5
--- /dev/null
+++ b/util/board_status/go/src/main/board_status.go
@@ -0,0 +1,134 @@
+package main
+
+import (
+ "cbfs"
+ "cbtables"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "kconfig"
+ "log"
+ "os"
+ "os/exec"
+)
+
+var ClobberDir = flag.Bool("clobber", false, "Clobber temporary output when finished. Useful for debugging.")
+
+func RunAndSave(output string, name string, arg ...string) {
+ cmd := exec.Command(name, arg...)
+
+ f, err := os.Create(output)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ cmd.Stdout = f
+ cmd.Stderr = f
+
+ err = cmd.Start()
+ if err != nil {
+ log.Fatal(err)
+ }
+ cmd.Wait()
+}
+
+/* Missing features: serial, upload, ssh */
+
+func main() {
+ flag.Parse()
+
+ cb, err := cbfs.OpenROM()
+ if err != nil {
+ log.Fatal(err)
+ }
+ config, err := cb.GetFile("config")
+ if err != nil {
+ log.Fatal(err)
+ }
+ revision, err := cb.GetFile("revision")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ parsedConfig := kconfig.ParseKConfig(config)
+ mainboardDir := kconfig.UnQuote(parsedConfig["CONFIG_MAINBOARD_DIR"])
+
+ tempDir, err := ioutil.TempDir("", "coreboot_board_status")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ tbl, err := cbtables.Open()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ taggedVersion, err := tbl.GetVersion()
+ if err != nil {
+ log.Fatal(err)
+ }
+ versionTimestamp, err := tbl.GetVersionTimestamp()
+ if err != nil {
+ log.Fatal(err)
+ }
+ timestampFormated := versionTimestamp.UTC().Format("2006-01-02T15:04:05Z")
+ outputDir := tempDir + "/" + mainboardDir + "/" + taggedVersion + "/" + timestampFormated
+ os.MkdirAll(outputDir, 0755)
+ fmt.Printf("Temporarily placing output in %s\n", outputDir)
+ cf, err := os.Create(outputDir + "/cbfs.txt")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer cf.Close()
+ fmt.Fprintf(cf, "%v", cb)
+
+ cf, err = os.Create(outputDir + "/config.txt")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer cf.Close()
+ cf.Write(config)
+
+ cf, err = os.Create(outputDir + "/revision.txt")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer cf.Close()
+ cf.Write(revision)
+
+ RunAndSave(outputDir+"/kernel_log.txt", "dmesg")
+
+ cons, lost, err := tbl.GetConsole()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ cf, err = os.Create(outputDir + "/coreboot_console.txt")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer cf.Close()
+ cf.Write(cons)
+ switch lost {
+ case 0:
+ case 1:
+ fmt.Fprintf(cf, "\none byte lost\n")
+ default:
+ fmt.Fprintf(cf, "\n%d bytes lost\n", lost)
+ }
+ timest, err := tbl.GetTimestamps()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ ts, err := os.Create(outputDir + "/coreboot_timestamps.txt")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer ts.Close()
+ fmt.Fprintf(ts, "%v", timest)
+
+ if *ClobberDir {
+ os.RemoveAll(tempDir)
+ }
+}