diff --git a/network/devicenames.go b/network/devicenames.go new file mode 100644 index 00000000000..8df817c0c80 --- /dev/null +++ b/network/devicenames.go @@ -0,0 +1,219 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package network + +import ( + "sort" + "strconv" + "unicode/utf8" +) + +const ( + EOF = iota + LITERAL + NUMBER +) + +type token int + +type deviceNameScanner struct { + src string + + // scanning state + ch rune // current character + offset int // character offset + rdOffset int // reading offset (position of next ch) +} + +type deviceName struct { + name string + tokens []int +} + +type devices []deviceName + +func (d devices) Len() int { + return len(d) +} + +func (d devices) Less(i, j int) bool { + if r := intCompare(d[i].tokens, d[j].tokens); r == -1 { + return true + } else { + return false + } +} + +func (d devices) Swap(i, j int) { + d[i], d[j] = d[j], d[i] +} + +// adapted from runtime/noasm.go +func intCompare(s1, s2 []int) int { + l := len(s1) + if len(s2) < l { + l = len(s2) + } + if l == 0 || &s1[0] == &s2[0] { + goto samebytes + } + for i := 0; i < l; i++ { + c1, c2 := s1[i], s2[i] + if c1 < c2 { + return -1 + } + if c1 > c2 { + return +1 + } + } +samebytes: + if len(s1) < len(s2) { + return -1 + } + if len(s1) > len(s2) { + return +1 + } + return 0 +} + +func (s *deviceNameScanner) init(src string) { + s.src = src + s.ch = ' ' + s.offset = 0 + s.rdOffset = 0 + s.next() +} + +func (s *deviceNameScanner) next() { + if s.rdOffset < len(s.src) { + s.offset = s.rdOffset + r, w := rune(s.src[s.rdOffset]), 1 + s.rdOffset += w + s.ch = r + } else { + s.offset = len(s.src) + s.ch = -1 // EOF + } +} + +func (s *deviceNameScanner) peek() rune { + if s.rdOffset < len(s.src) { + r, _ := rune(s.src[s.rdOffset]), 1 + return r + } + return -1 +} + +func isDigit(ch rune) bool { + return '0' <= ch && ch <= '9' +} + +func (s *deviceNameScanner) scanNumber() string { + // Treat leading zeros as discrete numbers as this aids the + // natural sort ordering. We also only parse whole numbers; + // floating point values are considered an integer- and + // fractional-part. + + if s.ch == '0' && s.peek() == '0' { + s.next() + return "0" + } + + cur := s.offset + + for isDigit(s.ch) { + s.next() + } + + return string(s.src[cur:s.offset]) +} + +func (s *deviceNameScanner) scan() (tok token, lit string) { + switch ch := s.ch; { + case -1 == ch: + return EOF, "" + case '0' <= ch && ch <= '9': + return NUMBER, s.scanNumber() + default: + lit = string(s.ch) + s.next() + return LITERAL, lit + } +} + +func parseDeviceName(src string) deviceName { + var s deviceNameScanner + + s.init(src) + + d := deviceName{name: src} + + for { + tok, lit := s.scan() + switch tok { + case EOF: + return d + case LITERAL: + x, _ := utf8.DecodeRuneInString(lit) + d.tokens = append(d.tokens, int(x)) + case NUMBER: + val, _ := strconv.Atoi(lit) + d.tokens = append(d.tokens, val) + } + } +} + +func parseDeviceNames(args ...string) devices { + devices := make(devices, 0) + + for _, a := range args { + devices = append(devices, parseDeviceName(a)) + } + + return devices +} + +// NaturallySortDeviceNames returns an ordered list of names based on +// a natural ordering where 'natural' is an ordering of the string +// value in alphabetical order, execept that multi-digit numbers are +// ordered as a single character. +// +// For example, sorting: +// +// [ br-eth10 br-eth1 br-eth2 ] +// +// would sort as: +// +// [ br-eth1 br-eth2 br-eth10 ] +// +// In purely alphabetical sorting "br-eth10" would be sorted before +// "br-eth2" because "1" is sorted as smaller than "2", while in +// natural sorting "br-eth2" is sorted before "br-eth10" because "2" +// is sorted as smaller than "10". +// +// This also extends to multiply repeated numbers (e.g., VLANs). +// +// For example, sorting: +// +// [ br-eth2 br-eth10.10 br-eth200.0 br-eth1.0 br-eth2.0 ] +// +// would sort as: +// +// [ br-eth1.0 br-eth2 br-eth2.0 br-eth10.10 br-eth200.0 ] +// +func NaturallySortDeviceNames(names ...string) []string { + if names == nil { + return nil + } + + devices := parseDeviceNames(names...) + sort.Sort(devices) + sortedNames := make([]string, len(devices)) + + for i, v := range devices { + sortedNames[i] = v.name + } + + return sortedNames +} diff --git a/network/devicenames_test.go b/network/devicenames_test.go new file mode 100644 index 00000000000..1cd5f138396 --- /dev/null +++ b/network/devicenames_test.go @@ -0,0 +1,89 @@ +// Copyright 2016 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package network_test + +import ( + gc "gopkg.in/check.v1" + + "github.com/juju/juju/network" +) + +type DeviceNamesSuite struct{} + +var _ = gc.Suite(&DeviceNamesSuite{}) + +func (s *DeviceNamesSuite) TestNaturallySortDeviceNames(c *gc.C) { + for i, test := range []struct { + message string + input []string + expected []string + }{{ + message: "empty input, empty output", + input: []string{}, + expected: []string{}, + }, { + message: "nil input, nil output", + }, { + message: "one input", + input: []string{"a"}, + expected: []string{"a"}, + }, { + message: "two values, no numbers", + input: []string{"b", "a"}, + expected: []string{"a", "b"}, + }, { + message: "two values, mixed content", + input: []string{"b1", "a1"}, + expected: []string{"a1", "b1"}, + }, { + message: "identical values, numbers only", + input: []string{"1", "1", "1", "1"}, + expected: []string{"1", "1", "1", "1"}, + }, { + message: "identical values, mixed content", + input: []string{"a1", "a1", "a1", "a1"}, + expected: []string{"a1", "a1", "a1", "a1"}, + }, { + message: "reversed input", + input: []string{"a10", "a9", "a8", "a7", "a6", "a5", "a4", "a3", "a2", "a1", "a0"}, + expected: []string{"a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10"}, + }, { + message: "multiple numbers per value", + input: []string{"a10.11", "a10.10", "a10.1"}, + expected: []string{"a10.1", "a10.10", "a10.11"}, + }, { + message: "value with leading zero", + input: []string{"a50", "a51.", "a50.31", "a50.4", "a5.034e1", "a50.300"}, + expected: []string{"a5.034e1", "a50", "a50.4", "a50.31", "a50.300", "a51."}, + }, { + message: "value with multiple leading zeros", + input: []string{"a50", "a51.", "a0050.31", "a50.4", "a5.034e1", "a00050.300"}, + expected: []string{"a00050.300", "a0050.31", "a5.034e1", "a50", "a50.4", "a51."}, + }, { + message: "strings with numbers in ascending order", + input: []string{"a2", "a5", "a9", "a1", "a4", "a10", "a6"}, + expected: []string{"a1", "a2", "a4", "a5", "a6", "a9", "a10"}, + }, { + message: "values that look like version numbers", + input: []string{"1.9.9a", "1.11", "1.9.9b", "1.11.4", "1.10.1"}, + expected: []string{"1.9.9a", "1.9.9b", "1.10.1", "1.11", "1.11.4"}, + }, { + message: "bridge device names", + input: []string{"br-eth10", "br-eth2", "br-eth1"}, + expected: []string{"br-eth1", "br-eth2", "br-eth10"}, + }, { + message: "bridge device names with VLAN numbers", + input: []string{"br-eth10.10", "br-eth2.10", "br-eth200", "br-eth1.100", "br-eth1.10"}, + expected: []string{"br-eth1.10", "br-eth1.100", "br-eth2.10", "br-eth10.10", "br-eth200"}, + }, { + message: "bridge device names with leading zero", + input: []string{"br-eth0", "br-eth10.10", "br-eth2.10", "br-eth1.100", "br-eth1.10", "br-eth10"}, + expected: []string{"br-eth0", "br-eth1.10", "br-eth1.100", "br-eth2.10", "br-eth10", "br-eth10.10"}, + }} { + c.Logf("%v: %s", i, test.message) + result := network.NaturallySortDeviceNames(test.input...) + c.Assert(result, gc.HasLen, len(test.input)) + c.Assert(result, gc.DeepEquals, test.expected) + } +}