Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dbb46c3ee | |||
| 30b5a6699a | |||
| d5cee37433 | |||
| e1cb7efbf1 | |||
| 925e0a7124 | |||
| 659f636d01 | |||
| e43c677fba | |||
| 94c55af888 |
@@ -488,6 +488,8 @@ linters:
|
|||||||
linters: [ exhaustive, mnd, revive ]
|
linters: [ exhaustive, mnd, revive ]
|
||||||
- path: 'backoff/backoff_test.go'
|
- path: 'backoff/backoff_test.go'
|
||||||
linters: [ testpackage ]
|
linters: [ testpackage ]
|
||||||
|
- path: "cmd/kgz/main.go"
|
||||||
|
linters: [ unconvert ]
|
||||||
- path: 'dbg/dbg_test.go'
|
- path: 'dbg/dbg_test.go'
|
||||||
linters: [ testpackage ]
|
linters: [ testpackage ]
|
||||||
- path: 'log/logger.go'
|
- path: 'log/logger.go'
|
||||||
|
|||||||
18
CHANGELOG
18
CHANGELOG
@@ -1,5 +1,23 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
|
|
||||||
|
v1.19.0 - 2026-02-12
|
||||||
|
|
||||||
|
Added:
|
||||||
|
- certlib/dump: DisplayCSR function for dumping Certificate Signing Request information.
|
||||||
|
- certlib: MatchKeysCSR function for testing if a CSR's public key matches a private key.
|
||||||
|
|
||||||
|
v1.18.0 - 2025-11-21
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- disable unconvert for kgz, as various platforms complain about it.
|
||||||
|
|
||||||
|
v1.17.2 - 2025-11-21
|
||||||
|
|
||||||
|
Note: 1.17.2 was a mangled release.
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- certlib: fix request configs in testdata.
|
||||||
|
|
||||||
v1.17.1 - 2025-11-21
|
v1.17.1 - 2025-11-21
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -88,12 +89,17 @@ func (cs CertificateRequest) Request(priv crypto.PrivateKey) (*x509.CertificateR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dnsNames := cs.Subject.DNSNames
|
||||||
|
if isFQDN(cs.Subject.CommonName) && !slices.Contains(dnsNames, cs.Subject.CommonName) {
|
||||||
|
dnsNames = append(dnsNames, cs.Subject.CommonName)
|
||||||
|
}
|
||||||
|
|
||||||
req := &x509.CertificateRequest{
|
req := &x509.CertificateRequest{
|
||||||
PublicKeyAlgorithm: 0,
|
PublicKeyAlgorithm: 0,
|
||||||
PublicKey: getPublic(priv),
|
PublicKey: getPublic(priv),
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
EmailAddresses: cs.Subject.Email,
|
EmailAddresses: cs.Subject.Email,
|
||||||
DNSNames: cs.Subject.DNSNames,
|
DNSNames: dnsNames,
|
||||||
IPAddresses: ipAddresses,
|
IPAddresses: ipAddresses,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,11 +131,13 @@ func (cs CertificateRequest) Generate() (crypto.PrivateKey, *x509.CertificateReq
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
IsCA bool `yaml:"is_ca"`
|
IsCA bool `yaml:"is_ca"`
|
||||||
PathLen int `yaml:"path_len"`
|
PathLen int `yaml:"path_len"`
|
||||||
KeyUse []string `yaml:"key_uses"`
|
KeyUse []string `yaml:"key_uses"`
|
||||||
ExtKeyUsages []string `yaml:"ext_key_usages"`
|
ExtKeyUsages []string `yaml:"ext_key_usages"`
|
||||||
Expiry string `yaml:"expiry"`
|
Expiry string `yaml:"expiry"`
|
||||||
|
OCSPServer []string `yaml:"ocsp_server,omitempty"`
|
||||||
|
IssuingCertificateURL []string `yaml:"issuing_certificate_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Profile) templateFromRequest(req *x509.CertificateRequest) (*x509.Certificate, error) {
|
func (p Profile) templateFromRequest(req *x509.CertificateRequest) (*x509.Certificate, error) {
|
||||||
@@ -175,6 +183,13 @@ func (p Profile) templateFromRequest(req *x509.CertificateRequest) (*x509.Certif
|
|||||||
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, eku)
|
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, eku)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(p.OCSPServer) > 0 {
|
||||||
|
certTemplate.OCSPServer = p.OCSPServer
|
||||||
|
}
|
||||||
|
if len(p.IssuingCertificateURL) > 0 {
|
||||||
|
certTemplate.IssuingCertificateURL = p.IssuingCertificateURL
|
||||||
|
}
|
||||||
|
|
||||||
return certTemplate, nil
|
return certTemplate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +225,32 @@ func (p Profile) SelfSign(req *x509.CertificateRequest, priv crypto.PrivateKey)
|
|||||||
return p.SignRequest(certTemplate, req, priv)
|
return p.SignRequest(certTemplate, req, priv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isFQDN returns true if s looks like a fully-qualified domain name.
|
||||||
|
func isFQDN(s string) bool {
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Must contain at least one dot and no spaces.
|
||||||
|
if !strings.Contains(s, ".") || strings.ContainsAny(s, " \t") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Each label must be non-empty and consist of letters, digits, or hyphens.
|
||||||
|
for label := range strings.SplitSeq(strings.TrimSuffix(s, "."), ".") {
|
||||||
|
if label == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, c := range label {
|
||||||
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if label[0] == '-' || label[len(label)-1] == '-' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func SerialNumber() (*big.Int, error) {
|
func SerialNumber() (*big.Int, error) {
|
||||||
serialNumberBytes := make([]byte, 20)
|
serialNumberBytes := make([]byte, 20)
|
||||||
_, err := rand.Read(serialNumberBytes)
|
_, err := rand.Read(serialNumberBytes)
|
||||||
|
|||||||
188
certlib/certgen/config_test.go
Normal file
188
certlib/certgen/config_test.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package certgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsFQDN(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"example.com", true},
|
||||||
|
{"sub.example.com", true},
|
||||||
|
{"example.com.", true}, // trailing dot
|
||||||
|
{"localhost", false}, // no dot
|
||||||
|
{"", false},
|
||||||
|
{"foo bar.com", false}, // space
|
||||||
|
{"-bad.com", false}, // leading hyphen
|
||||||
|
{"bad-.com", false}, // trailing hyphen
|
||||||
|
{"a..b.com", false}, // empty label
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := isFQDN(tt.input)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("isFQDN(%q) = %v, want %v", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestAddsFQDNAsDNSSAN(t *testing.T) {
|
||||||
|
creq := &CertificateRequest{
|
||||||
|
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
|
||||||
|
Subject: Subject{
|
||||||
|
CommonName: "example.com",
|
||||||
|
Organization: "Test Org",
|
||||||
|
},
|
||||||
|
Profile: Profile{
|
||||||
|
Expiry: "1h",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, req, err := creq.Generate()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.Contains(req.DNSNames, "example.com") {
|
||||||
|
t.Errorf("expected DNS SAN to contain %q, got %v", "example.com", req.DNSNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestFQDNNotDuplicated(t *testing.T) {
|
||||||
|
creq := &CertificateRequest{
|
||||||
|
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
|
||||||
|
Subject: Subject{
|
||||||
|
CommonName: "example.com",
|
||||||
|
Organization: "Test Org",
|
||||||
|
DNSNames: []string{"example.com", "www.example.com"},
|
||||||
|
},
|
||||||
|
Profile: Profile{
|
||||||
|
Expiry: "1h",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, req, err := creq.Generate()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for _, name := range req.DNSNames {
|
||||||
|
if name == "example.com" {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count != 1 {
|
||||||
|
t.Errorf("expected exactly 1 occurrence of %q in DNS SANs, got %d: %v", "example.com", count, req.DNSNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProfileAIAFieldsInCertificate(t *testing.T) {
|
||||||
|
caKey := KeySpec{Algorithm: "ecdsa", Size: 256}
|
||||||
|
_, caPriv, err := caKey.Generate()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate CA key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caProfile := Profile{
|
||||||
|
IsCA: true,
|
||||||
|
PathLen: 1,
|
||||||
|
KeyUse: []string{"cert sign", "crl sign"},
|
||||||
|
Expiry: "8760h",
|
||||||
|
}
|
||||||
|
|
||||||
|
caReq := &CertificateRequest{
|
||||||
|
KeySpec: caKey,
|
||||||
|
Subject: Subject{CommonName: "Test CA", Organization: "Test"},
|
||||||
|
Profile: caProfile,
|
||||||
|
}
|
||||||
|
caCSR, err := caReq.Request(caPriv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate CA CSR: %v", err)
|
||||||
|
}
|
||||||
|
caCert, err := caProfile.SelfSign(caCSR, caPriv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("self-sign CA: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
leafProfile := Profile{
|
||||||
|
KeyUse: []string{"digital signature"},
|
||||||
|
ExtKeyUsages: []string{"server auth"},
|
||||||
|
Expiry: "24h",
|
||||||
|
OCSPServer: []string{"https://ocsp.example.com"},
|
||||||
|
IssuingCertificateURL: []string{"https://pki.example.com/ca.pem"},
|
||||||
|
}
|
||||||
|
|
||||||
|
leafReq := &CertificateRequest{
|
||||||
|
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
|
||||||
|
Subject: Subject{CommonName: "leaf.example.com", Organization: "Test"},
|
||||||
|
Profile: leafProfile,
|
||||||
|
}
|
||||||
|
_, leafCSR, err := leafReq.Generate()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate leaf CSR: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
leafCert, err := leafProfile.SignRequest(caCert, leafCSR, caPriv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sign leaf: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(leafCert.OCSPServer) != 1 || leafCert.OCSPServer[0] != "https://ocsp.example.com" {
|
||||||
|
t.Errorf("OCSPServer = %v, want [https://ocsp.example.com]", leafCert.OCSPServer)
|
||||||
|
}
|
||||||
|
if len(leafCert.IssuingCertificateURL) != 1 || leafCert.IssuingCertificateURL[0] != "https://pki.example.com/ca.pem" {
|
||||||
|
t.Errorf("IssuingCertificateURL = %v, want [https://pki.example.com/ca.pem]", leafCert.IssuingCertificateURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProfileWithoutAIAOmitsExtension(t *testing.T) {
|
||||||
|
profile := Profile{
|
||||||
|
KeyUse: []string{"digital signature"},
|
||||||
|
ExtKeyUsages: []string{"server auth"},
|
||||||
|
Expiry: "24h",
|
||||||
|
}
|
||||||
|
|
||||||
|
creq := &CertificateRequest{
|
||||||
|
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
|
||||||
|
Subject: Subject{CommonName: "noaia.example.com", Organization: "Test"},
|
||||||
|
Profile: profile,
|
||||||
|
}
|
||||||
|
cert, _, err := GenerateSelfSigned(creq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cert.OCSPServer) != 0 {
|
||||||
|
t.Errorf("OCSPServer = %v, want empty", cert.OCSPServer)
|
||||||
|
}
|
||||||
|
if len(cert.IssuingCertificateURL) != 0 {
|
||||||
|
t.Errorf("IssuingCertificateURL = %v, want empty", cert.IssuingCertificateURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestNonFQDNCommonNameNotAdded(t *testing.T) {
|
||||||
|
creq := &CertificateRequest{
|
||||||
|
KeySpec: KeySpec{Algorithm: "ecdsa", Size: 256},
|
||||||
|
Subject: Subject{
|
||||||
|
CommonName: "localhost",
|
||||||
|
Organization: "Test Org",
|
||||||
|
},
|
||||||
|
Profile: Profile{
|
||||||
|
Expiry: "1h",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, req, err := creq.Generate()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(req.DNSNames, "localhost") {
|
||||||
|
t.Errorf("expected DNS SANs to not contain %q, got %v", "localhost", req.DNSNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,6 +165,28 @@ func certPublic(cert *x509.Certificate) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func csrPublic(csr *x509.CertificateRequest) string {
|
||||||
|
switch pub := csr.PublicKey.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return fmt.Sprintf("RSA-%d", pub.N.BitLen())
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
switch pub.Curve {
|
||||||
|
case elliptic.P256():
|
||||||
|
return "ECDSA-prime256v1"
|
||||||
|
case elliptic.P384():
|
||||||
|
return "ECDSA-secp384r1"
|
||||||
|
case elliptic.P521():
|
||||||
|
return "ECDSA-secp521r1"
|
||||||
|
default:
|
||||||
|
return "ECDSA (unknown curve)"
|
||||||
|
}
|
||||||
|
case *dsa.PublicKey:
|
||||||
|
return "DSA"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func DisplayName(name pkix.Name) string {
|
func DisplayName(name pkix.Name) string {
|
||||||
var ns []string
|
var ns []string
|
||||||
|
|
||||||
@@ -333,3 +355,36 @@ func DisplayCert(w io.Writer, cert *x509.Certificate, showHash bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DisplayCSR(w io.Writer, csr *x509.CertificateRequest, showHash bool) {
|
||||||
|
fmt.Fprintln(w, "CERTIFICATE REQUEST")
|
||||||
|
if showHash {
|
||||||
|
fmt.Fprintln(w, wrap(fmt.Sprintf("SHA256: %x", sha256.Sum256(csr.Raw)), 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(w, wrap("Subject: "+DisplayName(csr.Subject), 0))
|
||||||
|
fmt.Fprintf(w, "\tSignature algorithm: %s / %s\n", sigAlgoPK(csr.SignatureAlgorithm),
|
||||||
|
sigAlgoHash(csr.SignatureAlgorithm))
|
||||||
|
fmt.Fprintln(w, "Details:")
|
||||||
|
wrapPrint("Public key: "+csrPublic(csr), 1)
|
||||||
|
|
||||||
|
validNames := make([]string, 0, len(csr.DNSNames)+len(csr.EmailAddresses)+len(csr.IPAddresses)+len(csr.URIs))
|
||||||
|
for i := range csr.DNSNames {
|
||||||
|
validNames = append(validNames, "dns:"+csr.DNSNames[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range csr.EmailAddresses {
|
||||||
|
validNames = append(validNames, "email:"+csr.EmailAddresses[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range csr.IPAddresses {
|
||||||
|
validNames = append(validNames, "ip:"+csr.IPAddresses[i].String())
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range csr.URIs {
|
||||||
|
validNames = append(validNames, "uri:"+csr.URIs[i].String())
|
||||||
|
}
|
||||||
|
|
||||||
|
sans := fmt.Sprintf("SANs (%d): %s\n", len(validNames), strings.Join(validNames, ", "))
|
||||||
|
wrapPrint(sans, 1)
|
||||||
|
}
|
||||||
|
|||||||
@@ -133,3 +133,48 @@ func MatchKeys(cert *x509.Certificate, priv crypto.Signer) (bool, string) {
|
|||||||
return false, fmt.Sprintf("unrecognised private key type: %T", priv.Public())
|
return false, fmt.Sprintf("unrecognised private key type: %T", priv.Public())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatchKeysCSR determines whether the CSR's public key matches the given private key.
|
||||||
|
// It returns true if they match; otherwise, it returns false and a human-friendly reason.
|
||||||
|
func MatchKeysCSR(csr *x509.CertificateRequest, priv crypto.Signer) (bool, string) {
|
||||||
|
switch keyPub := priv.Public().(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
switch csrPub := csr.PublicKey.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
if matchRSA(csrPub, keyPub) {
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
return false, "public keys don't match"
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return false, "RSA private key, EC public key"
|
||||||
|
default:
|
||||||
|
return false, fmt.Sprintf("unsupported CSR public key type: %T", csr.PublicKey)
|
||||||
|
}
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
switch csrPub := csr.PublicKey.(type) {
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
if matchECDSA(csrPub, keyPub) {
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
// Determine a more precise reason
|
||||||
|
kc := getECCurve(keyPub)
|
||||||
|
cc := getECCurve(csrPub)
|
||||||
|
if kc == curveInvalid {
|
||||||
|
return false, "invalid private key curve"
|
||||||
|
}
|
||||||
|
if cc == curveRSA {
|
||||||
|
return false, "private key is EC, CSR is RSA"
|
||||||
|
}
|
||||||
|
if kc != cc {
|
||||||
|
return false, "EC curves don't match"
|
||||||
|
}
|
||||||
|
return false, "public keys don't match"
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return false, "private key is EC, CSR is RSA"
|
||||||
|
default:
|
||||||
|
return false, fmt.Sprintf("unsupported CSR public key type: %T", csr.PublicKey)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false, fmt.Sprintf("unrecognised private key type: %T", priv.Public())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
3
certlib/testdata/ec-ca.yaml
vendored
3
certlib/testdata/ec-ca.yaml
vendored
@@ -9,5 +9,6 @@ subject:
|
|||||||
profile:
|
profile:
|
||||||
is_ca: true
|
is_ca: true
|
||||||
path_len: 3
|
path_len: 3
|
||||||
key_uses: cert sign
|
key_uses:
|
||||||
|
- cert sign
|
||||||
expiry: 20y
|
expiry: 20y
|
||||||
|
|||||||
3
certlib/testdata/rsa-ca.yaml
vendored
3
certlib/testdata/rsa-ca.yaml
vendored
@@ -9,5 +9,6 @@ subject:
|
|||||||
profile:
|
profile:
|
||||||
is_ca: true
|
is_ca: true
|
||||||
path_len: 3
|
path_len: 3
|
||||||
key_uses: cert sign
|
key_uses:
|
||||||
|
- cert sign
|
||||||
expiry: 20y
|
expiry: 20y
|
||||||
|
|||||||
46
cmd/ccfind/main.go
Normal file
46
cmd/ccfind/main.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Prompt:
|
||||||
|
// The current main.go should accept a list of paths to search. In each
|
||||||
|
// of those paths, without recursing, it should find all files ending in
|
||||||
|
// C/C++ source extensions and print them one per line.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var extensions = []string{
|
||||||
|
".c", ".cpp", ".cc", ".cxx",
|
||||||
|
".h", ".hpp", ".hh", ".hxx",
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: %s <path> [path...]\n", os.Args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range os.Args[1:] {
|
||||||
|
entries, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := entry.Name()
|
||||||
|
ext := filepath.Ext(name)
|
||||||
|
if slices.Contains(extensions, strings.ToLower(ext)) {
|
||||||
|
fmt.Println(filepath.Join(path, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user