registry: Go Coverage Report

not tracked not covered covered

    // Package registry handles parsing Windows registry .pol files
    // to convert them to comprehensible entries datastructure for adsys to consume.
    package registry

    import (
            "bufio"
            "bytes"
            "encoding/binary"
            "encoding/json"
            "errors"
            "fmt"
            "io"
            "os"
            "path/filepath"
            "strconv"
            "strings"
            "unicode/utf16"

            "github.com/leonelquinteros/gotext"
            "github.com/ubuntu/adsys/internal/policies/entry"
            "github.com/ubuntu/decorate"
    )

    type dataType uint8

    /* From winNT.h */
    // We want it to be exhaustive even if not imported
    //nolint:deadcode,varcheck
    const (
            regNone              dataType = 0  /* no type */
            regSz                dataType = 1  /* string type (ASCII) */
            regExpandSz          dataType = 2  /* string, includes %ENVVAR% (expanded by caller) (ASCII) */
            regBinary            dataType = 3  /* binary format, callerspecific */
            regDwordLittleEndian dataType = 4  /* DWORD in little endian format */
            regDword             dataType = 4  /* DWORD in little endian format */
            regDwordBigEndian    dataType = 5  /* DWORD in big endian format  */
            regLink              dataType = 6  /* symbolic link (UNICODE) */
            regMultiSz           dataType = 7  /* multiple strings, delimited by \0, terminated by \0\0 (ASCII) */
            regQword             dataType = 11 /* QWORD in little endian format */
            regQwordLittleEndian dataType = 11 /* QWORD in little endian format */
    )

    const (
            policyContainerName      = "metaValues"
            policyWithNoChildrenName = "basic"
    )

    const (
            baseScanTokenSize = bufio.MaxScanTokenSize
            maxScanTokenSize  = int(^uint(0)>>1) / 2
    )

    type meta struct {
            Empty    string
            Meta     string
            Strategy string
    }

    // DecodePolicy parses a policy stream in registry file format and returns a slice of entries.
    func DecodePolicy(f *os.File) (entries []entry.Entry, err error) {
            defer decorate.OnError(&err, gotext.Get("can't parse policy"))

            ent, err := readPolicy(f)
            if err != nil {
                    return nil, err
            }

            var metaValues map[string]meta

            // translate to strings based on type
            var disabledContainer bool
            for _, e := range ent {
                    var res string
                    var disabled bool

                    disabled = strings.HasPrefix(e.key, "**del.")
                    if disabled {
                            e.key = strings.TrimPrefix(e.key, "**del.")
                    }
                    switch e.key {
                    case policyContainerName:
                            disabledContainer = disabled

                            // our supported policyContainerName is only of string type. Discard others which are from other policies
                            if e.dType != regSz {
                                    continue
                            }

                            metaValues, err = getMetaValues(e.data, fmt.Sprintf("%s\\%s", e.path, e.key))
                            if err != nil {
                                    return nil, err
                            }

                            // disabled keys with disabledvalues are not set as DISABLED
                            if _, exists := metaValues["DISABLED"]; exists {
                                    disabledContainer = true
                            }

                            continue

                    case policyWithNoChildrenName:
                            // This is not a container but a single key.

                            // our supported policyWithNoChildrenName is only of string type. Discard others which are from other policies
                            if e.dType != regSz {
                                    metaValues = make(map[string]meta)
                                    continue
                            }

                            // Get metavalues for the single key (with "all: {}")
                            metaValues, err = getMetaValues(e.data, fmt.Sprintf("%s\\%s", e.path, e.key))
                            if err != nil {
                                    return nil, err
                            }

                            // disabled keys with disabledvalues are not set as DISABLED
                            if _, exists := metaValues["DISABLED"]; exists {
                                    disabled = true
                            }

                            // Force it to be considered as an "all" key to apply to every releases
                            e.key = "all"

                            // Reset data to empty as it was the meta content before.
                            e.data = []byte{}

                    default:
                            // propagate disabled value from container to all children elements
                            if disabledContainer {
                                    disabled = true
                            }
                    }
                    e.path = strings.ReplaceAll(e.path, `\`, `/`)

                    // if the key is enabled, load value (or replace with default values for empty results)
                    if !disabled {
                            switch t := e.dType; t {
                            case regSz, regMultiSz:
                                    res, err = decodeUtf16(e.data)
                                    if err != nil {
                                            return nil, err
                                    }
                                    if res == "" {
                                            res = metaValues[e.key].Empty
                                    }
                                    // lines separators for multi lines textbox are \x00
                                    if t == regMultiSz {
                                            res = strings.ReplaceAll(res, "\x00", "\n")
                                    }
                            case regDword:
                                    var resInt uint32
                                    buf := bytes.NewReader(e.data)
                                    if err := binary.Read(buf, binary.LittleEndian, &resInt); err != nil {
                                            return nil, err
                                    }
                                    res = strconv.FormatUint(uint64(resInt), 10)
                            default:
                                    e.err = fmt.Errorf("%d type is not supported for key %s", t, e.key)
                            }
                    }

                    entries = append(entries, entry.Entry{
                            Key:      filepath.Join(e.path, e.key),
                            Value:    res,
                            Disabled: disabled,
                            Meta:     metaValues[e.key].Meta,
                            Strategy: metaValues[e.key].Strategy,
                            Err:      e.err,
                    })
            }

            return entries, nil
    }

    type policyRawEntry struct {
            path  string
            key   string
            dType dataType
            data  []byte
            err   error
    }

    type policyFileHeader struct {
            Signature int32
            Version   int32
    }

    func readPolicy(f *os.File) (entries []policyRawEntry, err error) {
            defer decorate.OnError(&err, gotext.Get("invalid policy"))

            validPolicyFileHeader := policyFileHeader{
                    Signature: 0x67655250,
                    Version:   1,
            }

            header := policyFileHeader{}
            err = binary.Read(f, binary.LittleEndian, &header)
            if err != nil {
                    if errors.Is(err, io.EOF) {
                            return nil, errors.New("empty file")
                    }
                    return nil, err
            }

            if header != validPolicyFileHeader {
                    return nil, fmt.Errorf("file header: %x%x", header.Signature, header.Version)
            }

            // Sometimes, the token size for the scanner might be too small, so let's try increasing it up until a certain limit.
            tokenSize := baseScanTokenSize
            for tokenSize <= maxScanTokenSize {
                    s := bufio.NewScanner(f)
                    s.Buffer(make([]byte, tokenSize), tokenSize)
                    s.Split(scanPolicyEntries)

                    entries, err = scanForPolicies(s)
                    if err == nil {
                            break
                    }
                    if !errors.Is(err, bufio.ErrTooLong) {
                            return nil, err
                    }

                    tokenSize *= 2

                    if _, err := f.Seek(0, 0); err != nil {
                            return nil, fmt.Errorf("could not reset file to retry scanning: %w", err)
                    }
            }
            return entries, nil
    }

    // scanPolicyEntries is a split function for a Scanner that returns each policy entry.
    //
    // It splits the data in the format: [key;value;type;size;data].
    func scanPolicyEntries(data []byte, atEOF bool) (advance int, token []byte, err error) {
            sectionStart := []byte{'[', 0}                 // [ in UTF-16 (little endian)
            sectionEnd := []byte{0, 0, ']', 0}             // \0] in UTF-16 (little endian)
            sectionEndNoNullChar := []byte{';', 0, ']', 0} // ;] in UTF-16 (little endian) - last field can be empty
            dataOffset := len(sectionStart)
            sectionEndWidth := len(sectionEnd)

            // Skip leading sectionStart.
            start := 0
            for ; start+dataOffset-1 < len(data); start++ {
                    if bytes.Equal(data[start:start+dataOffset], sectionStart) {
                            break
                    }
            }

            // Scan until sectionEnd, marking end of word.
            for i := start + dataOffset; i+sectionEndWidth-1 < len(data); i++ {
                    if bytes.Equal(data[i:i+sectionEndWidth], sectionEnd) ||
                            bytes.Equal(data[i:i+sectionEndWidth], sectionEndNoNullChar) {
                            return i + sectionEndWidth, data[start+dataOffset : i+2], nil
                    }
            }

            // If we're at EOF, we have a final, non-empty, non-terminated word. Return an error.
            if atEOF && len(data) > start {
                    // This means that we either increased or need to increase the token size in order to read the entry.
                    if len(data) >= baseScanTokenSize {
                            return 0, nil, bufio.ErrTooLong
                    }
                    return 0, nil, fmt.Errorf("item does not end with ']'")
            }
            // Request more data.
            return start, nil, nil
    }

    func scanForPolicies(s *bufio.Scanner) (entries []policyRawEntry, err error) {
            defer decorate.OnError(&err, gotext.Get("can't read policy entries"))

            delimiter := []byte{0, 0, ';', 0} // \0; in little endian (UTF-16)
            for s.Scan() {
                    var e error

                    elems := bytes.SplitN(s.Bytes(), delimiter, 5)
                    if len(elems) != 5 {
                            return nil, fmt.Errorf("item should contains 5 fields separated by ';': %s", strings.ToValidUTF8(s.Text(), "?"))
                    }

                    keyPrefix, err := decodeUtf16(elems[0])
                    if err != nil {
                            return nil, err
                    }
                    keySuffix, err := decodeUtf16(elems[1])
                    if err != nil {
                            return nil, err
                    }

                    if keyPrefix == "" {
                            return nil, fmt.Errorf("empty key in %s", strings.ToValidUTF8(s.Text(), "?"))
                    }
                    if keySuffix == "" {
                            e = fmt.Errorf("empty value in %s", strings.ToValidUTF8(s.Text(), "?"))
                    }

                    // Copy data to avoid pointing to newer elements on the next loop
                    // This reuse of memory is visible on files bigger than 4106.
                    // (-8 header bytes -> 4098).
                    var data = make([]byte, len(elems[4]))
                    copy(data, elems[4])

                    t := elems[2]
                    if len(t) != 2 {
                            return nil, fmt.Errorf("invalid type: %d", t)
                    }

                    entries = append(entries, policyRawEntry{
                            path:  keyPrefix,
                            key:   keySuffix,
                            dType: dataType(t[0]),
                            data:  data, // TODO: if admx support binary data, then also return size
                            err:   e,
                    })
            }

            if err := s.Err(); err != nil {
                    return nil, err
            }

            return entries, nil
    }

    func decodeUtf16(b []byte) (string, error) {
            if len(b)%2 != 0 {
                    return "", fmt.Errorf("%x is not a valid UTF-16 string", b)
            }
            ints := make([]uint16, len(b)/2)
            if err := binary.Read(bytes.NewReader(b), binary.LittleEndian, &ints); err != nil {
                    return "", err
            }
            // remove trailing \0
            if len(ints) >= 1 && ints[len(ints)-1] == 0 {
                    ints = ints[:len(ints)-1]
            }
            return string(utf16.Decode(ints)), nil
    }

    // getMetaValues returns meta values (including empty value) for options.
    func getMetaValues(data []byte, keypath string) (metaValues map[string]meta, err error) {
            defer decorate.OnError(&err, gotext.Get("can't decode meta value for %s: %v", keypath, err))

            metaValues = make(map[string]meta)
            v, err := decodeUtf16(data)
            if err != nil {
                    return nil, err
            }
            // Return empty dictionary for empty content (or space only)
            if strings.TrimSpace(v) == "" {
                    return metaValues, nil
            }
            if err := json.Unmarshal([]byte(v), &metaValues); err != nil {
                    return nil, err
            }

            return metaValues, nil
    }