// 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
}