Skip to content

Commit

Permalink
Fix exception caused by non-ascii paths
Browse files Browse the repository at this point in the history
  • Loading branch information
tachyonicClock committed Sep 30, 2024
1 parent 09f325f commit e06b0d5
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 19 deletions.
2 changes: 2 additions & 0 deletions doc/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Latest Changes:

- Support of np.float16 conversion with arrays.

- Fixed a problem that caused ``dir(jpype.JPackage("mypackage"))`` to fail if
the class path contained non-ascii characters. See issue #1194.

- **1.5.0 - 2023-04-03**

Expand Down
17 changes: 11 additions & 6 deletions jpype/_classpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,36 +26,41 @@
_SEP = _os.path.pathsep


def addClassPath(path1: typing.Union[str, _os.PathLike]) -> None:
""" Add a path to the Java class path
def addClassPath(
path1: typing.Union[str, _os.PathLike], relative_to_workdir: bool = False
) -> None:
"""Add a path to the Java class path
Classpath items can be a java, a directory, or a
glob pattern. Relative paths are relative to the
glob pattern. Relative paths are relative to the
caller location.
Arguments:
path(str):
relative_to_workdir(bool): Resolve the path relative to the current
working directory rather than the callers filepath.
"""
# We are deferring these imports until here as we only need them
# if this function is used.
from pathlib import Path
import inspect

global _CLASSPATHS

# Convert to an absolute path. Note that
# relative paths will be resolve based on the location
# of the caller rather than the JPype directory.
path1 = Path(path1)
if not path1.is_absolute():
if not (path1.is_absolute() or relative_to_workdir):
path2 = Path(inspect.stack(1)[1].filename).parent.resolve()
path1 = path2.joinpath(path1)

# If the JVM is already started then we will have to load the paths
# immediately into the DynamicClassLoader
if _jpype.isStarted():
Paths = _jpype.JClass('java.nio.file.Paths')
JContext = _jpype.JClass('org.jpype.JPypeContext')
Paths = _jpype.JClass("java.nio.file.Paths")
JContext = _jpype.JClass("org.jpype.JPypeContext")
classLoader = JContext.getInstance().getClassLoader()
if path1.name == "*":
paths = list(path1.parent.glob("*.jar"))
Expand Down
36 changes: 25 additions & 11 deletions jpype/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@

from ._jvmfinder import *

from jpype._classpath import addClassPath

# This import is required to bootstrap importlib, _jpype uses importlib.util
# but on some systems it may not load properly from C. To make sure it gets
# loaded properly we are going to force the issue even through we don't
Expand Down Expand Up @@ -115,12 +117,15 @@ def _hasClassPath(args) -> bool:
return False


def _handleClassPath(classpath) -> str:
def _handleClassPath(
classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None,
) -> typing.Sequence[str]:
"""
Return a classpath which represents the given tuple of classpath specifications
"""
out = []

if classpath is None:
return out
if isinstance(classpath, (str, os.PathLike)):
classpath = (classpath,)
try:
Expand All @@ -145,7 +150,7 @@ def _handleClassPath(classpath) -> str:
out.extend(glob.glob(pth + '.jar'))
else:
out.append(pth)
return _classpath._SEP.join(out)
return out


_JVM_started = False
Expand Down Expand Up @@ -221,8 +226,6 @@ def startJVM(
# Allow the path to be a PathLike.
jvmpath = os.fspath(jvmpath)

extra_jvm_args: typing.Tuple[str, ...] = tuple()

# Classpath handling
if _hasClassPath(jvmargs):
# Old style, specified in the arguments
Expand All @@ -233,18 +236,14 @@ def startJVM(
# Not specified at all, use the default classpath.
classpath = _classpath.getClassPath()

# Handle strings and list of strings.
if classpath:
extra_jvm_args += (f'-Djava.class.path={_handleClassPath(classpath)}', )

try:
import locale
# Gather a list of locale settings that Java may override (excluding LC_ALL)
categories = [getattr(locale, i) for i in dir(locale) if i.startswith('LC_') and i != 'LC_ALL']
# Keep the current locale settings, else Java will replace them.
prior = [locale.getlocale(i) for i in categories]
# Start the JVM
_jpype.startup(jvmpath, jvmargs + extra_jvm_args,
_jpype.startup(jvmpath, jvmargs,
ignoreUnrecognized, convertStrings, interrupt)
# Collect required resources for operation
initializeResources()
Expand All @@ -264,6 +263,21 @@ def startJVM(
raise RuntimeError(f"{jvmpath} is older than required Java version{version}") from ex
raise

"""Prior versions of JPype used the jvmargs to setup the class paths via
JNI (Java Native Interface) option strings:
i.e -Djava.class.path=...
See: https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/invocation.html
Unfortunately, unicode is unsupported by this interface on windows, since
windows uses wide-byte (16bit) character encoding.
See: https://stackoverflow.com/questions/20052455/jni-start-jvm-with-unicode-support
To resolve this issue we add the classpath after initialization since this
does support unicode via java strings.
"""
for cp in _handleClassPath(classpath):
addClassPath(cp, relative_to_workdir=True)


def initializeResources():
global _JVM_started
Expand Down Expand Up @@ -347,7 +361,7 @@ def initializeResources():
_jpype.JPypeContext = _jpype.JClass('org.jpype.JPypeContext').getInstance()
_jpype.JPypeClassLoader = _jpype.JPypeContext.getClassLoader()

# Everything successed so started is now true.
# Everything succeeded so started is now true.
_JVM_started = True


Expand Down
16 changes: 15 additions & 1 deletion native/java/org/jpype/pkg/JPypePackageManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,21 @@ private static URI toURI(Path path)
URI uri = path.toUri();
if (uri.getScheme().equals("jar") && uri.toString().contains("%2520"))
uri = URI.create("jar:" + uri.getRawSchemeSpecificPart().replaceAll("%25", "%"));
return uri;

// `toASCIIString` ensures the URI is URL encoded with only ascii
// characters. This avoids issues in `sun.nio.fs.UnixUriUtils.fromUri` that
// naively uses `uri.getRawPath()` despite the possibility that it contains
// non-ascii characters that will cause errors. By using `toASCIIString` and
// re-wrapping it in a URI object we ensure that the URI is properly
// encoded. See: https://github.com/jpype-project/jpype/issues/1194
try {
return new URI(uri.toASCIIString());
} catch (Exception e) {
// This exception *should* never occur as we are re-encoding a valid URI.
// Throwing a runtime exception avoids java exception handling boilerplate
// for a situation that *should* never occur.
throw new RuntimeException("Failed to encode URI: " + uri, e);
}
}
//</editor-fold>
}
Binary file added test/jar/non_ascii_à😎/mrjar.jar
Binary file not shown.
8 changes: 7 additions & 1 deletion test/jpypetest/test_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
# *****************************************************************************
import jpype
import subrun
import functools
import os
from pathlib import Path
import unittest
Expand Down Expand Up @@ -146,3 +145,10 @@ def testPathTwice(self):
def testBadKeyword(self):
with self.assertRaises(TypeError):
jpype.startJVM(invalid=True) # type: ignore

def testNonASCIIPath(self):
"""Test that paths with non-ASCII characters are handled correctly.
Regression test for https://github.com/jpype-project/jpype/issues/1194
"""
jpype.startJVM(jvmpath=Path(self.jvmpath), classpath="test/jar/non_ascii_à😎/mrjar.jar")
assert dir(jpype.JPackage('org.jpype.mrjar')) == ['A', 'B', 'sub']

0 comments on commit e06b0d5

Please sign in to comment.