Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM golang:1.25.4-trixie

# Install basic dependencies
RUN apt update && \
apt install -y \
git \
curl \
&& \
rm -rf /var/lib/apt/lists/*

# Install task
RUN sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d v3.36.0

# Install golangci-lint
RUN sh -c "$(curl --location https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh)" -- v2.6.2
4 changes: 3 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"image": "golang:1.25.4-trixie",
"build": {
"dockerfile": "Dockerfile"
},
"extensions": [
"golang.go"
]
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/build-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Build Dev Image

on: workflow_dispatch

permissions:
contents: read
packages: write
id-token: write

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Log in to the registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push image
uses: docker/build-push-action@v6
with:
context: .
file: .devcontainer/Dockerfile
tags: ghcr.io/${{ github.repository }}/dev:${{ github.sha }}
push: true
10 changes: 10 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: '3'

tasks:
test:
cmds:
- go test -v ./...

lint:
cmds:
- golangci-lint run
2 changes: 1 addition & 1 deletion loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ func (l *loader) Load(obj any) error {

return fmt.Errorf("failed to open file: %w", err)
}
defer func() { _ = f.Close() }()

// Populate the object by the file.
err = populateByFile(f, string(file.format), obj)
_ = f.Close()
if err != nil {
return fmt.Errorf("failed to load file: %w", err)
}
Expand Down
12 changes: 10 additions & 2 deletions pkg/dotpath/reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ func getValue(v reflect.Value, p string) (reflect.Value, error) {
// Traverse the path.
for len(parts) > 0 {
// If the value is a pointer, dereference it.
for v.Kind() == reflect.Ptr {
for v.Kind() == reflect.Pointer {
if v.IsNil() {
return reflect.Value{}, fmt.Errorf("value is nil at path: %s", p)
}

v = v.Elem()
}

Expand All @@ -70,6 +74,10 @@ func getValue(v reflect.Value, p string) (reflect.Value, error) {
return reflect.Value{}, fmt.Errorf("invalid index: %s", parts[0])
}

if index < 0 || index >= v.Len() {
return reflect.Value{}, fmt.Errorf("index out of bounds: %s", parts[0])
}

v = v.Index(index)
default:
return reflect.Value{}, fmt.Errorf("unsupported type: %s", v.Kind())
Expand All @@ -84,7 +92,7 @@ func getValue(v reflect.Value, p string) (reflect.Value, error) {

func setValue(v reflect.Value, value any) error {
// If the value is a pointer, dereference it.
for v.Kind() == reflect.Ptr {
for v.Kind() == reflect.Pointer {
v = v.Elem()
}

Expand Down
40 changes: 40 additions & 0 deletions pkg/dotpath/reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,46 @@ func Test_getValue(t *testing.T) {
p: "",
wantErr: true,
},
{
name: "slice index out of bounds (negative)",
v: reflect.ValueOf(TestStruct{
Items: []int{10, 20, 30},
}),
p: "Items.-1",
wantErr: true,
},
{
name: "slice index out of bounds (too large)",
v: reflect.ValueOf(TestStruct{
Items: []int{10, 20, 30},
}),
p: "Items.3",
wantErr: true,
},
{
name: "array index out of bounds (negative)",
v: reflect.ValueOf(TestStruct{
Array: [3]string{"a", "b", "c"},
}),
p: "Array.-1",
wantErr: true,
},
{
name: "array index out of bounds (too large)",
v: reflect.ValueOf(TestStruct{
Array: [3]string{"a", "b", "c"},
}),
p: "Array.3",
wantErr: true,
},
{
name: "empty slice index out of bounds",
v: reflect.ValueOf(TestStruct{
Items: []int{},
}),
p: "Items.0",
wantErr: true,
},
}

for _, tt := range tests {
Expand Down
13 changes: 9 additions & 4 deletions populate.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func populateByFlags(fset *flag.FlagSet, obj any) error {
// Replace the dash in the key with a dot.
key := strings.ReplaceAll(f.Name, "-", ".")

// Set the value at the given path.
// Set the value at the given path (ignore errors).
_ = dotpath.Set(obj, key, f.Value.String())
})

Expand All @@ -41,15 +41,20 @@ func populateByFlags(fset *flag.FlagSet, obj any) error {
// Populate the object by environment variables with the given prefix.
// Overrides existing values only if set in the environment variables.
// Names are converted to dot-separated paths (e.g. "MY_FLAG" -> "my.flag").
func populateByEnv(env []string, pre string, obj any) error {
func populateByEnv(envs []string, pre string, obj any) error {
// If the prefix is empty, do nothing.
if pre == "" {
return nil
}

// Check if the object is a pointer.
if reflect.TypeOf(obj).Kind() != reflect.Ptr {
if reflect.TypeOf(obj).Kind() != reflect.Pointer {
return ErrObjectNotAPointer
}

prefix := strings.ToLower(pre) + "_"

for _, env := range env {
for _, env := range envs {
// Split the environment variable into key and value.
parts := strings.SplitN(env, "=", 2)
if len(parts) != 2 {
Expand Down
Loading