From ecc95e9139aec70a6f483457b1d6cf46a2998b26 Mon Sep 17 00:00:00 2001 From: Tobias <96661824+CrazyWolf13@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:44:21 +0200 Subject: [PATCH] fix: panic on restoring with numeric position fields (#1089) Co-authored-by: kolaente --- pkg/modules/dump/restore.go | 71 ++++++++++++++------ pkg/modules/dump/restore_test.go | 108 +++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 18 deletions(-) create mode 100644 pkg/modules/dump/restore_test.go diff --git a/pkg/modules/dump/restore.go b/pkg/modules/dump/restore.go index cc565c2cc..66fe525a6 100644 --- a/pkg/modules/dump/restore.go +++ b/pkg/modules/dump/restore.go @@ -190,6 +190,55 @@ func Restore(filename string) error { return nil } +func convertFieldValue(fieldName string, value interface{}, isFloat bool) (interface{}, error) { + // Check if this is a float field and the value is already a number + if isFloat { + switch v := value.(type) { + case float64: + // Already a float64, no need to process + return v, nil + case int: + // Convert int to float64 + return float64(v), nil + case string: + // Try to decode from base64 string and convert to float + decoded, err := base64.StdEncoding.DecodeString(v) + if err != nil { + var corruptErr base64.CorruptInputError + if !errors.As(err, &corruptErr) { + return nil, fmt.Errorf("could not decode field '%s' %s: %w", fieldName, value, err) + } + // If it's a CorruptInputError, treat the string as raw data + decoded = []byte(v) + } + val, err := strconv.ParseFloat(string(decoded), 64) + if err != nil { + return nil, fmt.Errorf("could not parse double value for field '%s': %w", fieldName, err) + } + return val, nil + default: + return nil, fmt.Errorf("unexpected type for float field '%s': %T", fieldName, v) + } + } + + // Handle JSON fields (non-float) + switch v := value.(type) { + case string: + decoded, err := base64.StdEncoding.DecodeString(v) + if err != nil { + var corruptErr base64.CorruptInputError + if !errors.As(err, &corruptErr) { + return nil, fmt.Errorf("could not decode field '%s' %s: %w", fieldName, value, err) + } + // If it's a CorruptInputError, treat the string as raw data + decoded = []byte(v) + } + return string(decoded), nil + default: + return nil, fmt.Errorf("expected string for JSON field '%s', got %T", fieldName, v) + } +} + func restoreTableData(tables map[string]*zip.File) error { jsonFields := map[string][]string{ "api_tokens": {"permissions"}, @@ -221,25 +270,11 @@ func restoreTableData(tables map[string]*zip.File) error { continue } - var decoded []byte - decoded, err = base64.StdEncoding.DecodeString(content[i][f].(string)) - if err != nil && !errors.Is(err, base64.CorruptInputError(0)) { - return fmt.Errorf("could not decode field '%s' %s: %w", f, content[i][f], err) - } - - if err != nil && errors.Is(err, base64.CorruptInputError(0)) { - decoded = []byte(content[i][f].(string)) - } - - if isFloat { - val, err := strconv.ParseFloat(string(decoded), 64) - if err != nil { - return fmt.Errorf("could not parse double value for field '%s': %w", f, err) - } - content[i][f] = val - } else { - content[i][f] = string(decoded) + convertedValue, err := convertFieldValue(f, content[i][f], isFloat) + if err != nil { + return err } + content[i][f] = convertedValue } } return nil diff --git a/pkg/modules/dump/restore_test.go b/pkg/modules/dump/restore_test.go new file mode 100644 index 000000000..4d1b6cda6 --- /dev/null +++ b/pkg/modules/dump/restore_test.go @@ -0,0 +1,108 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package dump + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConvertFieldValue(t *testing.T) { + t.Run("Float field conversions", func(t *testing.T) { + t.Run("should return float64 as-is", func(t *testing.T) { + result, err := convertFieldValue("position", 123.45, true) + require.NoError(t, err) + assert.InEpsilon(t, 123.45, result, 0.0001) + }) + + t.Run("should convert int to float64", func(t *testing.T) { + result, err := convertFieldValue("position", 42, true) + require.NoError(t, err) + assert.InEpsilon(t, 42.0, result, 0.0001) + }) + + t.Run("should decode base64 string and convert to float", func(t *testing.T) { + encoded := base64.StdEncoding.EncodeToString([]byte("123.45")) + result, err := convertFieldValue("position", encoded, true) + require.NoError(t, err) + assert.InEpsilon(t, 123.45, result, 0.0001) + }) + + t.Run("should handle non-base64 string and convert to float", func(t *testing.T) { + result, err := convertFieldValue("position", "67.89", true) + require.NoError(t, err) + assert.InEpsilon(t, 67.89, result, 0.0001) + }) + + t.Run("should return error for invalid float string", func(t *testing.T) { + _, err := convertFieldValue("position", "not-a-number", true) + require.Error(t, err) + assert.Contains(t, err.Error(), "could not parse double value") + }) + + t.Run("should return error for unexpected type", func(t *testing.T) { + _, err := convertFieldValue("position", []int{1, 2, 3}, true) + require.Error(t, err) + assert.Contains(t, err.Error(), "unexpected type for float field") + }) + }) + + t.Run("JSON field conversions", func(t *testing.T) { + t.Run("should decode base64 string", func(t *testing.T) { + jsonData := `{"key": "value"}` + encoded := base64.StdEncoding.EncodeToString([]byte(jsonData)) + result, err := convertFieldValue("permissions", encoded, false) + require.NoError(t, err) + assert.JSONEq(t, jsonData, result.(string)) + }) + + t.Run("should handle non-base64 string", func(t *testing.T) { + jsonData := `{"key": "value"}` + result, err := convertFieldValue("permissions", jsonData, false) + require.NoError(t, err) + assert.JSONEq(t, jsonData, result.(string)) + }) + + t.Run("should return error for non-string type", func(t *testing.T) { + _, err := convertFieldValue("permissions", 123, false) + require.Error(t, err) + assert.Contains(t, err.Error(), "expected string for JSON field") + }) + }) + + t.Run("Base64 error handling", func(t *testing.T) { + t.Run("should handle base64 decode error for float field", func(t *testing.T) { + invalidBase64 := "invalid base64 with spaces and special chars!!!" + result, err := convertFieldValue("position", invalidBase64, true) + // Should not error on decode, but should error on parse float + require.Error(t, err) + assert.Contains(t, err.Error(), "could not parse double value") + assert.Nil(t, result) + }) + + t.Run("should handle base64 decode error for JSON field", func(t *testing.T) { + // For JSON fields, CorruptInputError just returns the raw string + invalidBase64 := "invalid base64 with spaces and special chars!!!" + result, err := convertFieldValue("permissions", invalidBase64, false) + require.NoError(t, err) + assert.Equal(t, invalidBase64, result) + }) + }) +}