diff --git a/src/service/v2/game_file.go b/src/service/v2/game_file.go index c1d1816c..a968dbf1 100644 --- a/src/service/v2/game_file.go +++ b/src/service/v2/game_file.go @@ -8,7 +8,9 @@ import ( "io" "net/url" "os" + "path" "slices" + "strings" "time" "github.com/traPtitech/trap-collection-server/src/domain" @@ -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 @@ -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 @@ -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 { diff --git a/src/service/v2/game_file_test.go b/src/service/v2/game_file_test.go index 8587ea7e..822240a3 100644 --- a/src/service/v2/game_file_test.go +++ b/src/service/v2/game_file_test.go @@ -7,6 +7,7 @@ import ( "errors" "io" "net/url" + "path" "strings" "testing" "time" @@ -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, }, } @@ -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))) @@ -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() diff --git a/testdata/b.txt b/testdata/b.txt new file mode 100644 index 00000000..4a52e8f4 --- /dev/null +++ b/testdata/b.txt @@ -0,0 +1 @@ +うわー diff --git a/testdata/b.zip b/testdata/b.zip new file mode 100644 index 00000000..00298f71 Binary files /dev/null and b/testdata/b.zip differ diff --git a/testdata/macOS_app/NoContents.zip b/testdata/macOS_app/NoContents.zip new file mode 100644 index 00000000..587fe33a Binary files /dev/null and b/testdata/macOS_app/NoContents.zip differ diff --git a/testdata/macOS_app/NoInfoPlist.zip b/testdata/macOS_app/NoInfoPlist.zip new file mode 100644 index 00000000..f0e1bce4 Binary files /dev/null and b/testdata/macOS_app/NoInfoPlist.zip differ diff --git a/testdata/macOS_app/NoMacOS.zip b/testdata/macOS_app/NoMacOS.zip new file mode 100644 index 00000000..c9f17839 Binary files /dev/null and b/testdata/macOS_app/NoMacOS.zip differ diff --git a/testdata/macOS_app/file.app.zip b/testdata/macOS_app/file.app.zip new file mode 100644 index 00000000..e05b6f2b Binary files /dev/null and b/testdata/macOS_app/file.app.zip differ diff --git a/testdata/macOS_app/test.app.zip b/testdata/macOS_app/test.app.zip new file mode 100644 index 00000000..423dd442 Binary files /dev/null and b/testdata/macOS_app/test.app.zip differ diff --git a/testdata/macOS_app/test.app2.zip b/testdata/macOS_app/test.app2.zip new file mode 100644 index 00000000..a39cd054 Binary files /dev/null and b/testdata/macOS_app/test.app2.zip differ