diff --git a/pkg/gotenberg/sort.go b/pkg/gotenberg/sort.go new file mode 100644 index 00000000..e8e2ea70 --- /dev/null +++ b/pkg/gotenberg/sort.go @@ -0,0 +1,69 @@ +package gotenberg + +import ( + "regexp" + "sort" + "strconv" +) + +// AlphanumericSort implements sort.Interface and helps to sort strings +// alphanumerically. +// +// See https://github.com/gotenberg/gotenberg/issues/805. +type AlphanumericSort []string + +func (s AlphanumericSort) Len() int { + return len(s) +} + +func (s AlphanumericSort) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s AlphanumericSort) Less(i, j int) bool { + numI, restI := extractPrefix(s[i]) + numJ, restJ := extractPrefix(s[j]) + + // Compares numerical prefixes if they exist. + if numI != -1 && numJ != -1 { + if numI != numJ { + return numI < numJ + } + // If numbers are equal, falls back to string comparison of the rest. + return restI < restJ + } + + // If one has a numerical prefix and the other doesn't, the one with the + // number comes first. + if numI != -1 { + return true + } + if numJ != -1 { + return false + } + + // If neither has a numerical prefix, compare as strings + return s[i] < s[j] +} + +// extractPrefix attempts to extract a numerical prefix and the rest of the filename +func extractPrefix(filename string) (int, string) { + matches := numPrefixRegexp.FindStringSubmatch(filename) + if len(matches) > 2 { + prefix, err := strconv.Atoi(matches[1]) + if err == nil { + return prefix, matches[2] + } + } + + // Returns -1 if no numerical prefix is found, indicating to just compare + // as strings. + return -1, filename +} + +var numPrefixRegexp = regexp.MustCompile(`^(\d+)(.*)$`) + +// Interface guard. +var ( + _ sort.Interface = (*AlphanumericSort)(nil) +) diff --git a/pkg/gotenberg/sort_test.go b/pkg/gotenberg/sort_test.go new file mode 100644 index 00000000..94b29166 --- /dev/null +++ b/pkg/gotenberg/sort_test.go @@ -0,0 +1,34 @@ +package gotenberg + +import ( + "reflect" + "sort" + "testing" +) + +func TestAlphanumericSort(t *testing.T) { + for _, tc := range []struct { + scenario string + values []string + expectedSort []string + }{ + { + scenario: "numeric and letters", + values: []string{"10qux.pdf", "2_baz.txt", "2_aza.txt", "1bar.pdf", "Afoo.txt", "Bbar.docx", "25zeta.txt", "3.pdf", "4_foo.pdf"}, + expectedSort: []string{"1bar.pdf", "2_aza.txt", "2_baz.txt", "3.pdf", "4_foo.pdf", "10qux.pdf", "25zeta.txt", "Afoo.txt", "Bbar.docx"}, + }, + { + scenario: "hrtime (PHP library)", + values: []string{"245654773395259", "245654773395039", "245654773395149", "245654773394919", "245654773394369"}, + expectedSort: []string{"245654773394369", "245654773394919", "245654773395039", "245654773395149", "245654773395259"}, + }, + } { + t.Run(tc.scenario, func(t *testing.T) { + sort.Sort(AlphanumericSort(tc.values)) + + if !reflect.DeepEqual(tc.values, tc.expectedSort) { + t.Fatalf("expected %+v but got: %+v", tc.expectedSort, tc.values) + } + }) + } +} diff --git a/pkg/modules/api/formdata.go b/pkg/modules/api/formdata.go index 2ceb5e15..d07c14c9 100644 --- a/pkg/modules/api/formdata.go +++ b/pkg/modules/api/formdata.go @@ -11,6 +11,8 @@ import ( "time" "go.uber.org/multierr" + + "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" ) // FormData is a helper for validating and hydrating values from a @@ -304,7 +306,7 @@ func (form *FormData) paths(extensions []string, target *[]string) *FormData { } // See https://github.com/gotenberg/gotenberg/issues/139. - sort.Strings(*target) + sort.Sort(gotenberg.AlphanumericSort(*target)) return form }