Skip to content

Commit

Permalink
Merge pull request #1039 from traPtitech/fix/zip_file
Browse files Browse the repository at this point in the history
.appファイルのエントリーポイントのバリデーション
  • Loading branch information
ikura-hamu authored Nov 14, 2024
2 parents cdadfdf + 422e922 commit 16e7853
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 22 deletions.
74 changes: 65 additions & 9 deletions src/service/v2/game_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"io"
"net/url"
"os"
"path"
"slices"
"strings"
"time"

"github.com/traPtitech/trap-collection-server/src/domain"
Expand Down Expand Up @@ -72,10 +74,19 @@ func (*GameFile) checkZip(_ context.Context, reader io.Reader) (zr *zip.Reader,
return zr, true, nil
}

func (*GameFile) checkEntryPointExist(_ context.Context, zr *zip.Reader, entryPoint values.GameFileEntryPoint) (bool, error) {
entryPointExists := slices.ContainsFunc(zr.File, func(zf *zip.File) bool {
return zf.Name == string(entryPoint) && !zf.FileInfo().IsDir()
func zipFileContains(zr *zip.Reader, filePath string, isDir bool) bool {
return slices.ContainsFunc(zr.File, func(zf *zip.File) bool {
if isDir {
return path.Clean(zf.Name) == filePath && zf.FileInfo().IsDir()
}
return zf.Name == filePath && !zf.FileInfo().IsDir()
})
}

// エントリーポイントが存在し、それがディレクトリでないことを確認。
// 一般的なエントリーポイントの存在確認に使う。
func (*GameFile) checkEntryPointExist(_ context.Context, zr *zip.Reader, entryPoint values.GameFileEntryPoint) (bool, error) {
entryPointExists := zipFileContains(zr, string(entryPoint), false)

if !entryPointExists {
return false, nil
Expand All @@ -84,6 +95,44 @@ func (*GameFile) checkEntryPointExist(_ context.Context, zr *zip.Reader, entryPo
return true, nil
}

// macOSのアプリケーション(*.app)のエントリーポイントが正しいか確認。
// 仕様は [Appleの開発者向けページ] を参照。
//
// 具体的には、
// - エントリーポイントがディレクトリで .app で終わること
// - エントリーポイント/Contents/MacOS というディレクトリが存在すること
// - エントリーポイント/Contents/Info.plist というファイルが存在すること
//
// [Appleの開発者向けページ]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1
func (*GameFile) checkMacOSAppEntryPointValid(_ context.Context, zr *zip.Reader, entryPoint values.GameFileEntryPoint) (bool, error) {
if !strings.HasSuffix(string(entryPoint), ".app") || !zipFileContains(zr, string(entryPoint), true) {
return false, nil
}

requiredDirs := []string{
path.Join(string(entryPoint), "Contents"),
path.Join(string(entryPoint), "Contents", "MacOS"),
}

for _, dir := range requiredDirs {
if !zipFileContains(zr, dir, true) {
return false, nil
}
}

requiredFiles := []string{
path.Join(string(entryPoint), "Contents", "Info.plist"),
}

for _, file := range requiredFiles {
if !zipFileContains(zr, file, false) {
return false, nil
}
}

return true, nil
}

func (gameFile *GameFile) SaveGameFile(ctx context.Context, reader io.Reader, gameID values.GameID, fileType values.GameFileType, entryPoint values.GameFileEntryPoint) (*domain.GameFile, error) {

var file *domain.GameFile
Expand Down Expand Up @@ -149,15 +198,22 @@ func (gameFile *GameFile) SaveGameFile(ctx context.Context, reader io.Reader, ga
return service.ErrNotZipFile
}

ok, err = gameFile.checkEntryPointExist(ctx, zr, entryPoint)
if err != nil {
return fmt.Errorf("failed to check entry point exist: %w", err)
// これらのどれか一つで成功した場合(trueが返ってきた場合)、有効なエントリーポイントとして扱う
checkers := []func(context.Context, *zip.Reader, values.GameFileEntryPoint) (bool, error){
gameFile.checkEntryPointExist,
gameFile.checkMacOSAppEntryPointValid,
}
if !ok {
return service.ErrInvalidEntryPoint
for _, checker := range checkers {
ok, err = checker(ctx, zr, entryPoint)
if err != nil {
return fmt.Errorf("failed to check entry point: %w", err)
}
if ok {
return nil
}
}

return nil
return service.ErrInvalidEntryPoint
})

eg.Go(func() error {
Expand Down
111 changes: 98 additions & 13 deletions src/service/v2/game_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"io"
"net/url"
"path"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -81,26 +82,36 @@ func Test_checkEntryPointExist(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
entryPoint values.GameFileEntryPoint
result bool
isErr bool
err error
entryPoint values.GameFileEntryPoint
zipFileName string
result bool
isErr bool
err error
}{
"特に問題ないのでエラーなし": {
entryPoint: values.NewGameFileEntryPoint("a/b/file"),
result: true,
entryPoint: values.NewGameFileEntryPoint("a/b/file"),
zipFileName: "a.zip",
result: true,
},
"存在しないパスなのでfalse": {
entryPoint: values.NewGameFileEntryPoint("a/b/not_exist"),
result: false,
entryPoint: values.NewGameFileEntryPoint("a/b/not_exist"),
zipFileName: "a.zip",
result: false,
},
".で始まる相対パスはfalse": {
entryPoint: values.NewGameFileEntryPoint("./a/b/file"),
result: false,
entryPoint: values.NewGameFileEntryPoint("./a/b/file"),
zipFileName: "a.zip",
result: false,
},
"ディレクトリを指定しているのでfalse": {
entryPoint: values.NewGameFileEntryPoint("a/b/"),
result: false,
entryPoint: values.NewGameFileEntryPoint("a/b/"),
zipFileName: "a.zip",
result: false,
},
"ファイルのzipでもエラー無し": {
entryPoint: values.NewGameFileEntryPoint("b.txt"),
zipFileName: "b.zip",
result: true,
},
}

Expand All @@ -109,7 +120,7 @@ func Test_checkEntryPointExist(t *testing.T) {
t.Run(name, func(t *testing.T) {
t.Parallel()

b, err := testdata.FS.ReadFile("a.zip")
b, err := testdata.FS.ReadFile(testCase.zipFileName)
require.NoError(t, err)

r, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
Expand All @@ -131,6 +142,80 @@ func Test_checkEntryPointExist(t *testing.T) {
}
}

func Test_checkMacOSAppEntryPointValid(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
zipFileName string
entryPoint values.GameFileEntryPoint
result bool
isErr bool
err error
}{
"特に問題ないのでエラー無し": {
zipFileName: "test.app.zip",
entryPoint: values.NewGameFileEntryPoint("test.app"),
result: true,
},
"entryPointが*.appでないのでfalse": {
zipFileName: "test.app2.zip",
entryPoint: values.NewGameFileEntryPoint("test.app2"),
result: false,
},
"Contentsフォルダが無いのでfalse": {
zipFileName: "NoContents.zip",
entryPoint: values.NewGameFileEntryPoint("test.app"),
result: false,
},
"MacOSフォルダが無いのでfalse": {
zipFileName: "NoMacOS.zip",
entryPoint: values.NewGameFileEntryPoint("test.app"),
result: false,
},
"Info.plistが無いのでfalse": {
zipFileName: "NoInfoPlist.zip",
entryPoint: values.NewGameFileEntryPoint("test.app"),
result: false,
},
"エントリーポイントに該当するファイルが無いのでfalse": {
zipFileName: "test.app.zip",
entryPoint: values.NewGameFileEntryPoint("invalid.app"),
result: false,
},
".appがフォルダでないのでfalse": {
zipFileName: "file.app.zip",
entryPoint: values.NewGameFileEntryPoint("file.app"),
result: false,
},
}
gameFile := &GameFile{}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

b, err := testdata.FS.ReadFile(path.Join("macOS_app", testCase.zipFileName))
require.NoError(t, err)

r, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
require.NoError(t, err)

ok, err := gameFile.checkMacOSAppEntryPointValid(context.Background(), r, testCase.entryPoint)
if testCase.isErr {
if testCase.err != nil {
assert.ErrorIs(t, err, testCase.err)
} else {
assert.Error(t, err)
}
} else {
assert.NoError(t, err)
}

assert.Equal(t, testCase.result, ok)
})
}
}

func TestSaveGameFile(t *testing.T) {
t.Parallel()

Expand Down
1 change: 1 addition & 0 deletions testdata/b.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
うわー
Binary file added testdata/b.zip
Binary file not shown.
Binary file added testdata/macOS_app/NoContents.zip
Binary file not shown.
Binary file added testdata/macOS_app/NoInfoPlist.zip
Binary file not shown.
Binary file added testdata/macOS_app/NoMacOS.zip
Binary file not shown.
Binary file added testdata/macOS_app/file.app.zip
Binary file not shown.
Binary file added testdata/macOS_app/test.app.zip
Binary file not shown.
Binary file added testdata/macOS_app/test.app2.zip
Binary file not shown.

0 comments on commit 16e7853

Please sign in to comment.