diff --git a/cmd/ebitenmobile/main.go b/cmd/ebitenmobile/main.go index 2f981f7f5..8345db63f 100644 --- a/cmd/ebitenmobile/main.go +++ b/cmd/ebitenmobile/main.go @@ -29,6 +29,7 @@ import ( "path/filepath" "strings" "text/template" + "unicode" exec "golang.org/x/sys/execabs" "golang.org/x/tools/go/packages" @@ -41,15 +42,6 @@ const ( //go:embed _files/EbitenViewController.h var objcH string -func init() { - flag.Usage = func() { - // This message is copied from `gomobile bind -h` - fmt.Fprintf(os.Stderr, "%s bind [-target android|ios] [-bootclasspath ] [-classpath ] [-o output] [build flags] [package]\n", ebitenmobileCommand) - os.Exit(2) - } - flag.Parse() -} - func goEnv(name string) string { if val := os.Getenv(name); val != "" { return val @@ -90,6 +82,13 @@ var ( ) func main() { + flag.Usage = func() { + // This message is copied from `gomobile bind -h` + fmt.Fprintf(os.Stderr, "%s bind [-target android|ios] [-bootclasspath ] [-classpath ] [-o output] [build flags] [package]\n", ebitenmobileCommand) + os.Exit(2) + } + flag.Parse() + args := flag.Args() if len(args) < 1 { flag.Usage() @@ -134,6 +133,10 @@ func main() { buildLdflags += " " } buildLdflags += "-extldflags=-Wl,-soname,libgojni.so" + + if !isValidJavaPackageName(bindJavaPkg) { + log.Fatalf("invalid Java package name: %s", bindJavaPkg) + } } dir, err := prepareGomobileCommands() @@ -296,3 +299,50 @@ var iosModuleMapTmpl = template.Must(template.New("iosmmap").Parse(`framework mo {{end}} export * }`)) + +func isValidJavaPackageName(name string) bool { + if name == "" { + return false + } + // A Java package name consists of one or more Java identifiers separated by dots. + for _, token := range strings.Split(name, ".") { + if !isValidJavaIdentifier(token) { + return false + } + } + return true +} + +// isValidJavaIdentifier reports whether the given strings is a valid Java identifier. +func isValidJavaIdentifier(name string) bool { + if name == "" { + return false + } + + // Java identifiers must not be a Java keyword. + switch name { + case "_", "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", "volatile", "while": + return false + } + if name == "null" || name == "true" || name == "false" { + return false + } + + // References: + // * https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Character.html#isJavaIdentifierPart(int) + // * https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Character.html#isJavaIdentifierStart(int) + + isJavaLetter := func(r rune) bool { + return unicode.IsLetter(r) || unicode.Is(unicode.Pc, r) || unicode.Is(unicode.Sc, r) + } + isJavaDigit := unicode.IsDigit + + // A Java identifier is a Java letter or Java letter followed by Java letters or Java digits. + // https://docs.oracle.com/javase/specs/jls/se13/html/jls-3.html#jls-Identifier + for i, r := range name { + if !isJavaLetter(r) && (i == 0 || !isJavaDigit(r)) { + return false + } + } + return true +} diff --git a/cmd/ebitenmobile/main_test.go b/cmd/ebitenmobile/main_test.go new file mode 100644 index 000000000..8652d8010 --- /dev/null +++ b/cmd/ebitenmobile/main_test.go @@ -0,0 +1,76 @@ +// Copyright 2024 The Ebitengine Authors +// +// 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 main + +import ( + "testing" +) + +func TestJavaPackageName(t *testing.T) { + testCases := []struct { + in string + out bool + }{ + { + in: "", + out: false, + }, + { + in: ".", + out: false, + }, + { + in: "com.hajimehoshi.goinovation", + out: true, + }, + { + in: "com.hajimehoshi.$goinovation", + out: true, + }, + { + in: "com.hajimehoshi..goinovation", + out: false, + }, + { + in: "com.hajimehoshi.go-inovation", + out: false, + }, + { + in: "com.hajimehoshi.strictfp", // strictfp is a Java keyword. + out: false, + }, + { + in: "com.hajimehoshi.null", + out: false, + }, + { + in: "com.hajimehoshi.go1inovation", + out: true, + }, + { + in: "com.hajimehoshi.1goinovation", + out: false, + }, + { + in: "あ.いうえお", + out: true, + }, + } + for _, tc := range testCases { + if got, want := isValidJavaPackageName(tc.in), tc.out; got != want { + t.Errorf("isValidJavaPackageName(%q) = %v; want %v", tc.in, got, want) + } + } +}