diff options
author | EuAndreh <eu@euandre.org> | 2024-09-12 16:13:11 -0300 |
---|---|---|
committer | EuAndreh <eu@euandre.org> | 2024-09-13 05:19:32 -0300 |
commit | 1e62da79d8c61cee0a13df4e16a349ca9bae383c (patch) | |
tree | 43d975c5c2407c2513c317eed4ed26343a24f517 | |
parent | Initial empty commit (diff) | |
download | uuid-1e62da79d8c61cee0a13df4e16a349ca9bae383c.tar.gz uuid-1e62da79d8c61cee0a13df4e16a349ca9bae383c.tar.xz |
Initial version of v4 UUID
Diffstat (limited to '')
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | Makefile | 137 | ||||
-rw-r--r-- | src/guuid.go | 103 | ||||
-rw-r--r-- | tests/guuid.go | 247 | ||||
-rw-r--r-- | tests/libbuild.go | 11 | ||||
-rw-r--r-- | tests/main.go | 7 |
6 files changed, 509 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b60c9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/src/version.go +/src/*.a +/tests/*.a +/tests/*.bin diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9caae43 --- /dev/null +++ b/Makefile @@ -0,0 +1,137 @@ +.POSIX: +DATE = 1970-01-01 +VERSION = 0.1.0 +NAME = guuid +NAME_UC = $(NAME) +LANGUAGES = en +## Installation prefix. Defaults to "/usr". +PREFIX = /usr +BINDIR = $(PREFIX)/bin +LIBDIR = $(PREFIX)/lib +GOLIBDIR = $(LIBDIR)/go +INCLUDEDIR = $(PREFIX)/include +SRCDIR = $(PREFIX)/src/$(NAME) +SHAREDIR = $(PREFIX)/share +LOCALEDIR = $(SHAREDIR)/locale +MANDIR = $(SHAREDIR)/man +EXEC = ./ +## Where to store the installation. Empty by default. +DESTDIR = +LDLIBS = +GOCFLAGS = -I $(GOLIBDIR) +GOLDFLAGS = -L $(GOLIBDIR) + + + +.SUFFIXES: +.SUFFIXES: .go .a .bin .bin-check + + + +objects = \ + src/$(NAME).a \ + tests/$(NAME).a \ + tests/main.a \ + tests/libbuild.a \ + +sources = \ + src/$(NAME).go \ + src/version.go \ + + +derived-assets = \ + src/version.go \ + $(objects) \ + tests/main.bin \ + tests/libbuild.bin \ + +side-assets = \ + + + +## Default target. Builds all artifacts required for testing +## and installation. +all: $(derived-assets) + + +$(objects): Makefile + +src/$(NAME).a: src/$(NAME).go src/version.go + go tool compile $(GOCFLAGS) -o $@ -p $(*F) -I $(@D) $*.go src/version.go + +tests/main.a: tests/main.go tests/$(NAME).a +tests/main.a: + go tool compile $(GOCFLAGS) -o $@ -p $(*F) -I $(@D) $*.go + +tests/libbuild.a: tests/libbuild.go src/$(NAME).a + go tool compile $(GOCFLAGS) -o $@ -p main -I src $*.go + +tests/$(NAME).a: tests/$(NAME).go src/$(NAME).go src/version.go + go tool compile $(GOCFLAGS) -o $@ -p $(*F) $*.go src/$(*F).go src/version.go + +tests/libbuild.bin: tests/libbuild.a + go tool link $(GOLDFLAGS) -o $@ -L src $*.a + +tests/main.bin: tests/main.a + go tool link $(GOLDFLAGS) -o $@ -L $(@D) --extldflags '$(LDLIBS)' $*.a + +src/version.go: Makefile + echo 'package $(NAME); var version = "$(VERSION)"' > $@ + + + +tests.bin-check = \ + tests/main.bin-check \ + tests/libbuild.bin-check \ + +tests/main.bin-check: tests/main.bin +tests/libbuild.bin-check: tests/libbuild.bin +$(tests.bin-check): + $(EXEC)$*.bin + +check-unit: $(tests.bin-check) + + +integration-tests = \ + +.PRECIOUS: $(integration-tests) +$(integration-tests): $(NAME).bin +$(integration-tests): ALWAYS + sh $@ + +check-integration: $(integration-tests) + + +## Run all tests. Each test suite is isolated, so that a parallel +## build can run tests at the same time. The required artifacts +## are created if missing. +check: check-unit check-integration + + + +## Remove *all* derived artifacts produced during the build. +## A dedicated test asserts that this is always true. +clean: + rm -rf $(derived-assets) $(side-assets) + + +## Installs into $(DESTDIR)$(PREFIX). Its dependency target +## ensures that all installable artifacts are crafted beforehand. +install: all + mkdir -p \ + '$(DESTDIR)$(GOLIBDIR)' \ + '$(DESTDIR)$(SRCDIR)' \ + + cp src/$(NAME).a '$(DESTDIR)$(GOLIBDIR)' + cp $(sources) '$(DESTDIR)$(SRCDIR)' + +## Uninstalls from $(DESTDIR)$(PREFIX). This is a perfect mirror +## of the "install" target, and removes *all* that was installed. +## A dedicated test asserts that this is always true. +uninstall: + rm -rf \ + '$(DESTDIR)$(GOLIBDIR)'/$(NAME).a \ + '$(DESTDIR)$(SRCDIR)' \ + + +ALWAYS: diff --git a/src/guuid.go b/src/guuid.go new file mode 100644 index 0000000..9e3e2ba --- /dev/null +++ b/src/guuid.go @@ -0,0 +1,103 @@ +package guuid + +import ( + "crypto/rand" + "encoding/hex" + "errors" + "io" + "strings" +) + + + +const ( + uuidByteCount = 16 + uuidDashCount = 4 + uuidEncodedLength = (uuidByteCount * 2) + uuidDashCount +) + + +var ( + dashIndexes = []int { 8, 13, 18, 23 } + emptyUUID = UUID {} +) + +var ( + BadLengthError = errors.New("guuid: str isn't of the correct length") + BadDashCountError = errors.New("guuid: Bad count of dashes in string") + BadDashPositionError = errors.New("guuid: Bad char in string") +) + + + +type UUID [uuidByteCount]byte + + + +func NewFrom(r io.Reader) (UUID, error) { + var uuid UUID + _, err := io.ReadFull(r, uuid[:]) + if err != nil { + return emptyUUID, err + } + uuid[6] = (uuid[6] & 0x0f) | 0x40 // v4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // variant 10 + return uuid, nil +} + +func New() UUID { + uuid, err := NewFrom(rand.Reader) + if err != nil { + panic(err) + } + return uuid +} + +func (uuid UUID) String() string { + dst := [uuidEncodedLength]byte { + 0, 0, 0, 0, + 0, 0, 0, 0, + '-', + 0, 0, 0, 0, + '-', + 0, 0, 0, 0, + '-', + 0, 0, 0, 0, + '-', + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + } + + hex.Encode(dst[ 0:8], uuid[0:4]) + hex.Encode(dst[ 9:13], uuid[4:6]) + hex.Encode(dst[14:18], uuid[6:8]) + hex.Encode(dst[19:23], uuid[8:10]) + hex.Encode(dst[24:36], uuid[10:]) + + return string(dst[:]) +} + +func FromString(str string) (UUID, error) { + if len(str) != uuidEncodedLength { + return emptyUUID, BadLengthError + } + + if strings.Count(str, "-") != uuidDashCount { + return emptyUUID, BadDashCountError + } + + for _, idx := range dashIndexes { + if str[idx] != '-' { + return emptyUUID, BadDashPositionError + } + } + + hexstr := strings.Join(strings.Split(str, "-"), "") + data, err := hex.DecodeString(hexstr) + if err != nil { + return emptyUUID, err + } + + return [uuidByteCount]byte(data), nil +} diff --git a/tests/guuid.go b/tests/guuid.go new file mode 100644 index 0000000..6470fca --- /dev/null +++ b/tests/guuid.go @@ -0,0 +1,247 @@ +package guuid + +import ( + "bytes" + "encoding/hex" + "fmt" + "os" + "reflect" + "strings" +) + + + +func showColour() bool { + return os.Getenv("NO_COLOUR") == "" +} + +func testStart(name string) { + fmt.Fprintf(os.Stderr, "%s:\n", name) +} + +func testing(message string, body func()) { + if showColour() { + fmt.Fprintf( + os.Stderr, + "\033[0;33mtesting\033[0m: %s... ", + message, + ) + body() + fmt.Fprintf(os.Stderr, "\033[0;32mOK\033[0m.\n") + } else { + fmt.Fprintf(os.Stderr, "testing: %s... ", message) + body() + fmt.Fprintf(os.Stderr, "OK.\n") + } +} + +func assertEq(given any, expected any) { + if !reflect.DeepEqual(given, expected) { + if showColour() { + fmt.Fprintf(os.Stderr, "\033[0;31mERR\033[0m.\n") + } else { + fmt.Fprintf(os.Stderr, "ERR.\n") + } + fmt.Fprintf(os.Stderr, "given != expected\n") + fmt.Fprintf(os.Stderr, "given: %#v\n", given) + fmt.Fprintf(os.Stderr, "expected: %#v\n", expected) + os.Exit(1) + } +} + + +func test_NewFrom() { + testStart("NewFrom()") + + testing("we get the same UUID from the same input", func() { + const s = "abcdefghijklmnop" + + r1 := strings.NewReader(s) + uuid1, err := NewFrom(r1) + assertEq(err, nil) + + r2 := strings.NewReader(s) + uuid2, err := NewFrom(r2) + assertEq(err, nil) + + assertEq(uuid1, uuid2) + }) + + testing("we the bytes in UUID are what the reader gives us", func() { + input := []byte{ + 0x00, + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, + 0x07, + 0x08, + 0x09, + 0x0a, + 0x0b, + 0x0c, + 0x0d, + 0x0e, + 0x0f, + } + + expected := UUID{ + 0x00, + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06 + 0x40, + 0x07, + 0x08 + 0x80, + 0x09, + 0x0a, + 0x0b, + 0x0c, + 0x0d, + 0x0e, + 0x0f, + } + + r := bytes.NewReader(input) + given, err := NewFrom(r) + assertEq(err, nil) + assertEq(given, expected) + }) + + testing("v4 and variant markers", func() { + input := []byte{ + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x71, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + } + + expected := UUID{ + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x41, // not 0x11 + 0x11, + 0xb1, // not 0x11 + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + 0x11, + } + + r := bytes.NewReader(input) + given, err := NewFrom(r) + assertEq(err, nil) + assertEq(given, expected) + }) +} + +func test_New() { + testStart("New()") + + testing("we can generate UUID values: ", func() { + var uuid UUID = New() + assertEq(len(uuid), 16) + }) +} + +func test_String() { + testStart("UUID.String()") + + testing("simple example values", func() { + uuids := []UUID { + UUID { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + }, + UUID { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + }, + UUID { + 222, 222, 222, 222, 222, 222, 222, 222, + 222, 222, 222, 222, 222, 222, 222, 222, + }, + UUID { + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + }, + } + + strs := []string { + "00000000-0000-0000-0000-000000000000", + "00010203-0405-0607-0809-0a0b0c0d0e0f", + "dededede-dede-dede-dede-dededededede", + "ffffffff-ffff-ffff-ffff-ffffffffffff", + } + + for i := range uuids { + s1 := uuids[i].String() + s2 := fmt.Sprintf("%v", uuids[i]) + assertEq(s1, strs[i]) + assertEq(s2, strs[i]) + } + }) +} + +func test_FromString() { + testStart("FromString()") + + testing("UUID -> string -> UUID round trip", func() { + for i := 0; i < 100; i++ { + uuid0 := New() + uuid1, err := FromString(uuid0.String()) + assertEq(err, nil) + assertEq(uuid0, uuid1) + } + }) + + testing("errors we detect", func() { + var err error + + _, err = FromString("") + assertEq(err, BadLengthError) + + _, err = FromString("---000000000000000000000000000000000") + assertEq(err, BadDashCountError) + + _, err = FromString("----00000000000000000000000000000000") + assertEq(err, BadDashPositionError) + + _, err = FromString("00000000-0000-0000-0000-00000000000g") + assertEq(err, hex.InvalidByteError('g')) + + _, err = FromString("00000000-0000-0000-0000-000000000000") + assertEq(err, nil) + }) +} + + +func MainTest() { + test_NewFrom() + test_New() + test_String() + test_FromString() +} diff --git a/tests/libbuild.go b/tests/libbuild.go new file mode 100644 index 0000000..2c02686 --- /dev/null +++ b/tests/libbuild.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + + "guuid" +) + +func main() { + fmt.Println(guuid.New()) +} diff --git a/tests/main.go b/tests/main.go new file mode 100644 index 0000000..b803727 --- /dev/null +++ b/tests/main.go @@ -0,0 +1,7 @@ +package main + +import "guuid" + +func main() { + guuid.MainTest() +} |