// Copyright 2016 Luke Shumaker
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sd_id128

import (
	"crypto/rand"
	"io/ioutil"
	"os"
	"strings"
)

// GetRandomUUID generates a random UUID (122 random bits).
//
// For convenience when a valid UUID is needed, rather than returning
// a totally random 128-bit number, this sets the UUID type
// ("variant") to RFC 4122 format, and the sub-type ("version") to
// "4", which is (pseudo) random generation.  This leaves 122 random
// bits.
//
// The pseudorandom number generator used is cryptographically secure.
func GetRandomUUID() (ID128, error) {
	var id ID128
	_, err := rand.Read(id[:])
	if err != nil {
		return ID128{}, err
	}

	// Set the type ("variant") to RFC 4122 format
	id[8] = (id[8] & 0x3F) | 0x80
	// Set the sub-type ("version") to 4 (random generation).
	id[6] = (id[6] & 0x0F) | 0x40

	return id, nil
}

var cachedMachineID ID128

// GetMachineID returns a unique identifier for this operating system
// installation.
//
// This is read as a plain hexadecimal number (with an optional
// trailing newline) from "/etc/machine-id".
//
// Note that unlike every other "Get*ID" function, the ID returned by
// GetMachineID is not guaranteed to be a valid UUID, though values
// generated by systemd v30 and later are valid UUIDs.
//
// See machine-id(5) for more information.
//
// machine-id(5): https://www.freedesktop.org/software/systemd/man/machine-id.html
func GetMachineID() (ID128, error) {
	if cachedMachineID == (ID128{}) {
		str, err := ioutil.ReadFile("/etc/machine-id")
		if err != nil {
			return ID128{}, err
		}
		id, err := ParsePlain(strings.TrimSuffix(string(str), "\n"))
		if err != nil {
			return ID128{}, err
		}
		if id == (ID128{}) {
			return ID128{}, ErrInvalid
		}
		cachedMachineID = id
	}
	return cachedMachineID, nil
}

var cachedBootID ID128

// GetBootUUID returns a UUID unique to this boot; it is generated by
// the kernel once very early in the boot process.
//
// This is read as a UUID (with trailing newline) from
// "/proc/sys/kernel/random/boot_id".
//
// It may be relied on that the Linux kernel sets boot_id to a UUID
// with the UUID type ("variant") set to RFC 4122 format, and the
// sub-type ("version") set appropriately.
//
// See random(4) for more information.
//
// random(4): http://man7.org/linux/man-pages/man4/random.4.html
func GetBootUUID() (ID128, error) {
	if cachedBootID == (ID128{}) {
		str, err := ioutil.ReadFile("/proc/sys/kernel/random/boot_id")
		if err != nil {
			return ID128{}, err
		}
		id, err := ParseUUID(strings.TrimSuffix(string(str), "\n"))
		if err != nil {
			return ID128{}, err
		}
		cachedBootID = id
	}
	return cachedBootID, nil
}

var cachedInvocationID ID128

// GetInvocationUUID returns a UUID unique to this invocation of the
// currently executed service.
//
// This is read from the "INVOCATION_ID" environment variable, which
// is expected to be set by the service manager.
//
// It may be relied on that the service manager sets this to a UUID
// with the UUID type ("variant") set to RFC 4122 format, and the
// sub-type ("version") set appropriately.
//
// The error returned is ErrNone if the service manager did not set
// INVOCATION_ID, or ErrInvalid if the value of INVOCATION_ID could
// not be parsed.
//
// See sd_id128_get_invocation(3) for more information.
//
// sd_id128_get_invocation(3): https://www.freedesktop.org/software/systemd/man/sd_id128_get_invocation.html
//
// BUG(lukeshu): GetInvocationUUID uses a mechanism specified by "what
// systemd currently does", rather than a stable specification.
func GetInvocationUUID() (ID128, error) {
	// BUG(lukeshu): GetInvocationUUID might logically belong in the
	// sd_daemon package, but we defer to the C-language
	// libsystemd's placement of sd_id128_get_invocation() in
	// sd-id128.h.
	if cachedInvocationID == (ID128{}) {
		// BUG(lukeshu): GetInvocationUUID should distrust the
		// environment in certain situations, similarly to GNU
		// libc secure_getenv.
		str, have := os.LookupEnv("INVOCATION_ID")
		if !have {
			return ID128{}, ErrNone
		}
		t, err := Parse(str)
		if err != nil {
			return ID128{}, err
		}
		cachedInvocationID = t
	}
	return cachedInvocationID, nil
}