├── .travis.yml ├── COPYING ├── README.md ├── main.go ├── run-checks └── uenv ├── env.go └── env_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | install: 4 | - go get -t -v ./... 5 | 6 | script: 7 | - go test -v ./... 8 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Canonical 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | . 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | . 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status][travis-image]][travis-url] 2 | # Read/write uboot environment 3 | 4 | Small go package/app to read/write uboot env files that contain crc32 + 1 byte 5 | padding. Unlike fw_{set,print}env it does not needs a 6 | /etc/fw_env.config config file. 7 | 8 | Example of the API: 9 | ``` 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "github.com/mvo5/uboot-go/uenv" 15 | "os" 16 | ) 17 | 18 | func main() { 19 | env, _ := uenv.Open(os.Args[1]) 20 | fmt.Print(env) 21 | env.Set("foo", "bar") 22 | fmt.Print(env) 23 | } 24 | ``` 25 | 26 | Example of the cmdline app for existing files: 27 | ``` 28 | $ uboot-go uboot.env print 29 | initrd_addr=0x88080000 30 | uenvcmd=load mmc ${bootpart} ${loadaddr} snappy-system.txt; env import -t $loadaddr $filesize; run snappy_boot 31 | bootpart=0:1 32 | 33 | $ uboot-go uboot.env set key value 34 | $ uboot-go uboot.env print 35 | initrd_addr=0x88080000 36 | uenvcmd=load mmc ${bootpart} ${loadaddr} snappy-system.txt; env import -t $loadaddr $filesize; run snappy_boot 37 | bootpart=0:1 38 | key=value 39 | 40 | # echo "$(pwd)/uboot.env 0x000 0x20000" > /etc/fw_env.config 41 | $ fw_printenv 42 | initrd_addr=0x88080000 43 | uenvcmd=load mmc ${bootpart} ${loadaddr} snappy-system.txt; env import -t $loadaddr $filesize; run snappy_boot 44 | bootpart=0:1 45 | key=value 46 | ``` 47 | 48 | Example of the cmdline app for creating new env files: 49 | ``` 50 | $ uboot-go uboot.env create 4096 51 | $ uboot-go uboot.env set foo bar 52 | $ uboot-go uboot.env print 53 | foo=bar 54 | ``` 55 | 56 | [travis-image]: https://travis-ci.org/mvo5/uboot-go.svg?branch=master 57 | [travis-url]: https://travis-ci.org/mvo5/uboot-go 58 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strconv" 8 | 9 | "github.com/mvo5/uboot-go/uenv" 10 | ) 11 | 12 | func main() { 13 | // FIXME: argsparse ftw! 14 | envFile := os.Args[1] 15 | cmd := os.Args[2] 16 | 17 | switch cmd { 18 | case "print": 19 | env, err := uenv.Open(envFile) 20 | if err != nil { 21 | log.Fatalf("uenv.Open failed for %s: %s", envFile, err) 22 | } 23 | fmt.Print(env) 24 | case "create": 25 | size, err := strconv.Atoi(os.Args[3]) 26 | if err != nil { 27 | log.Fatalf("Atoi failed for %s: %s", envFile, err) 28 | } 29 | env, err := uenv.Create(envFile, size) 30 | if err != nil { 31 | log.Fatalf("uenv.Create failed for %s: %s", envFile, err) 32 | } 33 | if err := env.Save(); err != nil { 34 | log.Fatalf("env.Save failed: %s", err) 35 | } 36 | 37 | case "set": 38 | env, err := uenv.Open(envFile) 39 | if err != nil { 40 | log.Fatalf("uenv.Open failed for %s: %s", envFile, err) 41 | } 42 | name := os.Args[3] 43 | value := os.Args[4] 44 | env.Set(name, value) 45 | if err := env.Save(); err != nil { 46 | log.Fatalf("env.Save failed for %s: %s", envFile, err) 47 | } 48 | case "import": 49 | env, err := uenv.Open(envFile) 50 | if err != nil { 51 | log.Fatalf("uenv.Open failed for %s: %s", envFile, err) 52 | } 53 | fname := os.Args[3] 54 | r, err := os.Open(fname) 55 | if err != nil { 56 | log.Fatalf("Open failed for %s: %s", fname, err) 57 | } 58 | if err := env.Import(r); err != nil { 59 | log.Fatalf("env.Import failed for %s: %s", envFile, err) 60 | } 61 | if err := env.Save(); err != nil { 62 | log.Fatalf("env.Save failed for %s: %s", envFile, err) 63 | } 64 | default: 65 | log.Fatalf("unknown command %s", cmd) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /run-checks: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if which goctest >/dev/null; then 6 | goctest="goctest" 7 | else 8 | goctest="go test" 9 | fi 10 | 11 | echo Checking formatting 12 | fmt="$(gofmt -l .)" 13 | 14 | if [ -n "$fmt" ]; then 15 | echo "Formatting wrong in following files" 16 | echo "$fmt" 17 | exit 1 18 | fi 19 | 20 | echo Installing godeps 21 | go get launchpad.net/godeps 22 | export PATH=$PATH:$GOPATH/bin 23 | 24 | echo Install golint 25 | go get github.com/golang/lint/golint 26 | export PATH=$PATH:$GOPATH/bin 27 | 28 | echo Building 29 | go build -v . 30 | 31 | 32 | # tests 33 | echo Running tests from $(pwd) 34 | $goctest -v -cover ./... 35 | 36 | 37 | # go vet 38 | echo Running vet 39 | go vet ./... 40 | 41 | # golint 42 | echo Running lint 43 | lint="$(golint ./...)" 44 | if [ -n "$lint" ]; then 45 | echo "Lint complains:" 46 | echo "$lint" 47 | exit 1 48 | fi 49 | -------------------------------------------------------------------------------- /uenv/env.go: -------------------------------------------------------------------------------- 1 | package uenv 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "fmt" 8 | "hash/crc32" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "sort" 13 | "strings" 14 | ) 15 | 16 | // FIXME: add config option for that so that the user can select if 17 | // he/she wants env with or without flags 18 | var headerSize = 5 19 | 20 | // Env contains the data of the uboot environment 21 | type Env struct { 22 | fname string 23 | size int 24 | data map[string]string 25 | } 26 | 27 | // little endian helpers 28 | func readUint32(data []byte) uint32 { 29 | var ret uint32 30 | buf := bytes.NewBuffer(data) 31 | binary.Read(buf, binary.LittleEndian, &ret) 32 | return ret 33 | } 34 | 35 | func writeUint32(u uint32) []byte { 36 | buf := bytes.NewBuffer(nil) 37 | binary.Write(buf, binary.LittleEndian, &u) 38 | return buf.Bytes() 39 | } 40 | 41 | // Create a new empty uboot env file with the given size 42 | func Create(fname string, size int) (*Env, error) { 43 | f, err := os.Create(fname) 44 | if err != nil { 45 | return nil, err 46 | } 47 | defer f.Close() 48 | 49 | env := &Env{ 50 | fname: fname, 51 | size: size, 52 | data: make(map[string]string), 53 | } 54 | 55 | return env, nil 56 | } 57 | 58 | // OpenFlags instructs open how to alter its behavior. 59 | type OpenFlags int 60 | 61 | const ( 62 | // OpenBestEffort instructs OpenWithFlags to skip malformed data without returning an error. 63 | OpenBestEffort OpenFlags = 1 << iota 64 | ) 65 | 66 | // Open opens a existing uboot env file 67 | func Open(fname string) (*Env, error) { 68 | return OpenWithFlags(fname, OpenFlags(0)) 69 | } 70 | 71 | // OpenWithFlags opens a existing uboot env file, passing additional flags. 72 | func OpenWithFlags(fname string, flags OpenFlags) (*Env, error) { 73 | f, err := os.Open(fname) 74 | if err != nil { 75 | return nil, err 76 | } 77 | defer f.Close() 78 | 79 | contentWithHeader, err := ioutil.ReadAll(f) 80 | if err != nil { 81 | return nil, err 82 | } 83 | crc := readUint32(contentWithHeader) 84 | 85 | payload := contentWithHeader[headerSize:] 86 | actualCRC := crc32.ChecksumIEEE(payload) 87 | if crc != actualCRC { 88 | return nil, fmt.Errorf("bad CRC: %v != %v", crc, actualCRC) 89 | } 90 | eof := bytes.Index(payload, []byte{0, 0}) 91 | 92 | data, err := parseData(payload[:eof], flags) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | env := &Env{ 98 | fname: fname, 99 | size: len(contentWithHeader), 100 | data: data, 101 | } 102 | 103 | return env, nil 104 | } 105 | 106 | func parseData(data []byte, flags OpenFlags) (map[string]string, error) { 107 | out := make(map[string]string) 108 | 109 | for _, envStr := range bytes.Split(data, []byte{0}) { 110 | if len(envStr) == 0 || envStr[0] == 0 || envStr[0] == 255 { 111 | continue 112 | } 113 | l := strings.SplitN(string(envStr), "=", 2) 114 | if len(l) != 2 { 115 | if flags&OpenBestEffort == OpenBestEffort { 116 | continue 117 | } 118 | return nil, fmt.Errorf("cannot parse line %q as key=value pair", envStr) 119 | } 120 | key := l[0] 121 | value := l[1] 122 | out[key] = value 123 | } 124 | 125 | return out, nil 126 | } 127 | 128 | func (env *Env) String() string { 129 | out := "" 130 | 131 | env.iterEnv(func(key, value string) { 132 | out += fmt.Sprintf("%s=%s\n", key, value) 133 | }) 134 | 135 | return out 136 | } 137 | 138 | // Get the value of the environment variable 139 | func (env *Env) Get(name string) string { 140 | return env.data[name] 141 | } 142 | 143 | // Set an environment name to the given value, if the value is empty 144 | // the variable will be removed from the environment 145 | func (env *Env) Set(name, value string) { 146 | if name == "" { 147 | panic(fmt.Sprintf("Set() can not be called with empty key for value: %q", value)) 148 | } 149 | if value == "" { 150 | delete(env.data, name) 151 | return 152 | } 153 | env.data[name] = value 154 | } 155 | 156 | // iterEnv calls the passed function f with key, value for environment 157 | // vars. The order is guaranteed (unlike just iterating over the map) 158 | func (env *Env) iterEnv(f func(key, value string)) { 159 | keys := make([]string, 0, len(env.data)) 160 | for k := range env.data { 161 | keys = append(keys, k) 162 | } 163 | sort.Strings(keys) 164 | 165 | for _, k := range keys { 166 | if k == "" { 167 | panic("iterEnv iterating over a empty key") 168 | } 169 | 170 | f(k, env.data[k]) 171 | } 172 | } 173 | 174 | // Save will write out the environment data 175 | func (env *Env) Save() error { 176 | w := bytes.NewBuffer(nil) 177 | // will panic if the buffer can't grow, all writes to 178 | // the buffer will be ok because we sized it correctly 179 | w.Grow(env.size - headerSize) 180 | 181 | // write the payload 182 | env.iterEnv(func(key, value string) { 183 | w.Write([]byte(fmt.Sprintf("%s=%s", key, value))) 184 | w.Write([]byte{0}) 185 | }) 186 | 187 | // write double \0 to mark the end of the env 188 | w.Write([]byte{0}) 189 | 190 | // no keys, so no previous \0 was written so we write one here 191 | if len(env.data) == 0 { 192 | w.Write([]byte{0}) 193 | } 194 | 195 | // write ff into the remaining parts 196 | writtenSoFar := w.Len() 197 | for i := 0; i < env.size-headerSize-writtenSoFar; i++ { 198 | w.Write([]byte{0xff}) 199 | } 200 | 201 | // checksum 202 | crc := crc32.ChecksumIEEE(w.Bytes()) 203 | 204 | // Note that we overwrite the existing file and do not do 205 | // the usual write-rename. The rationale is that we want to 206 | // minimize the amount of writes happening on a potential 207 | // FAT partition where the env is loaded from. The file will 208 | // always be of a fixed size so we know the writes will not 209 | // fail because of ENOSPC. 210 | // 211 | // The size of the env file never changes so we do not 212 | // truncate it. 213 | // 214 | // We also do not O_TRUNC to avoid reallocations on the FS 215 | // to minimize risk of fs corruption. 216 | f, err := os.OpenFile(env.fname, os.O_WRONLY, 0666) 217 | if err != nil { 218 | return err 219 | } 220 | defer f.Close() 221 | 222 | if _, err := f.Write(writeUint32(crc)); err != nil { 223 | return err 224 | } 225 | // padding bytes (e.g. for redundant header) 226 | pad := make([]byte, headerSize-binary.Size(crc)) 227 | if _, err := f.Write(pad); err != nil { 228 | return err 229 | } 230 | if _, err := f.Write(w.Bytes()); err != nil { 231 | return err 232 | } 233 | 234 | return f.Sync() 235 | } 236 | 237 | // Import is a helper that imports a given text file that contains 238 | // "key=value" paris into the uboot env. Lines starting with ^# are 239 | // ignored (like the input file on mkenvimage) 240 | func (env *Env) Import(r io.Reader) error { 241 | scanner := bufio.NewScanner(r) 242 | for scanner.Scan() { 243 | line := scanner.Text() 244 | if strings.HasPrefix(line, "#") || len(line) == 0 { 245 | continue 246 | } 247 | l := strings.SplitN(line, "=", 2) 248 | if len(l) == 1 { 249 | return fmt.Errorf("Invalid line: %q", line) 250 | } 251 | env.data[l[0]] = l[1] 252 | 253 | } 254 | 255 | return scanner.Err() 256 | } 257 | -------------------------------------------------------------------------------- /uenv/env_test.go: -------------------------------------------------------------------------------- 1 | package uenv 2 | 3 | import ( 4 | "bytes" 5 | "hash/crc32" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "testing" 11 | 12 | . "gopkg.in/check.v1" 13 | ) 14 | 15 | // Hook up check.v1 into the "go test" runner 16 | func Test(t *testing.T) { TestingT(t) } 17 | 18 | type uenvTestSuite struct { 19 | envFile string 20 | } 21 | 22 | var _ = Suite(&uenvTestSuite{}) 23 | 24 | func (u *uenvTestSuite) SetUpTest(c *C) { 25 | u.envFile = filepath.Join(c.MkDir(), "uboot.env") 26 | } 27 | 28 | func (u *uenvTestSuite) TestSetNoDuplicate(c *C) { 29 | env, err := Create(u.envFile, 4096) 30 | c.Assert(err, IsNil) 31 | env.Set("foo", "bar") 32 | env.Set("foo", "bar") 33 | c.Assert(env.String(), Equals, "foo=bar\n") 34 | } 35 | 36 | func (u *uenvTestSuite) TestOpenEnv(c *C) { 37 | env, err := Create(u.envFile, 4096) 38 | c.Assert(err, IsNil) 39 | env.Set("foo", "bar") 40 | c.Assert(env.String(), Equals, "foo=bar\n") 41 | err = env.Save() 42 | c.Assert(err, IsNil) 43 | 44 | env2, err := Open(u.envFile) 45 | c.Assert(err, IsNil) 46 | c.Assert(env2.String(), Equals, "foo=bar\n") 47 | } 48 | 49 | func (u *uenvTestSuite) TestGetSimple(c *C) { 50 | env, err := Create(u.envFile, 4096) 51 | c.Assert(err, IsNil) 52 | env.Set("foo", "bar") 53 | c.Assert(env.Get("foo"), Equals, "bar") 54 | } 55 | 56 | func (u *uenvTestSuite) TestGetNoSuchEntry(c *C) { 57 | env, err := Create(u.envFile, 4096) 58 | c.Assert(err, IsNil) 59 | c.Assert(env.Get("no-such-entry"), Equals, "") 60 | } 61 | 62 | func (u *uenvTestSuite) TestImport(c *C) { 63 | env, err := Create(u.envFile, 4096) 64 | c.Assert(err, IsNil) 65 | 66 | r := strings.NewReader("foo=bar\n#comment\n\nbaz=baz") 67 | err = env.Import(r) 68 | c.Assert(err, IsNil) 69 | // order is alphabetic 70 | c.Assert(env.String(), Equals, "baz=baz\nfoo=bar\n") 71 | } 72 | 73 | func (u *uenvTestSuite) TestImportHasError(c *C) { 74 | env, err := Create(u.envFile, 4096) 75 | c.Assert(err, IsNil) 76 | 77 | r := strings.NewReader("foxy") 78 | err = env.Import(r) 79 | c.Assert(err, ErrorMatches, "Invalid line: \"foxy\"") 80 | } 81 | 82 | func (u *uenvTestSuite) TestSetEmptyUnsets(c *C) { 83 | env, err := Create(u.envFile, 4096) 84 | c.Assert(err, IsNil) 85 | 86 | env.Set("foo", "bar") 87 | c.Assert(env.String(), Equals, "foo=bar\n") 88 | env.Set("foo", "") 89 | c.Assert(env.String(), Equals, "") 90 | } 91 | 92 | func (u *uenvTestSuite) makeUbootEnvFromData(c *C, mockData []byte) { 93 | w := bytes.NewBuffer(nil) 94 | crc := crc32.ChecksumIEEE(mockData) 95 | w.Write(writeUint32(crc)) 96 | w.Write([]byte{0}) 97 | w.Write(mockData) 98 | 99 | f, err := os.Create(u.envFile) 100 | c.Assert(err, IsNil) 101 | defer f.Close() 102 | _, err = f.Write(w.Bytes()) 103 | c.Assert(err, IsNil) 104 | } 105 | 106 | // ensure that the data after \0\0 is discarded (except for crc) 107 | func (u *uenvTestSuite) TestReadStopsAfterDoubleNull(c *C) { 108 | mockData := []byte{ 109 | // foo=bar 110 | 0x66, 0x6f, 0x6f, 0x3d, 0x62, 0x61, 0x72, 111 | // eof 112 | 0x00, 0x00, 113 | // junk after eof as written by fw_setenv sometimes 114 | // =b 115 | 0x3d, 62, 116 | // empty 117 | 0xff, 0xff, 118 | } 119 | u.makeUbootEnvFromData(c, mockData) 120 | 121 | env, err := Open(u.envFile) 122 | c.Assert(err, IsNil) 123 | c.Assert(env.String(), Equals, "foo=bar\n") 124 | } 125 | 126 | // ensure that the malformed data is not causing us to panic. 127 | func (u *uenvTestSuite) TestErrorOnMalformedData(c *C) { 128 | mockData := []byte{ 129 | // foo 130 | 0x66, 0x6f, 0x6f, 131 | // eof 132 | 0x00, 0x00, 133 | } 134 | u.makeUbootEnvFromData(c, mockData) 135 | 136 | env, err := Open(u.envFile) 137 | c.Assert(err, ErrorMatches, `cannot parse line "foo" as key=value pair`) 138 | c.Assert(env, IsNil) 139 | } 140 | 141 | // ensure that the malformed data is not causing us to panic. 142 | func (u *uenvTestSuite) TestOpenBestEffort(c *C) { 143 | mockData := []byte{ 144 | // key1=value1 145 | 0x6b, 0x65, 0x79, 0x31, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x31, 0x00, 146 | // foo 147 | 0x66, 0x6f, 0x6f, 0x00, 148 | // key2=value2 149 | 0x6b, 0x65, 0x79, 0x32, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x00, 150 | // eof 151 | 0x00, 0x00, 152 | } 153 | u.makeUbootEnvFromData(c, mockData) 154 | 155 | env, err := OpenWithFlags(u.envFile, OpenBestEffort) 156 | c.Assert(err, IsNil) 157 | c.Assert(env.String(), Equals, "key1=value1\nkey2=value2\n") 158 | } 159 | 160 | func (u *uenvTestSuite) TestReadEmptyFile(c *C) { 161 | mockData := []byte{ 162 | // eof 163 | 0x00, 0x00, 164 | // empty 165 | 0xff, 0xff, 166 | } 167 | u.makeUbootEnvFromData(c, mockData) 168 | 169 | env, err := Open(u.envFile) 170 | c.Assert(err, IsNil) 171 | c.Assert(env.String(), Equals, "") 172 | } 173 | 174 | func (u *uenvTestSuite) TestWritesEmptyFileWithDoubleNewline(c *C) { 175 | env, err := Create(u.envFile, 12) 176 | c.Assert(err, IsNil) 177 | err = env.Save() 178 | c.Assert(err, IsNil) 179 | 180 | r, err := os.Open(u.envFile) 181 | c.Assert(err, IsNil) 182 | defer r.Close() 183 | content, err := ioutil.ReadAll(r) 184 | c.Assert(err, IsNil) 185 | c.Assert(content, DeepEquals, []byte{ 186 | // crc 187 | 0x11, 0x38, 0xb3, 0x89, 188 | // redundant 189 | 0x0, 190 | // eof 191 | 0x0, 0x0, 192 | // footer 193 | 0xff, 0xff, 0xff, 0xff, 0xff, 194 | }) 195 | 196 | env, err = Open(u.envFile) 197 | c.Assert(err, IsNil) 198 | c.Assert(env.String(), Equals, "") 199 | } 200 | 201 | func (u *uenvTestSuite) TestWritesContentCorrectly(c *C) { 202 | totalSize := 16 203 | 204 | env, err := Create(u.envFile, totalSize) 205 | c.Assert(err, IsNil) 206 | env.Set("a", "b") 207 | env.Set("c", "d") 208 | err = env.Save() 209 | c.Assert(err, IsNil) 210 | 211 | r, err := os.Open(u.envFile) 212 | c.Assert(err, IsNil) 213 | defer r.Close() 214 | content, err := ioutil.ReadAll(r) 215 | c.Assert(err, IsNil) 216 | c.Assert(content, DeepEquals, []byte{ 217 | // crc 218 | 0xc7, 0xd9, 0x6b, 0xc5, 219 | // redundant 220 | 0x0, 221 | // a=b 222 | 0x61, 0x3d, 0x62, 223 | // eol 224 | 0x0, 225 | // c=d 226 | 0x63, 0x3d, 0x64, 227 | // eof 228 | 0x0, 0x0, 229 | // footer 230 | 0xff, 0xff, 231 | }) 232 | 233 | env, err = Open(u.envFile) 234 | c.Assert(err, IsNil) 235 | c.Assert(env.String(), Equals, "a=b\nc=d\n") 236 | c.Assert(env.size, Equals, totalSize) 237 | } 238 | --------------------------------------------------------------------------------