From 7c2c732c9fa7d1d8f281016733d49c3c3d52a11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enver=20Bi=C5=A1evac?= Date: Mon, 15 Jul 2024 14:41:58 +0000 Subject: [PATCH] fix complex file extensions like tar.gz (#2143) * fix complex file extensions like tar.gz --- app/api/handler/repo/archive.go | 7 +- app/api/request/archive.go | 33 ++++-- app/api/request/archive_test.go | 179 ++++++++++++++++++++++++++++++++ git/api/archive.go | 22 ++++ 4 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 app/api/request/archive_test.go diff --git a/app/api/handler/repo/archive.go b/app/api/handler/repo/archive.go index 7e24eaf9d..8055377fd 100644 --- a/app/api/handler/repo/archive.go +++ b/app/api/handler/repo/archive.go @@ -34,7 +34,12 @@ func HandleArchive(repoCtrl *repo.Controller) http.HandlerFunc { return } - params, filename := request.ParseArchiveParams(r) + params, filename, err := request.ParseArchiveParams(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + var contentType string switch params.Format { case api.ArchiveFormatTar: diff --git a/app/api/request/archive.go b/app/api/request/archive.go index 2a3aefb06..c69757444 100644 --- a/app/api/request/archive.go +++ b/app/api/request/archive.go @@ -33,16 +33,26 @@ const ( QueryParamArchiveCompression = "compression" ) -func ParseArchiveParams(r *http.Request) (api.ArchiveParams, string) { +func Ext(path string) string { + found := "" + for _, format := range api.ArchiveFormats { + if strings.HasSuffix(path, "."+string(format)) { + if len(found) == 0 || len(found) < len(format) { + found = string(format) + } + } + } + return found +} + +func ParseArchiveParams(r *http.Request) (api.ArchiveParams, string, error) { // separate rev and ref part from url, for example: // api/v1/repos/root/demo/+/archive/refs/heads/main.zip // will produce rev=refs/heads and ref=main.zip - rev, filename := filepath.Split(PathParamOrEmpty(r, PathParamArchiveGitRef)) + path := PathParamOrEmpty(r, PathParamArchiveGitRef) + rev, filename := filepath.Split(path) // use ext as format specifier - ext := filepath.Ext(filename) - // get name of the ref from filename - name := strings.TrimSuffix(filename, ext) - format := strings.Replace(ext, ".", "", 1) + format := Ext(filename) // prefix is used for git archive to prefix all paths. prefix, _ := QueryParam(r, QueryParamArchivePrefix) attributes, _ := QueryParam(r, QueryParamArchiveAttributes) @@ -65,13 +75,20 @@ func ParseArchiveParams(r *http.Request) (api.ArchiveParams, string) { } } + archFormat, err := api.ParseArchiveFormat(format) + if err != nil { + return api.ArchiveParams{}, "", err + } + + // get name from filename + name := strings.TrimSuffix(filename, "."+format) return api.ArchiveParams{ - Format: api.ArchiveFormat(format), + Format: archFormat, Prefix: prefix, Attributes: api.ArchiveAttribute(attributes), Time: mtime, Compression: compression, Treeish: rev + name, Paths: r.URL.Query()[QueryParamArchivePaths], - }, filename + }, filename, nil } diff --git a/app/api/request/archive_test.go b/app/api/request/archive_test.go new file mode 100644 index 000000000..b47bee363 --- /dev/null +++ b/app/api/request/archive_test.go @@ -0,0 +1,179 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package request + +import ( + "context" + "net/http" + "reflect" + "testing" + + "github.com/harness/gitness/git/api" + + "github.com/go-chi/chi" +) + +func TestParseArchiveParams(t *testing.T) { + req := func(param string) *http.Request { + rctx := chi.NewRouteContext() + rctx.URLParams.Add("*", param) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, rctx) + r, err := http.NewRequestWithContext(ctx, http.MethodGet, "", nil) + if err != nil { + t.Fatal(err) + } + return r + } + + type args struct { + r *http.Request + } + tests := []struct { + name string + args args + wantParams api.ArchiveParams + wantFilename string + wantErr bool + }{ + { + name: "git archive flag is empty returns error", + args: args{ + r: req("refs/heads/main"), + }, + wantParams: api.ArchiveParams{}, + wantErr: true, + }, + { + name: "git archive flag is unknown returns error", + args: args{ + r: req("refs/heads/main.7z"), + }, + wantParams: api.ArchiveParams{}, + wantErr: true, + }, + { + name: "git archive flag format 'tar'", + args: args{ + r: req("refs/heads/main.tar"), + }, + wantParams: api.ArchiveParams{ + Format: api.ArchiveFormatTar, + Treeish: "refs/heads/main", + }, + wantFilename: "main.tar", + }, + { + name: "git archive flag format 'zip'", + args: args{ + r: req("refs/heads/main.zip"), + }, + wantParams: api.ArchiveParams{ + Format: api.ArchiveFormatZip, + Treeish: "refs/heads/main", + }, + wantFilename: "main.zip", + }, + { + name: "git archive flag format 'gz'", + args: args{ + r: req("refs/heads/main.tar.gz"), + }, + wantParams: api.ArchiveParams{ + Format: api.ArchiveFormatTarGz, + Treeish: "refs/heads/main", + }, + wantFilename: "main.tar.gz", + }, + { + name: "git archive flag format 'tgz'", + args: args{ + r: req("refs/heads/main.tgz"), + }, + wantParams: api.ArchiveParams{ + Format: api.ArchiveFormatTgz, + Treeish: "refs/heads/main", + }, + wantFilename: "main.tgz", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotFilename, err := ParseArchiveParams(tt.args.r) + if !tt.wantErr && err != nil { + t.Errorf("ParseArchiveParams() expected error but err was nil") + } + if !reflect.DeepEqual(got, tt.wantParams) { + t.Errorf("ParseArchiveParams() expected = %v, got %v", tt.wantParams, got) + } + if gotFilename != tt.wantFilename { + t.Errorf("ParseArchiveParams() expected filename = %v, got %v", tt.wantFilename, gotFilename) + } + }) + } +} + +func TestExt(t *testing.T) { + type args struct { + path string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "test file without ext", + args: args{ + path: "./testdata/test", + }, + want: "", + }, + { + name: "test file ext tar", + args: args{ + path: "./testdata/test.tar", + }, + want: "tar", + }, + { + name: "test file ext zip", + args: args{ + path: "./testdata/test.zip", + }, + want: "zip", + }, + { + name: "test file ext tar.gz", + args: args{ + path: "./testdata/test.tar.gz", + }, + want: "tar.gz", + }, + { + name: "test file ext tgz", + args: args{ + path: "./testdata/test.tgz", + }, + want: "tgz", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Ext(tt.args.path); got != tt.want { + t.Errorf("Ext() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/git/api/archive.go b/git/api/archive.go index 85c4b992c..6b5acb907 100644 --- a/git/api/archive.go +++ b/git/api/archive.go @@ -34,6 +34,28 @@ const ( ArchiveFormatTgz ArchiveFormat = "tgz" ) +var ArchiveFormats = []ArchiveFormat{ + ArchiveFormatTar, + ArchiveFormatZip, + ArchiveFormatTarGz, + ArchiveFormatTgz, +} + +func ParseArchiveFormat(format string) (ArchiveFormat, error) { + switch format { + case "tar": + return ArchiveFormatTar, nil + case "zip": + return ArchiveFormatZip, nil + case "tar.gz": + return ArchiveFormatTarGz, nil + case "tgz": + return ArchiveFormatTgz, nil + default: + return "", errors.InvalidArgument("failed to parse file format '%s' is invalid", format) + } +} + func (f ArchiveFormat) Validate() error { switch f { case ArchiveFormatTar, ArchiveFormatZip, ArchiveFormatTarGz, ArchiveFormatTgz: