//go:build ignore

package main

import (
	"bufio"
	"bytes"
	_ "embed"
	"fmt"
	"log"
	"os"
	"regexp"
	"strings"
	"text/template"
)

const (
	// Constants and flags used to generate the column number IDs.
	// These were manually copied from lib/util/pkcs11t.h and pkcs11n.h.
	NSSCK_VENDOR_NSS    = 0x4E534350
	CKA_VENDOR_DEFINED  = 0x80000000
	CKA_NSS             = CKA_VENDOR_DEFINED | NSSCK_VENDOR_NSS
	CKA_TRUST           = CKA_NSS + 0x2000
	CKA_SUBPRIME_BITS   = 0x00000134
	CKF_ARRAY_ATTRIBUTE = 0x40000000
)

// This file contains the knownAttributes array, which is the list of columns.
//
//go:embed nss/lib/softoken/sdb.c
var sdb []byte

// This file defines most of the attributes that have columns in the db.
//
//go:embed nss/lib/util/pkcs11t.h
var pkcs11t []byte

// This file defines a few more attributes with columns in the db.
//
//go:embed nss/lib/util/pkcs11n.h
var pkcs11n []byte

func main() {
	attributes := parseKnownAttributes()
	columns := make(map[string]string, len(attributes))
	for _, attr := range attributes {
		columns[attr] = ""
	}

	err := findDefinitions(pkcs11t, columns)
	if err != nil {
		panic(err)
	}

	err = findDefinitions(pkcs11n, columns)
	if err != nil {
		panic(err)
	}

	for k, v := range columns {
		if v != "" {
			continue
		}
		fmt.Printf("%s column is unset\n", k)
		os.Exit(1)
	}

	const templ = `// generated by nssdb/generate/main.go
package nssdb

var columns = map[string]string{
	{{- range $k, $v := . }}
	{{ printf "%q" $k}}: {{ printf "%q" $v }},
	{{- end }}
}
`
	t, err := template.New("go").Parse(templ)
	if err != nil {
		panic(err)
	}
	t.Execute(os.Stdout, columns)

	log.Printf("Successfully generated column names for %d attributes\n", len(columns))
}

// Get the column attribute names from the array in lib/softoken/sdb.c that
// looks like this:
//
//	static const CK_ATTRIBUTE_TYPE known_attributes[] = {
//	    CKA_CLASS, CKA_TOKEN, CKA_PRIVATE, CKA_LABEL, CKA_APPLICATION,
//	    CKA_VALUE, CKA_OBJECT_ID, CKA_CERTIFICATE_TYPE, CKA_ISSUER,
//	    ...
//	}
func parseKnownAttributes() []string {
	scanner := bufio.NewScanner(bytes.NewReader(sdb))
	var columns []string

	inKnownAttributes := false
	for scanner.Scan() {
		switch {
		case scanner.Text() == "static const CK_ATTRIBUTE_TYPE known_attributes[] = {":
			inKnownAttributes = true
			continue
		case !inKnownAttributes:
			continue
		case scanner.Text() == "};":
			return columns
		default:
			for _, col := range strings.Split(scanner.Text(), ",") {
				col = strings.TrimSpace(col)
				if col == "" {
					continue
				}
				columns = append(columns, col)
			}
		}
	}

	return columns
}

var (
	defineCKA = regexp.MustCompile("^#define (CKA_[0-9A-Z_]+) (.*)")
	ckaNSS    = regexp.MustCompile(`\(CKA_NSS \+ (\d+)\)`)
	ckaTrust  = regexp.MustCompile(`\(CKA_TRUST \+ (\d+)\)`)
	ckfArray  = regexp.MustCompile(`\(CKF_ARRAY_ATTRIBUTE \| 0x([0-9A-Z]+)U?L\)`)
)

// Find the integer used in the sqlite column name for an attribute.
// Examples of the 5 types of lines we have to parse:
//
// #define CKA_PRIVATE 0x00000002UL
// #define CKA_TRUST_DATA_ENCIPHERMENT (CKA_TRUST + 4)
// #define CKA_NSS_EMAIL (CKA_NSS + 2)
// #define CKA_WRAP_TEMPLATE (CKF_ARRAY_ATTRIBUTE | 0x00000211UL)
// #define CKA_SUB_PRIME_BITS CKA_SUBPRIME_BITS
func findDefinitions(file []byte, columns map[string]string) error {
	scanner := bufio.NewScanner(bytes.NewReader(file))
	for scanner.Scan() {
		matches := defineCKA.FindStringSubmatch(scanner.Text())
		if len(matches) != 3 {
			continue
		}
		attr := matches[1]
		if _, ok := columns[attr]; !ok {
			continue
		}
		val := matches[2]
		switch {
		case strings.HasPrefix(val, "0x"):
			// 0x01UL
			// 0x01L
			val = strings.TrimSuffix(val, "L")
			val = strings.TrimSuffix(val, "U")
			var x int
			_, err := fmt.Sscanf(val, "0x%X", &x)
			if err != nil {
				return fmt.Errorf("failed to parse %q: %w", scanner.Text(), err)
			}
			columns[attr] = fmt.Sprintf("a%x", x)

		case ckaNSS.MatchString(scanner.Text()):
			matches := ckaNSS.FindStringSubmatch(scanner.Text())
			var plus int
			_, err := fmt.Sscanf(matches[1], "%d", &plus)
			if err != nil {
				return fmt.Errorf("failed to parse %q: %w", scanner.Text(), err)
			}
			columns[attr] = fmt.Sprintf("a%x", CKA_NSS+plus)

		case ckaTrust.MatchString(scanner.Text()):
			matches := ckaTrust.FindStringSubmatch(scanner.Text())
			var plus int
			_, err := fmt.Sscanf(matches[1], "%d", &plus)
			if err != nil {
				return fmt.Errorf("failed to parse %q: %w", scanner.Text(), err)
			}
			columns[attr] = fmt.Sprintf("a%x", CKA_TRUST+plus)

		case ckfArray.MatchString(scanner.Text()):
			matches := ckfArray.FindStringSubmatch(scanner.Text())
			var x int
			_, err := fmt.Sscanf(matches[1], "%X", &x)
			if err != nil {
				return fmt.Errorf("failed to parse %q: %w", scanner.Text(), err)
			}
			columns[attr] = fmt.Sprintf("a%x", CKF_ARRAY_ATTRIBUTE|x)

		case val == "CKA_SUBPRIME_BITS":
			columns[attr] = fmt.Sprintf("a%x", CKA_SUBPRIME_BITS)

		default:
			return fmt.Errorf("Cannot parse %q", scanner.Text())
		}
	}
	return nil
}
