├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── design_discussion.md │ └── feature_request.md └── workflows │ └── check.yml ├── .gitignore ├── LICENSE ├── PLCT.svg ├── README.md ├── base ├── build.gradle.kts └── src │ └── main │ └── java │ └── org │ └── glavo │ └── japp │ ├── CompressionMethod.java │ ├── JAppProperties.java │ ├── TODO.java │ ├── annotation │ └── Visibility.java │ ├── classfile │ └── ClassFile.java │ ├── io │ ├── ByteBufferChannel.java │ ├── ByteBufferInputStream.java │ ├── ByteBufferOutputStream.java │ ├── IOUtils.java │ ├── LittleEndianDataOutput.java │ └── WritableByteChannelWrapper.java │ └── util │ ├── ByteBufferUtils.java │ ├── CompressedNumber.java │ ├── MUTF8.java │ ├── MemoryAccess.java │ ├── XxHash64.java │ └── ZstdUtils.java ├── bin ├── japp.ps1 └── japp.sh ├── boot ├── build.gradle.kts └── src │ └── main │ ├── java │ └── org │ │ └── glavo │ │ └── japp │ │ └── boot │ │ ├── JAppBootArgs.java │ │ ├── JAppBootLauncher.java │ │ ├── JAppBootMetadata.java │ │ ├── JAppReader.java │ │ ├── JAppResource.java │ │ ├── JAppResourceField.java │ │ ├── JAppResourceGroup.java │ │ ├── JAppResourceRoot.java │ │ ├── decompressor │ │ ├── DecompressContext.java │ │ ├── classfile │ │ │ ├── ByteArrayPool.java │ │ │ └── ClassFileDecompressor.java │ │ └── zstd │ │ │ ├── BitInputStream.java │ │ │ ├── Constants.java │ │ │ ├── FiniteStateEntropy.java │ │ │ ├── FrameHeader.java │ │ │ ├── FseCompressionTable.java │ │ │ ├── FseTableReader.java │ │ │ ├── Huffman.java │ │ │ ├── MalformedInputException.java │ │ │ ├── Util.java │ │ │ ├── ZstdFrameDecompressor.java │ │ │ └── package-info.java │ │ ├── jappfs │ │ ├── JAppDirectoryStream.java │ │ ├── JAppFileAttributeView.java │ │ ├── JAppFileAttributes.java │ │ ├── JAppFileStore.java │ │ ├── JAppFileSystem.java │ │ ├── JAppFileSystemProvider.java │ │ └── JAppPath.java │ │ ├── module │ │ ├── JAppModuleFinder.java │ │ └── JAppModuleReference.java │ │ └── url │ │ ├── JAppURLConnection.java │ │ ├── JAppURLHandler.java │ │ ├── JAppURLStreamHandlerFactory.java │ │ └── JAppURLStreamHandlerProvider.java │ └── module-info │ └── module-info.java ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ └── LWJGL.java ├── docs ├── COMPARE.md └── introduce.md ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── native ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ ├── launcher.rs │ └── main.rs ├── settings.gradle.kts ├── specification.md ├── src ├── main │ ├── java │ │ └── org │ │ │ └── glavo │ │ │ └── japp │ │ │ ├── Main.java │ │ │ ├── condition │ │ │ ├── AndCondition.java │ │ │ ├── Condition.java │ │ │ ├── ConditionParser.java │ │ │ ├── JavaCondition.java │ │ │ ├── MatchList.java │ │ │ ├── NotCondition.java │ │ │ └── OrCondition.java │ │ │ ├── launcher │ │ │ ├── EmbeddedLauncher.java │ │ │ ├── JAppConfigGroup.java │ │ │ ├── JAppLauncherMetadata.java │ │ │ ├── JAppResourceGroupReference.java │ │ │ └── Launcher.java │ │ │ ├── maven │ │ │ ├── MavenRepository.java │ │ │ └── MavenResolver.java │ │ │ ├── packer │ │ │ ├── JAppPacker.java │ │ │ ├── JAppResourceInfo.java │ │ │ ├── JAppResourcesWriter.java │ │ │ ├── JAppWriter.java │ │ │ ├── ModuleInfoReader.java │ │ │ ├── compressor │ │ │ │ ├── CompressContext.java │ │ │ │ ├── CompressResult.java │ │ │ │ ├── Compressor.java │ │ │ │ ├── Compressors.java │ │ │ │ ├── DefaultCompressor.java │ │ │ │ └── classfile │ │ │ │ │ ├── ByteArrayPoolBuilder.java │ │ │ │ │ ├── ClassFileCompressor.java │ │ │ │ │ └── ClassFileReader.java │ │ │ └── processor │ │ │ │ ├── ClassPathProcessor.java │ │ │ │ ├── LocalClassPathProcessor.java │ │ │ │ ├── MavenClassPathProcessor.java │ │ │ │ └── PathListParser.java │ │ │ └── platform │ │ │ ├── Architecture.java │ │ │ ├── JAppRuntimeContext.java │ │ │ ├── JavaRuntime.java │ │ │ ├── LibC.java │ │ │ └── OperatingSystem.java │ └── resources │ │ └── org │ │ └── glavo │ │ └── japp │ │ └── packer │ │ └── header.sh └── test │ └── java │ └── org │ └── glavo │ └── japp │ ├── boot │ ├── JAppResourceTest.java │ └── decompressor │ │ └── zstd │ │ └── ZstdTest.java │ ├── classfile │ └── ByteArrayPoolTest.java │ ├── launcher │ └── EndZipTest.java │ ├── packer │ ├── ModuleInfoReaderTest.java │ ├── compressor │ │ └── ClassFileCompressorTest.java │ └── processor │ │ └── PathListParserTest.java │ ├── testcase │ ├── HelloWorldTest.java │ ├── JAppTestHelper.java │ ├── JAppTestTemplate.java │ └── ModulePathTest.java │ └── util │ ├── CompressedNumberTest.java │ ├── MUTF8Test.java │ └── XxHash64Test.java └── test-case ├── HelloWorld ├── build.gradle.kts └── src │ └── main │ └── java │ ├── module-info.java │ └── org │ └── glavo │ └── japp │ └── testcase │ └── helloworld │ └── HelloWorld.java └── ModulePath ├── build.gradle.kts └── src └── main └── java └── org └── glavo └── japp └── testcase └── modulepath └── ModulePath.java /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://donate.glavo.site/"] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[Bug] ' 5 | labels: ['bug'] 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/design_discussion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Design Discussion 3 | about: Discuss japp file and launcher design 4 | title: '[Design] ' 5 | labels: ['design'] 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[Feature] ' 5 | labels: ['enhancement'] 6 | assignees: '' 7 | 8 | --- 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Gradle Check 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | gradle-check: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-java@v3 15 | with: 16 | distribution: 'temurin' 17 | java-version: '21' 18 | - uses: gradle/gradle-build-action@v2 19 | with: 20 | arguments: check --info --no-daemon --stacktrace 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.japp 2 | /.japp 3 | .gradle 4 | build/ 5 | !gradle/wrapper/gradle-wrapper.jar 6 | !**/src/main/**/build/ 7 | !**/src/test/**/build/ 8 | 9 | ### IntelliJ IDEA ### 10 | .idea 11 | 12 | ### Eclipse ### 13 | .apt_generated 14 | .classpath 15 | .factorypath 16 | .project 17 | .settings 18 | .springBeans 19 | .sts4-cache 20 | !**/src/main/**/bin/ 21 | !**/src/test/**/bin/ 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | 33 | ### Mac OS ### 34 | .DS_Store 35 | 36 | # Other 37 | 38 | -------------------------------------------------------------------------------- /PLCT.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /base/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | 3 | tasks.compileJava { 4 | options.compilerArgs.addAll( 5 | listOf( 6 | "--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED", 7 | "--add-exports=java.base/jdk.internal.module=ALL-UNNAMED", 8 | "--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED", 9 | ) 10 | ) 11 | } 12 | 13 | val jappPropertiesFile = rootProject.layout.buildDirectory.file("japp.properties").get().asFile 14 | 15 | tasks.create("generateJAppProperties") { 16 | inputs 17 | outputs.file(jappPropertiesFile) 18 | doLast { 19 | val properties = Properties() 20 | properties["Project-Directory"] = rootProject.layout.projectDirectory.asFile.absolutePath 21 | properties["Boot-Jar"] = project(":boot").tasks.getByName("bootJar").archiveFile.get().asFile.absolutePath 22 | jappPropertiesFile.writer().use { writer -> 23 | properties.store(writer, null) 24 | } 25 | } 26 | } 27 | 28 | tasks.processResources { 29 | dependsOn("generateJAppProperties") 30 | 31 | into("org/glavo/japp") { 32 | from(jappPropertiesFile) 33 | } 34 | } -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/CompressionMethod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp; 17 | 18 | import java.io.IOException; 19 | import java.nio.ByteBuffer; 20 | 21 | public enum CompressionMethod { 22 | NONE, 23 | CLASSFILE, 24 | ZSTD; 25 | 26 | private static final CompressionMethod[] METHODS = values(); 27 | 28 | public static CompressionMethod of(int i) { 29 | return i >= 0 && i < METHODS.length ? METHODS[i] : null; 30 | } 31 | 32 | public static CompressionMethod readFrom(ByteBuffer buffer) throws IOException { 33 | byte id = buffer.get(); 34 | if (id >= 0 && id < METHODS.length) { 35 | return METHODS[id]; 36 | } 37 | 38 | throw new IOException(String.format("Unknown compression method: 0x%02x", Byte.toUnsignedInt(id))); 39 | } 40 | 41 | public byte id() { 42 | return (byte) ordinal(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/JAppProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp; 17 | 18 | import java.io.InputStreamReader; 19 | import java.io.Reader; 20 | import java.nio.file.Path; 21 | import java.nio.file.Paths; 22 | import java.util.Properties; 23 | 24 | import static java.nio.charset.StandardCharsets.UTF_8; 25 | 26 | public final class JAppProperties { 27 | 28 | private static final Path PROJECT_DIRECTORY; 29 | private static final Path HOME_DIRECTORY; 30 | private static final Path BOOT_JAR; 31 | 32 | static { 33 | Properties properties = new Properties(); 34 | 35 | //noinspection DataFlowIssue 36 | try (Reader reader = new InputStreamReader(JAppProperties.class.getResourceAsStream("japp.properties"), UTF_8)) { 37 | properties.load(reader); 38 | } catch (Exception e) { 39 | throw new AssertionError(e); 40 | } 41 | 42 | // In the early stages we isolate the configuration in the project directory 43 | PROJECT_DIRECTORY = Paths.get(properties.getProperty("Project-Directory")); 44 | HOME_DIRECTORY = PROJECT_DIRECTORY.resolve(".japp"); 45 | BOOT_JAR = Paths.get(properties.getProperty("Boot-Jar")); 46 | } 47 | 48 | public static Path getProjectDirectory() { 49 | return PROJECT_DIRECTORY; 50 | } 51 | 52 | public static Path getHomeDirectory() { 53 | return HOME_DIRECTORY; 54 | } 55 | 56 | public static Path getBootJar() { 57 | return BOOT_JAR; 58 | } 59 | 60 | private JAppProperties() { 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/TODO.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp; 17 | 18 | public final class TODO extends Error { 19 | public TODO() { 20 | super("TODO"); 21 | } 22 | 23 | public TODO(String message) { 24 | super("TODO: " + message); 25 | } 26 | 27 | public TODO(String message, Throwable cause) { 28 | super("TODO: " + message, cause); 29 | } 30 | 31 | public TODO(Throwable cause) { 32 | super("TODO", cause); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/annotation/Visibility.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.annotation; 17 | 18 | import java.lang.annotation.Retention; 19 | import java.lang.annotation.RetentionPolicy; 20 | 21 | @Retention(RetentionPolicy.SOURCE) 22 | public @interface Visibility { 23 | enum Context { 24 | BOOT, 25 | LAUNCHER, 26 | PACKER 27 | } 28 | 29 | Context[] value(); 30 | } 31 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/classfile/ClassFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.classfile; 17 | 18 | public final class ClassFile { 19 | 20 | public static final int MAGIC_NUMBER = 0xcafebabe; 21 | 22 | public static final byte CONSTANT_Utf8 = 1; 23 | public static final byte CONSTANT_Integer = 3; 24 | public static final byte CONSTANT_Float = 4; 25 | public static final byte CONSTANT_Long = 5; 26 | public static final byte CONSTANT_Double = 6; 27 | public static final byte CONSTANT_Class = 7; 28 | public static final byte CONSTANT_String = 8; 29 | public static final byte CONSTANT_Fieldref = 9; 30 | public static final byte CONSTANT_Methodref = 10; 31 | public static final byte CONSTANT_InterfaceMethodref = 11; 32 | public static final byte CONSTANT_NameAndType = 12; 33 | public static final byte CONSTANT_MethodHandle = 15; 34 | public static final byte CONSTANT_MethodType = 16; 35 | public static final byte CONSTANT_Dynamic = 17; 36 | public static final byte CONSTANT_InvokeDynamic = 18; 37 | public static final byte CONSTANT_Module = 19; 38 | public static final byte CONSTANT_Package = 20; 39 | 40 | public static final byte CONSTANT_EXTERNAL_STRING = -1; 41 | public static final byte CONSTANT_EXTERNAL_STRING_Class = -2; 42 | public static final byte CONSTANT_EXTERNAL_STRING_Descriptor = -3; 43 | public static final byte CONSTANT_EXTERNAL_STRING_Signature = -4; 44 | 45 | public static final byte[] CONSTANT_SIZE = new byte[32]; 46 | static { 47 | CONSTANT_SIZE[CONSTANT_Integer] = 4; 48 | CONSTANT_SIZE[CONSTANT_Float] = 4; 49 | CONSTANT_SIZE[CONSTANT_Long] = 8; 50 | CONSTANT_SIZE[CONSTANT_Double] = 8; 51 | CONSTANT_SIZE[CONSTANT_Class] = 2; 52 | CONSTANT_SIZE[CONSTANT_String] = 2; 53 | CONSTANT_SIZE[CONSTANT_Fieldref] = 4; 54 | CONSTANT_SIZE[CONSTANT_Methodref] = 4; 55 | CONSTANT_SIZE[CONSTANT_InterfaceMethodref] = 4; 56 | CONSTANT_SIZE[CONSTANT_NameAndType] = 4; 57 | CONSTANT_SIZE[CONSTANT_MethodHandle] = 3; 58 | CONSTANT_SIZE[CONSTANT_MethodType] = 2; 59 | CONSTANT_SIZE[CONSTANT_Dynamic] = 4; 60 | CONSTANT_SIZE[CONSTANT_InvokeDynamic] = 4; 61 | CONSTANT_SIZE[CONSTANT_Module] = 2; 62 | CONSTANT_SIZE[CONSTANT_Package] = 2; 63 | } 64 | 65 | private ClassFile() { 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/io/ByteBufferChannel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.io; 17 | 18 | import java.io.IOException; 19 | import java.nio.ByteBuffer; 20 | import java.nio.channels.ClosedChannelException; 21 | import java.nio.channels.NonWritableChannelException; 22 | import java.nio.channels.SeekableByteChannel; 23 | 24 | public final class ByteBufferChannel implements SeekableByteChannel { 25 | private ByteBuffer buffer; 26 | 27 | public ByteBufferChannel(ByteBuffer buffer) { 28 | this.buffer = buffer; 29 | } 30 | 31 | private void ensureOpen() throws IOException { 32 | if (buffer == null) { 33 | throw new ClosedChannelException(); 34 | } 35 | } 36 | 37 | @Override 38 | public boolean isOpen() { 39 | return buffer != null; 40 | } 41 | 42 | @Override 43 | public long position() throws IOException { 44 | ensureOpen(); 45 | return buffer.position(); 46 | } 47 | 48 | @Override 49 | public long size() throws IOException { 50 | ensureOpen(); 51 | return buffer.capacity(); 52 | } 53 | 54 | @Override 55 | public int read(ByteBuffer dst) throws IOException { 56 | ensureOpen(); 57 | 58 | int remaining = buffer.remaining(); 59 | 60 | if (remaining == 0) { 61 | return -1; 62 | } 63 | 64 | int n = Math.min(dst.remaining(), remaining); 65 | int end = buffer.position() + n; 66 | dst.put(buffer.duplicate().limit(end)); 67 | buffer.position(end); 68 | return n; 69 | } 70 | 71 | @Override 72 | public void close() throws IOException { 73 | buffer = null; 74 | } 75 | 76 | @Override 77 | public int write(ByteBuffer src) throws IOException { 78 | throw new NonWritableChannelException(); 79 | } 80 | 81 | @Override 82 | public SeekableByteChannel position(long newPosition) throws IOException { 83 | if (newPosition < 0 || newPosition >= Integer.MAX_VALUE) { 84 | throw new IllegalArgumentException("Illegal position " + newPosition); 85 | } 86 | this.buffer.position(Math.min((int) newPosition, buffer.limit())); 87 | return this; 88 | } 89 | 90 | @Override 91 | public SeekableByteChannel truncate(long size) throws IOException { 92 | throw new NonWritableChannelException(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/io/ByteBufferInputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.io; 17 | 18 | import java.io.EOFException; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.nio.ByteBuffer; 22 | import java.nio.InvalidMarkException; 23 | import java.util.Objects; 24 | 25 | public final class ByteBufferInputStream extends InputStream { 26 | 27 | private final ByteBuffer buffer; 28 | 29 | public ByteBufferInputStream(byte[] array) { 30 | this.buffer = ByteBuffer.wrap(array); 31 | } 32 | 33 | public ByteBufferInputStream(ByteBuffer buffer) { 34 | this.buffer = buffer; 35 | } 36 | 37 | @Override 38 | public int available() throws IOException { 39 | return buffer.remaining(); 40 | } 41 | 42 | @Override 43 | public int read() throws IOException { 44 | if (buffer.hasRemaining()) { 45 | return Byte.toUnsignedInt(buffer.get()); 46 | } else { 47 | return -1; 48 | } 49 | } 50 | 51 | @Override 52 | public int read(byte[] b, int off, int len) throws IOException { 53 | Objects.checkFromIndexSize(off, len, b.length); 54 | if (len == 0) { 55 | return 0; 56 | } 57 | 58 | int remaining = buffer.remaining(); 59 | if (remaining == 0) { 60 | return -1; 61 | } 62 | 63 | int n = Math.min(remaining, len); 64 | buffer.get(b, off, n); 65 | return n; 66 | } 67 | 68 | // @Override 69 | public byte[] readAllBytes() throws IOException { 70 | int remaining = buffer.remaining(); 71 | byte[] res = new byte[remaining]; 72 | buffer.get(res); 73 | return res; 74 | } 75 | 76 | @Override 77 | public long skip(long n) throws IOException { 78 | if (n <= 0) { 79 | return 0; 80 | } 81 | 82 | int res = (int) Math.min(n, buffer.remaining()); 83 | buffer.position(buffer.position() + res); 84 | return res; 85 | } 86 | 87 | // @Override 88 | @SuppressWarnings("Since15") 89 | public void skipNBytes(long n) throws IOException { 90 | if (n <= 0) { 91 | return; 92 | } 93 | 94 | int remaining = buffer.remaining(); 95 | if (n > remaining) { 96 | throw new EOFException(); 97 | } 98 | 99 | int res = (int) Math.min(n, buffer.remaining()); 100 | buffer.position(buffer.position() + res); 101 | } 102 | 103 | @Override 104 | public boolean markSupported() { 105 | return true; 106 | } 107 | 108 | @Override 109 | public void mark(int readlimit) { 110 | buffer.mark(); 111 | } 112 | 113 | @Override 114 | public void reset() throws IOException { 115 | try { 116 | buffer.reset(); 117 | } catch (InvalidMarkException e) { 118 | throw new IOException(e); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/io/ByteBufferOutputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.io; 17 | 18 | import java.io.IOException; 19 | import java.io.OutputStream; 20 | import java.nio.ByteBuffer; 21 | import java.nio.ByteOrder; 22 | import java.nio.charset.StandardCharsets; 23 | import java.util.Arrays; 24 | 25 | public final class ByteBufferOutputStream extends LittleEndianDataOutput { 26 | private ByteBuffer buffer; 27 | 28 | public ByteBufferOutputStream() { 29 | this(ByteOrder.LITTLE_ENDIAN, 8192); 30 | } 31 | 32 | public ByteBufferOutputStream(int initialCapacity) { 33 | this(ByteOrder.LITTLE_ENDIAN, initialCapacity); 34 | } 35 | 36 | public ByteBufferOutputStream(ByteOrder order, int initialCapacity) { 37 | this.buffer = ByteBuffer.allocate(initialCapacity).order(order); 38 | } 39 | 40 | public ByteBufferOutputStream(ByteBuffer buffer) { 41 | this.buffer = buffer; 42 | } 43 | 44 | private void prepare(int next) { 45 | if (buffer.remaining() < next) { 46 | byte[] arr = buffer.array(); 47 | int prevLen = buffer.position(); 48 | int nextLen = Math.max(prevLen * 2, prevLen + next); 49 | 50 | buffer = ByteBuffer.allocate(nextLen).order(buffer.order()); 51 | buffer.put(arr, 0, prevLen); 52 | } 53 | } 54 | 55 | public ByteBuffer getByteBuffer() { 56 | return buffer; 57 | } 58 | 59 | public long getTotalBytes() { 60 | return buffer.position(); 61 | } 62 | 63 | @Override 64 | public void write(int b) { 65 | writeByte((byte) b); 66 | } 67 | 68 | @Override 69 | public void write(byte[] b, int off, int len) { 70 | writeBytes(b, off, len); 71 | } 72 | 73 | public void writeByte(byte v) { 74 | prepare(Byte.BYTES); 75 | buffer.put(v); 76 | } 77 | 78 | public void writeShort(short v) { 79 | prepare(Short.BYTES); 80 | buffer.putShort(v); 81 | } 82 | 83 | public void writeInt(int v) { 84 | prepare(Integer.BYTES); 85 | buffer.putInt(v); 86 | } 87 | 88 | public void writeLong(long v) { 89 | prepare(Long.BYTES); 90 | buffer.putLong(v); 91 | } 92 | 93 | public void writeBytes(byte[] array, int offset, int len) { 94 | prepare(len); 95 | buffer.put(array, offset, len); 96 | } 97 | 98 | public void writeTo(OutputStream out) throws IOException { 99 | out.write(buffer.array(), 0, buffer.position()); 100 | } 101 | 102 | public byte[] toByteArray() { 103 | return Arrays.copyOf(buffer.array(), buffer.position()); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/io/IOUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.io; 17 | 18 | import java.io.EOFException; 19 | import java.io.IOException; 20 | import java.nio.ByteBuffer; 21 | import java.nio.channels.ReadableByteChannel; 22 | import java.nio.channels.WritableByteChannel; 23 | 24 | public final class IOUtils { 25 | 26 | public static void readFully(ReadableByteChannel channel, ByteBuffer buffer) throws IOException { 27 | 28 | //noinspection StatementWithEmptyBody 29 | while (channel.read(buffer) > 0) { 30 | } 31 | 32 | if (buffer.hasRemaining()) { 33 | throw new EOFException("Unexpected end of data"); 34 | } 35 | } 36 | 37 | public static void writeFully(WritableByteChannel channel, ByteBuffer buffer) throws IOException { 38 | while (buffer.hasRemaining()) { 39 | channel.write(buffer); 40 | } 41 | } 42 | 43 | private IOUtils() { 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/io/LittleEndianDataOutput.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.io; 17 | 18 | import java.io.Closeable; 19 | import java.io.IOException; 20 | import java.io.OutputStream; 21 | import java.nio.channels.Channels; 22 | import java.nio.channels.WritableByteChannel; 23 | import java.nio.charset.StandardCharsets; 24 | 25 | public abstract class LittleEndianDataOutput extends OutputStream implements Closeable { 26 | 27 | public static LittleEndianDataOutput of(OutputStream outputStream) { 28 | return new WritableByteChannelWrapper(Channels.newChannel(outputStream)); 29 | } 30 | 31 | public static LittleEndianDataOutput of(WritableByteChannel channel) { 32 | return new WritableByteChannelWrapper(channel); 33 | } 34 | 35 | public abstract long getTotalBytes(); 36 | 37 | public abstract void writeByte(byte v) throws IOException; 38 | 39 | public void writeUnsignedByte(int v) throws IOException { 40 | if (v < 0 || v > 0xff) { 41 | throw new IllegalArgumentException(); 42 | } 43 | 44 | writeByte((byte) v); 45 | } 46 | 47 | public abstract void writeShort(short v) throws IOException; 48 | 49 | public void writeUnsignedShort(int v) throws IOException { 50 | if (v < 0 || v > 0xffff) { 51 | throw new IllegalArgumentException(); 52 | } 53 | 54 | writeShort((short) v); 55 | } 56 | 57 | public abstract void writeInt(int v) throws IOException; 58 | 59 | public void writeUnsignedInt(long v) throws IOException { 60 | if (v < 0 || v > 0xffff_ffffL) { 61 | throw new IllegalArgumentException(); 62 | } 63 | 64 | writeInt((int) v); 65 | } 66 | 67 | public abstract void writeLong(long v) throws IOException; 68 | 69 | public void writeBytes(byte[] array) throws IOException { 70 | writeBytes(array, 0, array.length); 71 | } 72 | 73 | public abstract void writeBytes(byte[] array, int offset, int len) throws IOException; 74 | 75 | public void writeString(String str) throws IOException { 76 | if (str != null) { 77 | byte[] bytes = str.getBytes(StandardCharsets.UTF_8); 78 | writeInt(bytes.length); 79 | writeBytes(bytes); 80 | } else { 81 | writeInt(0); 82 | } 83 | } 84 | 85 | @Override 86 | public void write(int b) throws IOException { 87 | LittleEndianDataOutput.this.writeByte((byte) b); 88 | } 89 | 90 | @Override 91 | public void write(byte[] b, int off, int len) throws IOException { 92 | LittleEndianDataOutput.this.writeBytes(b, off, len); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/io/WritableByteChannelWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.io; 17 | 18 | import java.io.IOException; 19 | import java.io.OutputStream; 20 | import java.nio.ByteBuffer; 21 | import java.nio.ByteOrder; 22 | import java.nio.channels.Channels; 23 | import java.nio.channels.WritableByteChannel; 24 | import java.util.Objects; 25 | 26 | final class WritableByteChannelWrapper extends LittleEndianDataOutput { 27 | private final WritableByteChannel channel; 28 | private final ByteBuffer buffer = ByteBuffer.allocate(8192).order(ByteOrder.LITTLE_ENDIAN); 29 | private long totalBytes = 0L; 30 | 31 | WritableByteChannelWrapper(OutputStream outputStream) { 32 | this.channel = Channels.newChannel(outputStream); 33 | } 34 | 35 | WritableByteChannelWrapper(WritableByteChannel channel) { 36 | this.channel = channel; 37 | } 38 | 39 | private void flushBuffer() throws IOException { 40 | if (buffer.position() > 0) { 41 | buffer.flip(); 42 | IOUtils.writeFully(channel, buffer); 43 | buffer.clear(); 44 | } 45 | } 46 | 47 | private void prepare(int next) throws IOException { 48 | if (buffer.remaining() < next) { 49 | flushBuffer(); 50 | } 51 | } 52 | 53 | @Override 54 | public long getTotalBytes() { 55 | return totalBytes; 56 | } 57 | 58 | @Override 59 | public void writeByte(byte v) throws IOException { 60 | prepare(Byte.BYTES); 61 | buffer.put(v); 62 | totalBytes += Byte.BYTES; 63 | } 64 | 65 | @Override 66 | public void writeShort(short v) throws IOException { 67 | prepare(Short.BYTES); 68 | buffer.putShort(v); 69 | totalBytes += Short.BYTES; 70 | } 71 | 72 | @Override 73 | public void writeInt(int v) throws IOException { 74 | prepare(Integer.BYTES); 75 | buffer.putInt(v); 76 | totalBytes += Integer.BYTES; 77 | } 78 | 79 | @Override 80 | public void writeLong(long v) throws IOException { 81 | prepare(Long.BYTES); 82 | buffer.putLong(v); 83 | totalBytes += Long.BYTES; 84 | } 85 | 86 | @Override 87 | public void writeBytes(byte[] array, int offset, int len) throws IOException { 88 | Objects.checkFromIndexSize(offset, len, array.length); 89 | if (len == 0) { 90 | return; 91 | } 92 | 93 | if (len < buffer.capacity()) { 94 | if (len > buffer.remaining()) { 95 | flushBuffer(); 96 | } 97 | buffer.put(array, offset, len); 98 | } else { 99 | flushBuffer(); 100 | IOUtils.writeFully(channel, ByteBuffer.wrap(array, offset, len)); 101 | } 102 | totalBytes += len; 103 | } 104 | 105 | @Override 106 | public void close() throws IOException { 107 | try { 108 | flushBuffer(); 109 | } finally { 110 | channel.close(); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/util/ByteBufferUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.util; 17 | 18 | import java.nio.ByteBuffer; 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.List; 21 | 22 | public final class ByteBufferUtils { 23 | 24 | @SuppressWarnings("deprecation") 25 | private static final int release = Runtime.version().major(); 26 | 27 | public static ByteBuffer slice(ByteBuffer buffer, int index, int length) { 28 | if (release >= 13) { 29 | //noinspection Since15 30 | return buffer.slice(index, length); 31 | } else { 32 | return buffer.duplicate().limit(index + length).position(index).slice(); 33 | } 34 | } 35 | 36 | public static String readString(ByteBuffer buffer) { 37 | int length = buffer.getInt(); 38 | byte[] array = new byte[length]; 39 | buffer.get(array); 40 | return new String(array, StandardCharsets.UTF_8); 41 | } 42 | 43 | public static String readStringOrNull(ByteBuffer buffer) { 44 | int length = buffer.getInt(); 45 | if (length == 0) { 46 | return null; 47 | } 48 | 49 | byte[] array = new byte[length]; 50 | buffer.get(array); 51 | return new String(array, StandardCharsets.UTF_8); 52 | } 53 | 54 | public static void readStringList(ByteBuffer buffer, List out) { 55 | int count = buffer.getInt(); 56 | for (int i = 0; i < count; i++) { 57 | out.add(readString(buffer)); 58 | } 59 | } 60 | 61 | private ByteBufferUtils() { 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/util/CompressedNumber.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.util; 17 | 18 | import java.nio.ByteBuffer; 19 | 20 | public final class CompressedNumber { 21 | 22 | private static final int SIGN_MASK = 0b1000_0000; 23 | private static final int VALUE_MASK = 0b0111_1111; 24 | private static final int TAIL_VALUE_MASK = 0b0111; 25 | 26 | public static void putInt(ByteBuffer out, int value) { 27 | if (value < 0) { 28 | throw new AssertionError(); 29 | } 30 | do { 31 | int b = value & VALUE_MASK; 32 | value >>>= 7; 33 | 34 | if (value != 0) { 35 | b |= SIGN_MASK; 36 | } 37 | 38 | out.put((byte) b); 39 | } while (value != 0); 40 | } 41 | 42 | public static int getInt(ByteBuffer in) { 43 | int res = 0; 44 | for (int i = 0; i < 4; i++) { 45 | int b = Byte.toUnsignedInt(in.get()); 46 | int bv = b & VALUE_MASK; 47 | 48 | res = res | (bv << (7 * i)); 49 | if (b == bv) { 50 | return res; 51 | } 52 | } 53 | 54 | int b = Byte.toUnsignedInt(in.get()); 55 | int bv = b & TAIL_VALUE_MASK; 56 | if (b != bv) { 57 | throw new AssertionError(); 58 | } 59 | 60 | return res | (bv << (7 * 4)); 61 | } 62 | 63 | private CompressedNumber() { 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/util/MUTF8.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.util; 17 | 18 | import java.nio.charset.StandardCharsets; 19 | 20 | public final class MUTF8 { 21 | public static String stringFromMUTF8(byte[] bytes, int offset, int count) { 22 | final int end = offset + count; 23 | 24 | int i; 25 | for (i = offset; i < end; i++) { 26 | if ((bytes[i] & 0x80) != 0) { 27 | break; 28 | } 29 | } 30 | 31 | if (i == end) { 32 | return new String(bytes, offset, count, StandardCharsets.US_ASCII); 33 | } 34 | 35 | StringBuilder builder = new StringBuilder(count); 36 | for (i = offset; i < end; i++) { 37 | byte ch = bytes[i]; 38 | 39 | if (ch == 0) { 40 | break; 41 | } 42 | 43 | if (ch > 0) { 44 | builder.append((char) ch); 45 | } else { 46 | int uch = ch & 0x7F; 47 | int mask = 0x40; 48 | 49 | while ((uch & mask) != 0) { 50 | ch = bytes[++i]; 51 | 52 | if ((ch & 0xC0) != 0x80) { 53 | throw new IllegalArgumentException("bad continuation 0x" + Integer.toHexString(ch)); 54 | } 55 | 56 | uch = ((uch & ~mask) << 6) | (ch & 0x3F); 57 | mask <<= 6 - 1; 58 | } 59 | 60 | if ((uch & 0xFFFF) != uch) { 61 | throw new IllegalArgumentException("character out of range \\u" + Integer.toHexString(uch)); 62 | } 63 | builder.appendCodePoint(uch); 64 | } 65 | } 66 | return builder.toString(); 67 | } 68 | 69 | public static String stringFromMUTF8(byte[] bytes) { 70 | return stringFromMUTF8(bytes, 0, bytes.length); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/util/MemoryAccess.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.util; 17 | 18 | import jdk.internal.misc.Unsafe; 19 | 20 | import java.nio.Buffer; 21 | import java.nio.ByteBuffer; 22 | 23 | public final class MemoryAccess { 24 | private static final Unsafe UNSAFE = Unsafe.getUnsafe(); 25 | private static final long BUFFER_ADDRESS_OFFSET = UNSAFE.objectFieldOffset(Buffer.class, "address"); 26 | 27 | public static final int ARRAY_BYTE_BASE_OFFSET = Unsafe.ARRAY_BYTE_BASE_OFFSET; 28 | 29 | private MemoryAccess() { 30 | } 31 | 32 | public static long getDirectBufferAddress(ByteBuffer buffer) { 33 | assert buffer.isDirect(); 34 | return UNSAFE.getLong(buffer, BUFFER_ADDRESS_OFFSET); 35 | } 36 | 37 | public static byte getByte(Object o, long offset) { 38 | return UNSAFE.getByte(o, offset); 39 | } 40 | 41 | public static int getUnsignedByte(Object o, long offset) { 42 | return Byte.toUnsignedInt(getByte(o, offset)); 43 | } 44 | 45 | public static void putByte(Object o, long offset, byte x) { 46 | UNSAFE.putByte(o, offset, x); 47 | } 48 | 49 | public static short getShort(Object o, long offset) { 50 | return UNSAFE.getShortUnaligned(o, offset, false); 51 | } 52 | 53 | public static void putShort(Object o, long offset, short x) { 54 | UNSAFE.putShortUnaligned(o, offset, x, false); 55 | } 56 | 57 | public static int getInt(Object o, long offset) { 58 | return UNSAFE.getIntUnaligned(o, offset, false); 59 | } 60 | 61 | public static long getUnsignedInt(Object o, long offset) { 62 | return Integer.toUnsignedLong(getInt(o, offset)); 63 | } 64 | 65 | public static void putInt(Object o, long offset, int x) { 66 | UNSAFE.putIntUnaligned(o, offset, x, false); 67 | } 68 | 69 | public static long getLong(Object o, long offset) { 70 | return UNSAFE.getLongUnaligned(o, offset, false); 71 | } 72 | 73 | public static void putLong(Object o, long offset, long x) { 74 | UNSAFE.putLongUnaligned(o, offset, x, false); 75 | } 76 | 77 | public static void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes) { 78 | UNSAFE.copyMemory(srcBase, srcOffset, destBase, destOffset, bytes); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/util/XxHash64.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.util; 17 | 18 | import java.lang.ref.Reference; 19 | import java.nio.ByteBuffer; 20 | 21 | import static org.glavo.japp.util.MemoryAccess.ARRAY_BYTE_BASE_OFFSET; 22 | 23 | public final class XxHash64 { 24 | private static final long P1 = 0x9E3779B185EBCA87L; 25 | private static final long P2 = 0xC2B2AE3D27D4EB4FL; 26 | private static final long P3 = 0x165667B19E3779F9L; 27 | private static final long P4 = 0x85EBCA77C2b2AE63L; 28 | private static final long P5 = 0x27D4EB2F165667C5L; 29 | 30 | public static long hashByteBufferWithoutUpdate(ByteBuffer buffer) { 31 | return hashByteBufferWithoutUpdate(0L, buffer); 32 | } 33 | 34 | public static long hashByteBufferWithoutUpdate(long seed, ByteBuffer buffer) { 35 | Object inputBase; 36 | long inputAddress; 37 | long inputLimit; 38 | 39 | if (buffer.hasArray()) { 40 | inputBase = buffer.array(); 41 | inputAddress = ARRAY_BYTE_BASE_OFFSET + buffer.arrayOffset() + buffer.position(); 42 | } else { 43 | inputBase = null; 44 | inputAddress = MemoryAccess.getDirectBufferAddress(buffer); 45 | } 46 | inputLimit = inputAddress + buffer.remaining(); 47 | 48 | try { 49 | return hash(seed, inputBase, inputAddress, inputLimit); 50 | } finally { 51 | Reference.reachabilityFence(buffer); 52 | } 53 | } 54 | 55 | public static long hash(byte[] array) { 56 | return hash(0, array); 57 | } 58 | 59 | public static long hash(long seed, byte[] array) { 60 | return hash(seed, array, 0, array.length); 61 | } 62 | 63 | public static long hash(long seed, byte[] array, int offset, int length) { 64 | return hash(seed, (Object) array, ARRAY_BYTE_BASE_OFFSET + offset, ARRAY_BYTE_BASE_OFFSET + offset + length); 65 | } 66 | 67 | public static long hash(long seed, Object inputBase, long inputAddress, long inputLimit) { 68 | long hash; 69 | long address = inputAddress; 70 | 71 | if (inputLimit - address >= 32) { 72 | long v1 = seed + P1 + P2; 73 | long v2 = seed + P2; 74 | long v3 = seed; 75 | long v4 = seed - P1; 76 | 77 | do { 78 | v1 = mix(v1, MemoryAccess.getLong(inputBase, address)); 79 | v2 = mix(v2, MemoryAccess.getLong(inputBase, address + 8)); 80 | v3 = mix(v3, MemoryAccess.getLong(inputBase, address + 16)); 81 | v4 = mix(v4, MemoryAccess.getLong(inputBase, address + 24)); 82 | 83 | address += 32; 84 | } while (inputLimit - address >= 32); 85 | 86 | hash = Long.rotateLeft(v1, 1) 87 | + Long.rotateLeft(v2, 7) 88 | + Long.rotateLeft(v3, 12) 89 | + Long.rotateLeft(v4, 18); 90 | 91 | hash = update(hash, v1); 92 | hash = update(hash, v2); 93 | hash = update(hash, v3); 94 | hash = update(hash, v4); 95 | } else { 96 | hash = seed + P5; 97 | } 98 | 99 | hash += inputLimit - inputAddress; 100 | 101 | while (address <= inputLimit - 8) { 102 | long k1 = MemoryAccess.getLong(inputBase, address); 103 | k1 *= P2; 104 | k1 = Long.rotateLeft(k1, 31); 105 | k1 *= P1; 106 | hash ^= k1; 107 | hash = Long.rotateLeft(hash, 27) * P1 + P4; 108 | address += 8; 109 | } 110 | 111 | if (address <= inputLimit - 4) { 112 | hash ^= MemoryAccess.getUnsignedInt(inputBase, address) * P1; 113 | hash = Long.rotateLeft(hash, 23) * P2 + P3; 114 | address += 4; 115 | } 116 | 117 | while (address < inputLimit) { 118 | hash ^= MemoryAccess.getUnsignedByte(inputBase, address) * P5; 119 | hash = Long.rotateLeft(hash, 11) * P1; 120 | address++; 121 | } 122 | 123 | return finalize(hash); 124 | } 125 | 126 | private static long mix(long current, long value) { 127 | return Long.rotateLeft(current + value * P2, 31) * P1; 128 | } 129 | 130 | private static long update(long hash, long value) { 131 | return (hash ^ mix(0, value)) * P1 + P4; 132 | } 133 | 134 | private static long finalize(long hash) { 135 | hash ^= hash >>> 33; 136 | hash *= P2; 137 | hash ^= hash >>> 29; 138 | hash *= P3; 139 | hash ^= hash >>> 32; 140 | return hash; 141 | } 142 | 143 | } 144 | 145 | -------------------------------------------------------------------------------- /base/src/main/java/org/glavo/japp/util/ZstdUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.util; 17 | 18 | public final class ZstdUtils { 19 | public static int maxCompressedLength(int sourceLength) { 20 | int maxCompressedSize = sourceLength + (sourceLength >>> 8); 21 | 22 | if (sourceLength < 128 * 1024) { 23 | maxCompressedSize += (128 * 1024 - sourceLength) >>> 11; 24 | } 25 | return maxCompressedSize; 26 | } 27 | 28 | private ZstdUtils() { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bin/japp.ps1: -------------------------------------------------------------------------------- 1 | $ScriptPath = $MyInvocation.MyCommand.Path 2 | $ProjectDir = Resolve-Path $ScriptPath\..\.. 3 | $JAppJar = "$ProjectDir\build\japp.jar" 4 | 5 | if(-Not(Test-Path -Path $JAppJar)) 6 | { 7 | throw "Please build the project using '.\gradlew' first" 8 | } 9 | 10 | java -jar $JAppJar @args 11 | exit $LASTEXITCODE 12 | -------------------------------------------------------------------------------- /bin/japp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | project_dir=$(realpath $(dirname $(realpath "$0"))/..) 4 | japp_jar="$project_dir/build/japp.jar" 5 | 6 | if [ ! -f "$japp_jar" ]; then 7 | echo "Please build the project using './gradlew' first" >&2 8 | exit 1 9 | fi 10 | 11 | exec java -jar $japp_jar "$@" 12 | -------------------------------------------------------------------------------- /boot/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.glavo.mic.tasks.CompileModuleInfo 2 | 3 | plugins { 4 | id("org.glavo.compile-module-info-plugin") version "2.0" apply false 5 | } 6 | 7 | dependencies { 8 | implementation(project(":base")) 9 | } 10 | 11 | tasks.compileJava { 12 | options.compilerArgs.addAll( 13 | listOf( 14 | "--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED", 15 | "--add-exports=java.base/jdk.internal.module=ALL-UNNAMED", 16 | "--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED", 17 | ) 18 | ) 19 | } 20 | 21 | val mainClassName = "org.glavo.japp.boot.JAppBootLauncher" 22 | 23 | val compileModuleInfo = tasks.create("compileModuleInfo") { 24 | sourceFile.set(layout.projectDirectory.file("src/main/module-info/module-info.java")) 25 | targetFile.set(layout.buildDirectory.file("classes/module-info/main/module-info.class")) 26 | moduleMainClass = mainClassName 27 | } 28 | 29 | tasks.classes.get().dependsOn(compileModuleInfo) 30 | 31 | tasks.create("bootJar") { 32 | destinationDirectory.set(rootProject.layout.buildDirectory) 33 | archiveFileName.set("japp-boot.jar") 34 | 35 | dependsOn(configurations.runtimeClasspath) 36 | 37 | from(sourceSets.main.get().output) 38 | from(configurations.runtimeClasspath.get().map { zipTree(it) }) 39 | } 40 | 41 | tasks.withType { 42 | from(compileModuleInfo.targetFile) 43 | manifest.attributes("Main-Class" to mainClassName) 44 | } 45 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/JAppBootArgs.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot; 17 | 18 | import java.io.IOException; 19 | import java.nio.ByteBuffer; 20 | import java.nio.file.Path; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | public final class JAppBootArgs { 25 | public enum Field { 26 | END, 27 | MAIN_CLASS, 28 | MAIN_MODULE, 29 | MODULE_PATH, 30 | CLASS_PATH, 31 | ADD_READS, 32 | ADD_EXPORTS, 33 | ADD_OPENS, 34 | ENABLE_NATIVE_ACCESS; 35 | 36 | private static final Field[] VALUES = values(); 37 | 38 | public static Field readFrom(ByteBuffer buffer) throws IOException { 39 | byte id = buffer.get(); 40 | if (id >= 0 && id < VALUES.length) { 41 | return VALUES[id]; 42 | } 43 | throw new IOException(String.format("Unknown field: 0x%02x", Byte.toUnsignedInt(id))); 44 | } 45 | 46 | public byte id() { 47 | return (byte) ordinal(); 48 | } 49 | } 50 | 51 | public static final byte ID_RESOLVED_REFERENCE_END = 0; 52 | public static final byte ID_RESOLVED_REFERENCE_LOCAL = 1; 53 | public static final byte ID_RESOLVED_REFERENCE_EXTERNAL = 2; 54 | 55 | String mainClass; 56 | String mainModule; 57 | 58 | final List addReads = new ArrayList<>(); 59 | final List addExports = new ArrayList<>(); 60 | final List addOpens = new ArrayList<>(); 61 | 62 | final List enableNativeAccess = new ArrayList<>(); 63 | 64 | final List externalModules = new ArrayList<>(); 65 | } 66 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/JAppResourceField.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot; 17 | 18 | public enum JAppResourceField { 19 | END, 20 | 21 | /** 22 | * XXH64 Checksum (8byte) 23 | */ 24 | CHECKSUM, 25 | 26 | FILE_CREATE_TIME, 27 | FILE_LAST_MODIFIED_TIME, 28 | FILE_LAST_ACCESS_TIME 29 | ; 30 | 31 | private static final JAppResourceField[] FIELDS = values(); 32 | 33 | public static JAppResourceField of(int i) { 34 | return i >= 0 && i < FIELDS.length ? FIELDS[i] : null; 35 | } 36 | 37 | public byte id() { 38 | return (byte) ordinal(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/JAppResourceGroup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot; 17 | 18 | import java.util.LinkedHashMap; 19 | 20 | public final class JAppResourceGroup extends LinkedHashMap { 21 | 22 | public static final byte MAGIC_NUMBER = (byte) 0xeb; 23 | public static final int HEADER_LENGTH = 24; // 1 + 1 + 2 + 4 + 4 + 4 + 8 24 | 25 | private String name; 26 | 27 | public JAppResourceGroup() { 28 | } 29 | 30 | public void initName(String name) { 31 | if (this.name != null) { 32 | throw new IllegalStateException(); 33 | } 34 | 35 | this.name = name; 36 | } 37 | 38 | public String getName() { 39 | return name; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return getClass().getSimpleName() + super.toString(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/JAppResourceRoot.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot; 17 | 18 | import java.net.URI; 19 | import java.net.URISyntaxException; 20 | import java.util.Locale; 21 | 22 | public enum JAppResourceRoot { 23 | MODULES, CLASSPATH, RESOURCE; 24 | 25 | private final String rootName; 26 | private final String pathPrefix; 27 | 28 | JAppResourceRoot() { 29 | this.rootName = name().toLowerCase(Locale.ROOT); 30 | this.pathPrefix = '/' + rootName; 31 | } 32 | 33 | public String getRootName() { 34 | return rootName; 35 | } 36 | 37 | public String getPathPrefix() { 38 | return pathPrefix; 39 | } 40 | 41 | public URI toURI(JAppResourceGroup group) { 42 | try { 43 | return new URI("japp", null, pathPrefix + '/' + group.getName() + '/', null); 44 | } catch (URISyntaxException e) { 45 | throw new RuntimeException(e); 46 | } 47 | } 48 | 49 | public URI toURI(JAppResourceGroup group, JAppResource resource) { 50 | try { 51 | return new URI("japp", null, pathPrefix + '/' + group.getName() + '/' + resource.getName(), null); 52 | } catch (URISyntaxException e) { 53 | throw new RuntimeException(e); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/decompressor/DecompressContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.decompressor; 17 | 18 | import org.glavo.japp.boot.decompressor.classfile.ByteArrayPool; 19 | 20 | import java.nio.ByteBuffer; 21 | 22 | public interface DecompressContext { 23 | ByteArrayPool getPool(); 24 | 25 | void decompressZstd(ByteBuffer input, ByteBuffer output); 26 | } 27 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/decompressor/classfile/ByteArrayPool.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.decompressor.classfile; 17 | 18 | import org.glavo.japp.CompressionMethod; 19 | import org.glavo.japp.boot.decompressor.zstd.ZstdFrameDecompressor; 20 | 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | 24 | public final class ByteArrayPool { 25 | public static final byte MAGIC_NUMBER = (byte) 0xf0; 26 | 27 | private final byte[] bytes; 28 | private final long[] offsetAndSize; 29 | 30 | private ByteArrayPool(byte[] bytes, long[] offsetAndSize) { 31 | this.bytes = bytes; 32 | this.offsetAndSize = offsetAndSize; 33 | } 34 | 35 | public static ByteArrayPool readFrom(ByteBuffer buffer, ZstdFrameDecompressor decompressor) throws IOException { 36 | byte magic = buffer.get(); 37 | if (magic != MAGIC_NUMBER) { 38 | throw new IOException(String.format("Wrong boot magic: 0x%02x", Byte.toUnsignedInt(magic))); 39 | } 40 | 41 | CompressionMethod compressionMethod = CompressionMethod.readFrom(buffer); 42 | 43 | short reserved = buffer.getShort(); 44 | if (reserved != 0) { 45 | throw new IOException("Reserved is not zero"); 46 | } 47 | 48 | int count = buffer.getInt(); 49 | int uncompressedBytesSize = buffer.getInt(); 50 | int compressedBytesSize = buffer.getInt(); 51 | 52 | long[] offsetAndSize = new long[count]; 53 | 54 | int offset = 0; 55 | for (int i = 0; i < count; i++) { 56 | int s = Short.toUnsignedInt(buffer.getShort()); 57 | offsetAndSize[i] = (((long) s) << 32) | (long) offset; 58 | offset += s; 59 | } 60 | 61 | byte[] compressedBytes = new byte[compressedBytesSize]; 62 | buffer.get(compressedBytes); 63 | 64 | byte[] uncompressedBytes; 65 | if (compressionMethod == CompressionMethod.NONE) { 66 | uncompressedBytes = compressedBytes; 67 | } else { 68 | uncompressedBytes = new byte[uncompressedBytesSize]; 69 | if (compressionMethod == CompressionMethod.ZSTD) { 70 | decompressor.decompress(compressedBytes, 0, compressedBytesSize, uncompressedBytes, 0, uncompressedBytesSize); 71 | } else { 72 | throw new IOException("Unsupported compression method: " + compressionMethod); 73 | } 74 | } 75 | 76 | return new ByteArrayPool(uncompressedBytes, offsetAndSize); 77 | } 78 | 79 | public ByteBuffer get(int index) { 80 | long l = offsetAndSize[index]; 81 | int offset = (int) (l & 0xffff_ffffL); 82 | int size = (int) (l >>> 32); 83 | 84 | return ByteBuffer.wrap(bytes, offset, size); 85 | } 86 | 87 | public int get(int index, ByteBuffer output) { 88 | long l = offsetAndSize[index]; 89 | int offset = (int) (l & 0xffff_ffffL); 90 | int size = (int) (l >>> 32); 91 | 92 | output.put(bytes, offset, size); 93 | return size; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.glavo.japp.boot.decompressor.zstd; 15 | 16 | final class Constants { 17 | public static final int SIZE_OF_BYTE = 1; 18 | public static final int SIZE_OF_SHORT = 2; 19 | public static final int SIZE_OF_INT = 4; 20 | public static final int SIZE_OF_LONG = 8; 21 | 22 | public static final int MAGIC_NUMBER = 0xFD2FB528; 23 | 24 | public static final int MIN_WINDOW_LOG = 10; 25 | public static final int MAX_WINDOW_LOG = 31; 26 | 27 | public static final int SIZE_OF_BLOCK_HEADER = 3; 28 | 29 | public static final int MIN_SEQUENCES_SIZE = 1; 30 | public static final int MIN_BLOCK_SIZE = 1 // block type tag 31 | + 1 // min size of raw or rle length header 32 | + MIN_SEQUENCES_SIZE; 33 | public static final int MAX_BLOCK_SIZE = 128 * 1024; 34 | 35 | public static final int REPEATED_OFFSET_COUNT = 3; 36 | 37 | // block types 38 | public static final int RAW_BLOCK = 0; 39 | public static final int RLE_BLOCK = 1; 40 | public static final int COMPRESSED_BLOCK = 2; 41 | 42 | // sequence encoding types 43 | public static final int SEQUENCE_ENCODING_BASIC = 0; 44 | public static final int SEQUENCE_ENCODING_RLE = 1; 45 | public static final int SEQUENCE_ENCODING_COMPRESSED = 2; 46 | public static final int SEQUENCE_ENCODING_REPEAT = 3; 47 | 48 | public static final int MAX_LITERALS_LENGTH_SYMBOL = 35; 49 | public static final int MAX_MATCH_LENGTH_SYMBOL = 52; 50 | public static final int MAX_OFFSET_CODE_SYMBOL = 31; 51 | public static final int DEFAULT_MAX_OFFSET_CODE_SYMBOL = 28; 52 | 53 | public static final int LITERAL_LENGTH_TABLE_LOG = 9; 54 | public static final int MATCH_LENGTH_TABLE_LOG = 9; 55 | public static final int OFFSET_TABLE_LOG = 8; 56 | 57 | // literal block types 58 | public static final int RAW_LITERALS_BLOCK = 0; 59 | public static final int RLE_LITERALS_BLOCK = 1; 60 | public static final int COMPRESSED_LITERALS_BLOCK = 2; 61 | public static final int TREELESS_LITERALS_BLOCK = 3; 62 | 63 | public static final int LONG_NUMBER_OF_SEQUENCES = 0x7F00; 64 | 65 | public static final int[] LITERALS_LENGTH_BITS = {0, 0, 0, 0, 0, 0, 0, 0, 66 | 0, 0, 0, 0, 0, 0, 0, 0, 67 | 1, 1, 1, 1, 2, 2, 3, 3, 68 | 4, 6, 7, 8, 9, 10, 11, 12, 69 | 13, 14, 15, 16}; 70 | 71 | public static final int[] MATCH_LENGTH_BITS = {0, 0, 0, 0, 0, 0, 0, 0, 72 | 0, 0, 0, 0, 0, 0, 0, 0, 73 | 0, 0, 0, 0, 0, 0, 0, 0, 74 | 0, 0, 0, 0, 0, 0, 0, 0, 75 | 1, 1, 1, 1, 2, 2, 3, 3, 76 | 4, 4, 5, 7, 8, 9, 10, 11, 77 | 12, 13, 14, 15, 16}; 78 | 79 | private Constants() { 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/FrameHeader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.glavo.japp.boot.decompressor.zstd; 15 | 16 | import java.util.Objects; 17 | import java.util.StringJoiner; 18 | 19 | import static org.glavo.japp.boot.decompressor.zstd.Util.checkState; 20 | 21 | final class FrameHeader { 22 | final long headerSize; 23 | final int windowSize; 24 | final long contentSize; 25 | final long dictionaryId; 26 | final boolean hasChecksum; 27 | 28 | public FrameHeader(long headerSize, int windowSize, long contentSize, long dictionaryId, boolean hasChecksum) { 29 | checkState(windowSize >= 0 || contentSize >= 0, "Invalid frame header: contentSize or windowSize must be set"); 30 | this.headerSize = headerSize; 31 | this.windowSize = windowSize; 32 | this.contentSize = contentSize; 33 | this.dictionaryId = dictionaryId; 34 | this.hasChecksum = hasChecksum; 35 | } 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) { 40 | return true; 41 | } 42 | if (o == null || getClass() != o.getClass()) { 43 | return false; 44 | } 45 | FrameHeader that = (FrameHeader) o; 46 | return headerSize == that.headerSize && 47 | windowSize == that.windowSize && 48 | contentSize == that.contentSize && 49 | dictionaryId == that.dictionaryId && 50 | hasChecksum == that.hasChecksum; 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return Objects.hash(headerSize, windowSize, contentSize, dictionaryId, hasChecksum); 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return new StringJoiner(", ", FrameHeader.class.getSimpleName() + "[", "]") 61 | .add("headerSize=" + headerSize) 62 | .add("windowSize=" + windowSize) 63 | .add("contentSize=" + contentSize) 64 | .add("dictionaryId=" + dictionaryId) 65 | .add("hasChecksum=" + hasChecksum) 66 | .toString(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/FseCompressionTable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.glavo.japp.boot.decompressor.zstd; 15 | 16 | final class FseCompressionTable { 17 | private FseCompressionTable() { 18 | } 19 | 20 | private static int calculateStep(int tableSize) { 21 | return (tableSize >>> 1) + (tableSize >>> 3) + 3; 22 | } 23 | 24 | public static int spreadSymbols(short[] normalizedCounters, int maxSymbolValue, int tableSize, int highThreshold, byte[] symbols) { 25 | int mask = tableSize - 1; 26 | int step = calculateStep(tableSize); 27 | 28 | int position = 0; 29 | for (byte symbol = 0; symbol <= maxSymbolValue; symbol++) { 30 | for (int i = 0; i < normalizedCounters[symbol]; i++) { 31 | symbols[position] = symbol; 32 | do { 33 | position = (position + step) & mask; 34 | } 35 | while (position > highThreshold); 36 | } 37 | } 38 | return position; 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/MalformedInputException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.glavo.japp.boot.decompressor.zstd; 15 | 16 | public class MalformedInputException extends RuntimeException { 17 | private final long offset; 18 | 19 | public MalformedInputException(long offset) { 20 | this(offset, "Malformed input"); 21 | } 22 | 23 | public MalformedInputException(long offset, String reason) { 24 | super(reason + ": offset=" + offset); 25 | this.offset = offset; 26 | } 27 | 28 | public long getOffset() { 29 | return offset; 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/Util.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package org.glavo.japp.boot.decompressor.zstd; 15 | 16 | import org.glavo.japp.util.MemoryAccess; 17 | 18 | import static org.glavo.japp.boot.decompressor.zstd.Constants.SIZE_OF_SHORT; 19 | 20 | final class Util { 21 | private Util() { 22 | } 23 | 24 | public static int highestBit(int value) { 25 | return 31 - Integer.numberOfLeadingZeros(value); 26 | } 27 | 28 | public static boolean isPowerOf2(int value) { 29 | return (value & (value - 1)) == 0; 30 | } 31 | 32 | public static int mask(int bits) { 33 | return (1 << bits) - 1; 34 | } 35 | 36 | public static void verify(boolean condition, long offset, String reason) { 37 | if (!condition) { 38 | throw new MalformedInputException(offset, reason); 39 | } 40 | } 41 | 42 | public static void checkArgument(boolean condition, String reason) { 43 | if (!condition) { 44 | throw new IllegalArgumentException(reason); 45 | } 46 | } 47 | 48 | static void checkPositionIndexes(int start, int end, int size) { 49 | // Carefully optimized for execution by hotspot (explanatory comment above) 50 | if (start < 0 || end < start || end > size) { 51 | throw new IndexOutOfBoundsException(badPositionIndexes(start, end, size)); 52 | } 53 | } 54 | 55 | private static String badPositionIndexes(int start, int end, int size) { 56 | if (start < 0 || start > size) { 57 | return badPositionIndex(start, size, "start index"); 58 | } 59 | if (end < 0 || end > size) { 60 | return badPositionIndex(end, size, "end index"); 61 | } 62 | // end < start 63 | return String.format("end index (%s) must not be less than start index (%s)", end, start); 64 | } 65 | 66 | private static String badPositionIndex(int index, int size, String desc) { 67 | if (index < 0) { 68 | return String.format("%s (%s) must not be negative", desc, index); 69 | } else if (size < 0) { 70 | throw new IllegalArgumentException("negative size: " + size); 71 | } else { // index > size 72 | return String.format("%s (%s) must not be greater than size (%s)", desc, index, size); 73 | } 74 | } 75 | 76 | public static void checkState(boolean condition, String reason) { 77 | if (!condition) { 78 | throw new IllegalStateException(reason); 79 | } 80 | } 81 | 82 | public static MalformedInputException fail(long offset, String reason) { 83 | throw new MalformedInputException(offset, reason); 84 | } 85 | 86 | public static int get24BitLittleEndian(Object inputBase, long inputAddress) { 87 | return (MemoryAccess.getShort(inputBase, inputAddress) & 0xFFFF) 88 | | ((MemoryAccess.getByte(inputBase, inputAddress + SIZE_OF_SHORT) & 0xFF) << Short.SIZE); 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Based on aircompressor (https://github.com/airlift/aircompressor) 3 | */ 4 | package org.glavo.japp.boot.decompressor.zstd; -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/jappfs/JAppDirectoryStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.jappfs; 17 | 18 | import java.io.IOException; 19 | import java.nio.file.ClosedDirectoryStreamException; 20 | import java.nio.file.DirectoryStream; 21 | import java.nio.file.Path; 22 | import java.util.Iterator; 23 | import java.util.NoSuchElementException; 24 | 25 | public final class JAppDirectoryStream implements DirectoryStream { 26 | 27 | private final JAppPath path; 28 | private final JAppFileSystem.DirectoryNode node; 29 | private final DirectoryStream.Filter filter; 30 | private Itr itr; 31 | 32 | private boolean isClosed = false; 33 | 34 | public JAppDirectoryStream(JAppPath path, JAppFileSystem.DirectoryNode node, Filter filter) { 35 | this.path = path; 36 | this.filter = filter; 37 | this.node = node; 38 | } 39 | 40 | @Override 41 | public Iterator iterator() { 42 | if (isClosed) { 43 | throw new ClosedDirectoryStreamException(); 44 | } 45 | 46 | if (itr != null) { 47 | throw new IllegalStateException("Iterator has already been returned"); 48 | } 49 | 50 | return itr = new Itr(); 51 | } 52 | 53 | @Override 54 | public void close() throws IOException { 55 | isClosed = true; 56 | } 57 | 58 | private final class Itr implements Iterator { 59 | private final Iterator nodeIterator = node.getChildren().iterator(); 60 | private Path nextPath; 61 | 62 | @Override 63 | public boolean hasNext() { 64 | if (isClosed) { 65 | return false; 66 | } 67 | 68 | if (nextPath != null) { 69 | return true; 70 | } 71 | 72 | while (nodeIterator.hasNext()) { 73 | JAppFileSystem.Node node = nodeIterator.next(); 74 | JAppPath p = (JAppPath) path.resolve(node.getName()); 75 | try { 76 | if (filter == null || filter.accept(p)) { 77 | nextPath = p; 78 | return true; 79 | } 80 | } catch (IOException ignored) { 81 | } 82 | } 83 | return false; 84 | } 85 | 86 | @Override 87 | public Path next() { 88 | if (hasNext()) { 89 | Path p = nextPath; 90 | nextPath = null; 91 | return p; 92 | } else { 93 | throw new NoSuchElementException(); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/jappfs/JAppFileAttributeView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.jappfs; 17 | 18 | import java.io.IOException; 19 | import java.nio.file.LinkOption; 20 | import java.nio.file.NoSuchFileException; 21 | import java.nio.file.ReadOnlyFileSystemException; 22 | import java.nio.file.attribute.BasicFileAttributeView; 23 | import java.nio.file.attribute.FileTime; 24 | 25 | public final class JAppFileAttributeView implements BasicFileAttributeView { 26 | 27 | private final JAppPath path; 28 | private final boolean isJAppView; 29 | private final LinkOption[] options; 30 | 31 | public JAppFileAttributeView(JAppPath path, boolean isJAppView, LinkOption... options) { 32 | this.path = path; 33 | this.isJAppView = isJAppView; 34 | this.options = options; 35 | } 36 | 37 | @Override 38 | public String name() { 39 | return isJAppView ? "japp" : "basic"; 40 | } 41 | 42 | @Override 43 | public JAppFileAttributes readAttributes() throws IOException { 44 | JAppFileSystem fileSystem = path.getFileSystem(); 45 | JAppFileSystem.Node node = fileSystem.resolve(path); 46 | if (node == null) { 47 | throw new NoSuchFileException(path.toString()); 48 | } 49 | return new JAppFileAttributes(fileSystem, node); 50 | } 51 | 52 | @Override 53 | public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException { 54 | throw new ReadOnlyFileSystemException(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/jappfs/JAppFileAttributes.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.jappfs; 17 | 18 | import java.nio.file.attribute.BasicFileAttributes; 19 | import java.nio.file.attribute.FileTime; 20 | 21 | public class JAppFileAttributes implements BasicFileAttributes { 22 | 23 | enum Attribute { 24 | // basic 25 | size, 26 | creationTime, 27 | lastAccessTime, 28 | lastModifiedTime, 29 | isDirectory, 30 | isRegularFile, 31 | isSymbolicLink, 32 | isOther, 33 | fileKey, 34 | 35 | // japp 36 | compressedSize 37 | } 38 | 39 | private final JAppFileSystem fileSystem; 40 | private final JAppFileSystem.Node node; 41 | 42 | public JAppFileAttributes(JAppFileSystem fileSystem, JAppFileSystem.Node node) { 43 | this.fileSystem = fileSystem; 44 | this.node = node; 45 | } 46 | 47 | private FileTime getDefaultFileTime() { 48 | return FileTime.fromMillis(0); 49 | } 50 | 51 | Object getAttribute(Attribute attribute) { 52 | switch (attribute) { 53 | case size: 54 | return size(); 55 | case creationTime: 56 | return creationTime(); 57 | case lastAccessTime: 58 | return lastAccessTime(); 59 | case lastModifiedTime: 60 | return lastModifiedTime(); 61 | case isDirectory: 62 | return isDirectory(); 63 | case isRegularFile: 64 | return isRegularFile(); 65 | case isSymbolicLink: 66 | return isSymbolicLink(); 67 | case isOther: 68 | return isOther(); 69 | case fileKey: 70 | return fileKey(); 71 | case compressedSize: 72 | return compressedSize(); 73 | default: 74 | return null; 75 | } 76 | } 77 | 78 | @Override 79 | public FileTime lastModifiedTime() { 80 | if (node instanceof JAppFileSystem.ResourceNode) { 81 | return ((JAppFileSystem.ResourceNode) node).getResource().getLastModifiedTime(); 82 | } else { 83 | return getDefaultFileTime(); 84 | } 85 | } 86 | 87 | @Override 88 | public FileTime lastAccessTime() { 89 | if (node instanceof JAppFileSystem.ResourceNode) { 90 | return ((JAppFileSystem.ResourceNode) node).getResource().getLastAccessTime(); 91 | } else { 92 | return getDefaultFileTime(); 93 | } 94 | } 95 | 96 | @Override 97 | public FileTime creationTime() { 98 | if (node instanceof JAppFileSystem.ResourceNode) { 99 | return ((JAppFileSystem.ResourceNode) node).getResource().getCreationTime(); 100 | } else { 101 | return getDefaultFileTime(); 102 | } 103 | } 104 | 105 | @Override 106 | public boolean isRegularFile() { 107 | return node instanceof JAppFileSystem.ResourceNode; 108 | } 109 | 110 | @Override 111 | public boolean isDirectory() { 112 | return node instanceof JAppFileSystem.DirectoryNode; 113 | } 114 | 115 | @Override 116 | public boolean isSymbolicLink() { 117 | return false; 118 | } 119 | 120 | @Override 121 | public boolean isOther() { 122 | return false; 123 | } 124 | 125 | @Override 126 | public long size() { 127 | return (node instanceof JAppFileSystem.ResourceNode) ? ((JAppFileSystem.ResourceNode) node).getResource().getSize() : 0L; 128 | } 129 | 130 | @Override 131 | public Object fileKey() { 132 | return node; 133 | } 134 | 135 | // JApp 136 | 137 | public long compressedSize() { 138 | return (node instanceof JAppFileSystem.ResourceNode) ? ((JAppFileSystem.ResourceNode) node).getResource().getCompressedSize() : 0L; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/jappfs/JAppFileStore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.jappfs; 17 | 18 | import java.io.IOException; 19 | import java.nio.file.FileStore; 20 | import java.nio.file.attribute.BasicFileAttributeView; 21 | import java.nio.file.attribute.FileAttributeView; 22 | import java.nio.file.attribute.FileStoreAttributeView; 23 | import java.util.Objects; 24 | 25 | public final class JAppFileStore extends FileStore { 26 | 27 | private final JAppFileSystem fs; 28 | 29 | JAppFileStore(JAppFileSystem fs) { 30 | this.fs = fs; 31 | } 32 | 33 | @Override 34 | public String name() { 35 | return fs + "/"; 36 | } 37 | 38 | @Override 39 | public String type() { 40 | return "jappfs"; 41 | } 42 | 43 | @Override 44 | public boolean isReadOnly() { 45 | return true; 46 | } 47 | 48 | @Override 49 | public boolean supportsFileAttributeView(String name) { 50 | return name.equals("basic"); 51 | } 52 | 53 | @Override 54 | public boolean supportsFileAttributeView(Class type) { 55 | return type == BasicFileAttributeView.class; 56 | } 57 | 58 | @Override 59 | public V getFileStoreAttributeView(Class type) { 60 | Objects.requireNonNull(type); 61 | return null; 62 | } 63 | 64 | @Override 65 | public long getTotalSpace() throws IOException { 66 | throw new UnsupportedOperationException("getTotalSpace"); 67 | } 68 | 69 | @Override 70 | public long getUsableSpace() throws IOException { 71 | return 0L; 72 | } 73 | 74 | @Override 75 | public long getUnallocatedSpace() throws IOException { 76 | return 0L; 77 | } 78 | 79 | @Override 80 | public Object getAttribute(String attribute) throws IOException { 81 | throw new UnsupportedOperationException("does not support " + attribute); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/module/JAppModuleReference.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.module; 17 | 18 | import org.glavo.japp.boot.JAppResourceGroup; 19 | import org.glavo.japp.boot.JAppReader; 20 | import org.glavo.japp.boot.JAppResource; 21 | import org.glavo.japp.boot.JAppResourceRoot; 22 | 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.lang.module.ModuleDescriptor; 26 | import java.lang.module.ModuleReader; 27 | import java.lang.module.ModuleReference; 28 | import java.net.URI; 29 | import java.nio.ByteBuffer; 30 | import java.util.Optional; 31 | import java.util.stream.Stream; 32 | 33 | public final class JAppModuleReference extends ModuleReference implements ModuleReader { 34 | 35 | private final JAppReader reader; 36 | private final JAppResourceGroup group; 37 | 38 | public JAppModuleReference(JAppReader reader, ModuleDescriptor descriptor, JAppResourceGroup group) { 39 | super(descriptor, JAppResourceRoot.MODULES.toURI(group)); 40 | 41 | this.reader = reader; 42 | this.group = group; 43 | } 44 | 45 | @Override 46 | public ModuleReader open() throws IOException { 47 | return this; 48 | } 49 | 50 | // ModuleReader 51 | 52 | @Override 53 | public Optional find(String name) throws IOException { 54 | JAppResource resource = group.get(name); 55 | if (resource != null) { 56 | return Optional.of(JAppResourceRoot.MODULES.toURI(group, resource)); 57 | } else { 58 | return Optional.empty(); 59 | } 60 | } 61 | 62 | @Override 63 | public Optional open(String name) throws IOException { 64 | JAppResource resource = group.get(name); 65 | if (resource != null) { 66 | return Optional.of(reader.openResource(resource)); 67 | } else { 68 | return Optional.empty(); 69 | } 70 | } 71 | 72 | @Override 73 | public Optional read(String name) throws IOException { 74 | JAppResource resource = group.get(name); 75 | if (resource != null) { 76 | return Optional.of(reader.readResource(resource)); 77 | } else { 78 | return Optional.empty(); 79 | } 80 | } 81 | 82 | @Override 83 | public Stream list() throws IOException { 84 | return group.values().stream().map(JAppResource::getName); 85 | } 86 | 87 | @Override 88 | public void close() throws IOException { 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/url/JAppURLConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.url; 17 | 18 | import org.glavo.japp.boot.JAppReader; 19 | import org.glavo.japp.boot.JAppResource; 20 | import org.glavo.japp.boot.JAppResourceRoot; 21 | 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.net.MalformedURLException; 25 | import java.net.URL; 26 | import java.net.URLConnection; 27 | 28 | public final class JAppURLConnection extends URLConnection { 29 | 30 | private final JAppReader reader; 31 | 32 | private JAppResource resource; 33 | 34 | private final JAppResourceRoot root; 35 | private final String group; 36 | private final String path; 37 | 38 | JAppURLConnection(JAppReader reader, URL url) throws MalformedURLException { 39 | super(url); 40 | 41 | this.reader = reader; 42 | 43 | String fullPath = url.getPath(); 44 | if (!fullPath.startsWith("/")) { 45 | throw invalidURL(); 46 | } 47 | 48 | JAppResourceRoot root = null; 49 | String group = null; 50 | String path = null; 51 | for (JAppResourceRoot r : JAppResourceRoot.values()) { 52 | String prefix = r.getPathPrefix(); 53 | if (fullPath.startsWith(prefix) && fullPath.length() > prefix.length() && fullPath.charAt(prefix.length()) == '/') { 54 | int idx = fullPath.indexOf('/', prefix.length() + 1); 55 | if (idx < 0) { 56 | throw invalidURL(); 57 | } 58 | 59 | root = r; 60 | group = fullPath.substring(prefix.length() + 1, idx); 61 | path = fullPath.substring(idx + 1); 62 | break; 63 | } 64 | } 65 | 66 | if (root == null) { 67 | throw invalidURL(); 68 | } 69 | 70 | this.root = root; 71 | this.group = group; 72 | this.path = path; 73 | } 74 | 75 | private MalformedURLException invalidURL() { 76 | return new MalformedURLException("Invalid URL: " + this.url); 77 | } 78 | 79 | @Override 80 | public void connect() throws IOException { 81 | if (connected) { 82 | return; 83 | } 84 | 85 | resource = reader.findResource(root, group, path); 86 | if (resource == null) { 87 | throw new IOException("Resource not found"); 88 | } 89 | 90 | connected = true; 91 | } 92 | 93 | @Override 94 | public InputStream getInputStream() throws IOException { 95 | connect(); 96 | return reader.openResource(resource); 97 | } 98 | 99 | @Override 100 | public long getContentLengthLong() { 101 | try { 102 | connect(); 103 | return resource.getSize(); 104 | } catch (IOException ignored) { 105 | return -1L; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/url/JAppURLHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.url; 17 | 18 | import org.glavo.japp.boot.JAppReader; 19 | 20 | import java.io.IOException; 21 | import java.net.URL; 22 | import java.net.URLConnection; 23 | import java.net.URLStreamHandler; 24 | 25 | public class JAppURLHandler extends URLStreamHandler { 26 | @Override 27 | protected URLConnection openConnection(URL u) throws IOException { 28 | return new JAppURLConnection(JAppReader.getSystemReader(), u); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/url/JAppURLStreamHandlerFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.url; 17 | 18 | import java.net.URL; 19 | import java.net.URLStreamHandler; 20 | import java.net.URLStreamHandlerFactory; 21 | import java.util.*; 22 | 23 | // For Java 8 24 | public class JAppURLStreamHandlerFactory implements URLStreamHandlerFactory { 25 | 26 | private static final String DEFAULT_PREFIX = "sun.net.www.protocol"; 27 | 28 | public static void setup() { 29 | URL.setURLStreamHandlerFactory(new JAppURLStreamHandlerFactory(System.getProperty("java.protocol.handler.pkgs"))); 30 | } 31 | 32 | private final List packagePrefixes; 33 | private final Map handles = new HashMap<>(); 34 | 35 | public JAppURLStreamHandlerFactory(String packagePrefixList) { 36 | handles.put("japp", new JAppURLHandler()); 37 | 38 | if (packagePrefixList == null) { 39 | this.packagePrefixes = Collections.singletonList(DEFAULT_PREFIX); 40 | } else { 41 | Set prefixes = new LinkedHashSet<>(); 42 | 43 | StringTokenizer packagePrefixIter = 44 | new StringTokenizer(packagePrefixList, "|"); 45 | 46 | while (packagePrefixIter.hasMoreTokens()) { 47 | prefixes.add(packagePrefixIter.nextToken().trim()); 48 | } 49 | 50 | prefixes.add(DEFAULT_PREFIX); 51 | 52 | this.packagePrefixes = Arrays.asList(prefixes.toArray(new String[0])); 53 | } 54 | } 55 | 56 | @SuppressWarnings("deprecation") 57 | @Override 58 | public URLStreamHandler createURLStreamHandler(String protocol) { 59 | // Avoid using reflection during bootstrap 60 | 61 | URLStreamHandler handler = handles.get(protocol); 62 | if (handler != null) { 63 | return handler; 64 | } 65 | 66 | synchronized (this) { 67 | handler = handles.get(protocol); 68 | if (handler != null) { 69 | return handler; 70 | } 71 | 72 | 73 | for (String prefix : packagePrefixes) { 74 | String name = prefix + protocol + ".Handler"; 75 | try { 76 | Class cls = Class.forName(name); 77 | handler = (URLStreamHandler) cls.newInstance(); 78 | break; 79 | } catch (Exception ignored) { 80 | } 81 | } 82 | 83 | if (handler != null) { 84 | handles.put(protocol, handler); 85 | } 86 | } 87 | 88 | return handler; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /boot/src/main/java/org/glavo/japp/boot/url/JAppURLStreamHandlerProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.url; 17 | 18 | import java.net.URLStreamHandler; 19 | import java.net.spi.URLStreamHandlerProvider; 20 | 21 | // For Java 9+ 22 | public class JAppURLStreamHandlerProvider extends URLStreamHandlerProvider { 23 | @Override 24 | public URLStreamHandler createURLStreamHandler(String protocol) { 25 | if ("japp".equals(protocol)) { 26 | return new JAppURLHandler(); 27 | } 28 | return null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /boot/src/main/module-info/module-info.java: -------------------------------------------------------------------------------- 1 | module org.glavo.japp.boot { 2 | provides java.net.spi.URLStreamHandlerProvider 3 | with org.glavo.japp.boot.url.JAppURLStreamHandlerProvider; 4 | provides java.nio.file.spi.FileSystemProvider 5 | with org.glavo.japp.boot.jappfs.JAppFileSystemProvider; 6 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("java") 19 | id("com.github.johnrengelman.shadow") version "8.1.1" 20 | } 21 | 22 | allprojects { 23 | apply { 24 | plugin("java") 25 | } 26 | 27 | group = "org.glavo" 28 | version = "0.1.0-SNAPSHOT" 29 | 30 | repositories { 31 | mavenCentral() 32 | } 33 | 34 | tasks.compileJava { 35 | // TODO: Java 8 36 | sourceCompatibility = "9" 37 | targetCompatibility = "9" 38 | } 39 | } 40 | 41 | tasks.create("buildAll") { 42 | dependsOn( 43 | ":boot:bootJar", ":shadowJar" 44 | ) 45 | } 46 | 47 | defaultTasks("buildAll") 48 | 49 | dependencies { 50 | testImplementation(platform("org.junit:junit-bom:5.10.2")) 51 | testImplementation("org.junit.jupiter:junit-jupiter") 52 | 53 | testImplementation(libs.zstd.jni) 54 | 55 | LWJGL.addDependency(this, "testImplementation", "lwjgl") 56 | LWJGL.addDependency(this, "testImplementation", "lwjgl-xxhash") 57 | } 58 | 59 | tasks.compileTestJava { 60 | options.compilerArgs.add("--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED") 61 | } 62 | 63 | tasks.test { 64 | dependsOn( 65 | "buildAll", 66 | ":test-case:HelloWorld:jar", 67 | ":test-case:ModulePath:jar", 68 | ) 69 | 70 | useJUnitPlatform() 71 | jvmArgs( 72 | "--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED" 73 | ) 74 | 75 | fun jarPath(projectName: String) = 76 | project(projectName).tasks.getByName("jar").archiveFile.get().asFile.absolutePath 77 | 78 | fun testCase(projectName: String): String { 79 | val p = project(projectName) 80 | 81 | return p.configurations.runtimeClasspath.get().map { it.absolutePath } 82 | .plus(p.tasks.getByName("jar").archiveFile.get().asFile.absolutePath) 83 | .joinToString(File.pathSeparator) 84 | } 85 | 86 | systemProperties( 87 | "japp.jar" to tasks.getByName("shadowJar").archiveFile.get().asFile.absolutePath, 88 | "japp.testcase.helloworld" to testCase(":test-case:HelloWorld"), 89 | "japp.testcase.modulepath" to testCase(":test-case:ModulePath"), 90 | ) 91 | 92 | testLogging { 93 | this.showStandardStreams = true 94 | } 95 | } 96 | 97 | dependencies { 98 | implementation(project(":base")) 99 | implementation(project(":boot")) 100 | implementation(libs.zstd.jni) 101 | } 102 | 103 | tasks.jar { 104 | manifest.attributes( 105 | "Main-Class" to "org.glavo.japp.Main", 106 | "Add-Exports" to "java.base/jdk.internal.misc" 107 | ) 108 | } 109 | 110 | tasks.shadowJar { 111 | destinationDirectory.set(rootProject.layout.buildDirectory) 112 | archiveFileName.set("japp.jar") 113 | } 114 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glavo/japp/f288a5458a50d22714b294fb3a893ac7bf1d1dff/buildSrc/build.gradle.kts -------------------------------------------------------------------------------- /buildSrc/src/main/java/LWJGL.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import org.gradle.api.artifacts.dsl.DependencyHandler; 18 | 19 | import java.util.Locale; 20 | 21 | public class LWJGL { 22 | private static final String LWJGL_VERSION = "3.3.4"; 23 | private static final String LWJGL_PLATFORM; 24 | 25 | static { 26 | String lwjglPlatform; 27 | 28 | String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT); 29 | String archName = System.getProperty("os.arch").toLowerCase(Locale.ROOT); 30 | if (osName.startsWith("win")) { 31 | lwjglPlatform = "windows"; 32 | } else if (osName.contains("darwin") || osName.contains("mac") || osName.contains("osx")) { 33 | lwjglPlatform = "macos"; 34 | } else { 35 | lwjglPlatform = "linux"; 36 | } 37 | 38 | switch (archName) { 39 | case "x8664": 40 | case "x86-64": 41 | case "x86_64": 42 | case "amd64": 43 | case "ia32e": 44 | case "em64t": 45 | case "x64": 46 | break; 47 | case "x86": 48 | case "i386": 49 | case "i486": 50 | case "i586": 51 | case "i686": 52 | lwjglPlatform += "-x86"; 53 | break; 54 | case "arm64": 55 | case "aarch64": 56 | lwjglPlatform += "-arm64"; 57 | break; 58 | case "arm": 59 | case "arm32": 60 | lwjglPlatform += "-arm32"; 61 | break; 62 | case "riscv64": 63 | lwjglPlatform += "-riscv64"; 64 | break; 65 | case "ppc64le": 66 | case "powerpc64le": 67 | lwjglPlatform += "-ppc64le"; 68 | break; 69 | default: 70 | if (archName.startsWith("armv7")) { 71 | lwjglPlatform += "-arm32"; 72 | } else if (archName.startsWith("armv8") || archName.startsWith("armv9")) { 73 | lwjglPlatform += "-arm64"; 74 | } 75 | } 76 | 77 | LWJGL_PLATFORM = lwjglPlatform; 78 | } 79 | 80 | public static void addDependency(DependencyHandler handler, String configurationName, String moduleName) { 81 | handler.add(configurationName, "org.lwjgl:" + moduleName + ":" + LWJGL_VERSION); 82 | handler.add(configurationName, "org.lwjgl:" + moduleName + ":" + LWJGL_VERSION + ":natives-" + LWJGL_PLATFORM); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /docs/COMPARE.md: -------------------------------------------------------------------------------- 1 | # What's wrong with the existing packaging format? 2 | 3 | For Java programs, we now have many ways to package them, each with different advantages and disadvantages: 4 | 5 | ## jlink/jpackage 6 | 7 | This is the packaging method currently promoted by OpenJDK officials. 8 | 9 | jlink trims the JDK to retain only the required modules, and then packages the JDK and the program together. 10 | 11 | Advantage: 12 | 13 | * The program comes with a Java runtime environment, so users do not need to install additional dependencies; 14 | * Developers can choose the Java runtime to use, avoiding compatibility issues as much as possible. 15 | 16 | Disadvantage: 17 | 18 | * jlink compromises the cross-platform capabilities of Java programs. 19 | This makes it more difficult for users other than Windows/Linux/macOS systems and x86/ARM architectures 20 | (such as Linux RISC-V/LoongArch and AIX users) to use Java programs. 21 | 22 | Most Java programs are actually perfectly cross-platform and can run on any platform that supports the JVM. 23 | However, jlink packages the program with the native files of a specific platform, 24 | so that the packaged program can only run on one platform. 25 | 26 | Although jlink supports cross-building and can package program for other platforms. 27 | However, this is different from Golang, which can easily generate small executable files for different platforms. 28 | jlink needs to download a JDK for each target platform, 29 | and the packaged program ranges from tens of megabytes to hundreds of megabytes. 30 | 31 | For each additional target platform, 32 | an additional 200MB of JDK must be downloaded to the packaging device, 33 | the packaging time will increase by tens of seconds to several minutes, 34 | and finally an additional file of tens to hundreds of MB must be distributed. 35 | This reduces the incentive for developers to provide distribution to more platforms. 36 | 37 | Another thing to consider is, where do we download these JDKs from? 38 | Most developers choose a vendor they trust (such as Eclipse Adoptium, BellSoft, and Azul) 39 | and download all JDKs from him. 40 | This means that the compatibility of the programs they distribute often depends on this vendor. 41 | 42 | The platforms supported by these vendors cover most of the common platforms, 43 | but there are some niche platforms that are not taken care of. 44 | For example, platforms like FreeBSD, Alpine Linux, and Linux LoongArch 64, few JDK vendors considers them, 45 | and Java on these platforms is often provided by the system's package manager. 46 | Therefore, these platforms are rarely considered by developers who use jlink to package programs. 47 | * Since the program is always distributed with the Java runtime environment, 48 | this greatly increases the size of the programs 49 | and the Java runtime can no longer be shared between multiple Java programs. 50 | 51 | For end users, the countless chromium on the disk has already troubled many people, 52 | and jlink has brings countless Java standard libraries/JVMs to their disks. 53 | 54 | For servers, the Java runtime environment does not need to change often. 55 | Traditionally, the Java runtime environment can be installed on the system, or in a base image. 56 | If you want to use jlink, you have to transfer the entire Java standard library/JVM for every update, 57 | which is a complete waste. 58 | * Currently, jlink packages a zip archive instead of a single executable file. 59 | Users need to find a place to unzip/install the program before they can use it, 60 | which is not so convenient and flexible. 61 | 62 | The [Hermetic Java](https://cr.openjdk.org/~jiangli/hermetic_java.pdf) project plans to address this issue, 63 | but it is unknown when it will be completed. 64 | 65 | ## Shadow(Fat) JAR 66 | 67 | Shadow(Fat) JAR technology packages the class files and resources of a Java program and all its dependencies into a single JAR file. 68 | 69 | Advantage: 70 | 71 | * Single file, small, cross-platform, and fast to package. 72 | 73 | Disadvantage: 74 | 75 | * Users need to install Java to run it; 76 | * It cannot choose which Java to start itself with; 77 | * It is not possible to pass JVM options to the JVM and only provides limited control over the JVM through the manifest file; 78 | * Since a JAR file can contain only one module, it does not work well with JPMS and often only works with the classpath. -------------------------------------------------------------------------------- /docs/introduce.md: -------------------------------------------------------------------------------- 1 | Hi everyone, I'm working on a more modern packaging format for Java programs to replace shadow(fat) jars. 2 | 3 | I already have a prototype and hope to get more people to discuss the design before moving forward to the next step 4 | 5 | Here are some features: 6 | 7 | * Pack multiple modular or non-modular JARs into one file. 8 | * Full support for JPMS (Java Module System). 9 | * Smaller file size (via zstd, metadata compression, and constant pool sharing). 10 | * Supports declaring some requirements for Java (such as Java version) and finding a Java that meets the requirements at startup. 11 | * Supports declaring some JVM options (such as `--add-exports`, `--enable-native-access`, `-Da=b`, etc.) 12 | * Support for declaring external dependencies. Artifacts from Maven Central can be added to the classpath or module path to download on demand at startup. 13 | * Supports conditional addition of JVM options, classpath, and module paths. 14 | * Supports appending other content to the file header. For example, we can append an exe/bash launcher to the header to download Java and launch the program. 15 | 16 | My ambition is to make it the standard way of distributing Java programs. 17 | 18 | The official strong recommendation for jlink makes me feel uneasy: 19 | 20 | I want to be able to download a single program file of a few hundred KB to a few MB, rather than a compressed package of a few hundred MB;
21 | I want to avoid having dozens of JVMs and Java standard libraries on disk;
22 | I want to be able to easily get programs that work on Linux RISC-V 64 or FreeBSD. 23 | 24 | For me, the most serious problem among them is that jlink hurts the cross-platform capabilities of programs in the Java world. 25 | 26 | Think about it, in a world dominated by jlink and jpackage, who cares about users of niche platforms like FreeBSD, AIX, and Linux RISC-V 64? 27 | 28 | Although jlink can package program for other platforms, it is different from Golang, 29 | which can easily generate small executable files for different platforms. 30 | The jlink needs to download a JDK for each target platform, 31 | and the packaged program ranges from tens of megabytes to hundreds of megabytes. 32 | 33 | For each additional target platform, 34 | an additional 200MB JDK must be downloaded to the packaging device, 35 | the packaging time increases by tens of seconds to several minutes, 36 | and finally an additional file of tens to hundreds of MB must be distributed. 37 | This reduces the incentive for developers to provide distribution to more platforms. 38 | 39 | Another thing to consider is, where do we download these JDKs from? 40 | Most developers choose a vendor they trust (such as Eclipse Adoptium, BellSoft, and Azul) 41 | and download all JDKs from him. 42 | This means the compatibility of the programs they distribute often depends on this vendor. 43 | 44 | The platforms supported by these vendors cover most of the common platforms, 45 | but there are some niche platforms that are not taken care of. 46 | For example, platforms like FreeBSD, Alpine Linux, and Linux LoongArch 64 are rarely considered by JDK vendors, 47 | and Java on these platforms is often provided by the package manager. 48 | Therefore, these platforms are rarely considered by developers who use jlink to package programs. 49 | 50 | Due to these dissatisfactions, I developed the japp project. 51 | 52 | If you have the same ambition as me, please give me a hand. -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 Glavo 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [versions] 16 | zstd-jni = "1.5.6-8" 17 | 18 | [libraries] 19 | zstd-jni = { module = "com.github.luben:zstd-jni", version.ref = "zstd-jni" } 20 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glavo/japp/f288a5458a50d22714b294fb3a893ac7bf1d1dff/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /native/.gitignore: -------------------------------------------------------------------------------- 1 | /target -------------------------------------------------------------------------------- /native/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "japp" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /native/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "japp" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /native/src/launcher.rs: -------------------------------------------------------------------------------- 1 | use std::process::ExitCode; 2 | 3 | pub fn run_japp(args: Vec) -> ExitCode { 4 | let mut options = Vec::new(); 5 | let mut japp_file = None; 6 | 7 | let mut iter = args.iter(); 8 | while let Some(arg) = iter.next() { 9 | if arg.starts_with('-') { 10 | options.push(arg); 11 | } else { 12 | japp_file = Some(arg); 13 | break; 14 | } 15 | } 16 | 17 | let japp_file = japp_file.expect("Missing japp file name"); 18 | 19 | panic!("TODO: run"); 20 | } -------------------------------------------------------------------------------- /native/src/main.rs: -------------------------------------------------------------------------------- 1 | mod launcher; 2 | 3 | use std::process::ExitCode; 4 | 5 | fn main() -> ExitCode { 6 | let mut iter = std::env::args(); 7 | 8 | iter.next().expect("Missing executable file name"); 9 | 10 | let command = match iter.next() { 11 | Some(command) => command, 12 | None => { 13 | print_help_message(); 14 | return ExitCode::SUCCESS; 15 | } 16 | }; 17 | 18 | return match command.as_str() { 19 | "help" | "-help" | "--help" | "-?" => { 20 | print_help_message(); 21 | ExitCode::SUCCESS 22 | } 23 | "version" | "-version" | "--version" => { 24 | panic!("TODO: version"); 25 | } 26 | "run" => { 27 | launcher::run_japp(iter.collect()) 28 | } 29 | _ => { 30 | eprintln!("Unsupported command: {command}"); 31 | ExitCode::FAILURE 32 | } 33 | }; 34 | } 35 | 36 | fn print_help_message() { 37 | println!("TODO: Help Message") 38 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "japp" 2 | 3 | include( 4 | "base", 5 | "boot", 6 | 7 | "test-case:HelloWorld", 8 | "test-case:ModulePath", 9 | ) 10 | -------------------------------------------------------------------------------- /specification.md: -------------------------------------------------------------------------------- 1 | # Specification (Draft) 2 | 3 | ## types 4 | 5 | `u1`/`u2`/`u4`/`u8`: Little-endian unsigned 1/2/4/8 byte(s) integer. 6 | 7 | 8 | Field: 9 | 10 | ``` 11 | Field { 12 | u1 field_id; 13 | u1[...] field_body; 14 | } 15 | ``` 16 | 17 | FieldList: A list of fields, ending with the field with field_id `0`. 18 | 19 | ## japp file 20 | 21 | File format: 22 | 23 | ``` 24 | JAppFile { 25 | u4 magic_number; // 0x5050414a ("JAPP") 26 | u1[...] data_pool; 27 | BootMetadata boot_metadata; 28 | LauncherMetadata launcher_metadata; 29 | FileEnd { 30 | u4 magic_number; // 0x5050414a ("JAPP") 31 | u2 major_version; 32 | u2 minor_version; 33 | u8 flags; 34 | u8 file_size; 35 | u8 boot_metadata_offset; 36 | u8 launcher_metadata_offset; 37 | u1[24] reserved; 38 | } end; 39 | } 40 | ``` 41 | 42 | ## boot 43 | 44 | [BootMetadata](boot/src/main/java/org/glavo/japp/boot/JAppBootMetadata.java): 45 | 46 | ``` 47 | BootMetadata { 48 | u4 magic_number; // 0x544f4f42 ("BOOT") 49 | u4 group_count; 50 | ByteArrayPool stringsPool; 51 | ResourceGroup[group_count] groups; 52 | } 53 | ``` 54 | 55 | [ByteArrayPool](): 56 | 57 | ``` 58 | ByteArrayPool { 59 | u1 magic_number; // 0xf0 60 | u1 compress_method; 61 | u2 resvered; 62 | u4 count; 63 | u4 uncompressed_bytes_size; 64 | u4 compressed_bytes_size; 65 | u2[count] sizes; 66 | u1[compressed_length] compressed_bytes; 67 | } 68 | ``` 69 | 70 | [ResourceGroup](boot/src/main/java/org/glavo/japp/boot/JAppResourceGroup.java): 71 | 72 | ``` 73 | ResourceGroup { 74 | u1 magic_number; // 0xeb 75 | u1 compress_method; 76 | u2 reserved; 77 | 78 | u4 uncompressed_size; 79 | u4 compressed_size; 80 | u4 resources_count; 81 | u8 checksum; 82 | 83 | u1[compressed_size] compressed_resources; 84 | } 85 | ``` 86 | 87 | [Resource](boot/src/main/java/org/glavo/japp/boot/JAppResource.java): 88 | 89 | ``` 90 | Resource { 91 | u1 magic_number; // 0x1b 92 | u1 compress_method; 93 | u2 path_length; 94 | u4 reserved; 95 | u8 uncompressed_size; 96 | u8 compressed_size; 97 | u8 content_offset; 98 | u1[path_length] path; // UTF-8 99 | 100 | ResourceFields optional_fields; 101 | } 102 | ``` 103 | 104 | ## launcher 105 | 106 | [LauncherMetadata](src/main/java/org/glavo/japp/launcher/JAppLauncherMetadata.java): 107 | 108 | ``` 109 | LauncherMetadata { 110 | ConfigGroup root_group; 111 | } 112 | ``` 113 | 114 | [ConfigGroup](src/main/java/org/glavo/japp/launcher/JAppConfigGroup.java): 115 | 116 | ``` 117 | ConfigGroup { 118 | u4 magic_number; // 0x00505247 ("GRP\0") 119 | ConfigGroupFields fields; 120 | } 121 | ``` 122 | 123 | [ResourceGroupReference](src/main/java/org/glavo/japp/JAppResourceGroupReference.java): 124 | 125 | ``` 126 | ResourceGroupReference { 127 | u1 id; 128 | String name; 129 | union { 130 | LocalResourceGroupReference local_reference; 131 | MavenResourceGroupReference maven_reference; 132 | } reference; 133 | } 134 | 135 | LocalResourceGroupReference { 136 | 137 | u4 index; 138 | u4 multi_count; 139 | { 140 | u4 multi_version; 141 | u4 multi_index; 142 | }[multi_count] multi_index_pairs; 143 | } 144 | 145 | MavenResourceGroupReference { 146 | String repository; 147 | String group; 148 | String artifact; 149 | String version; 150 | String classifier; 151 | } 152 | ``` 153 | 154 | ## share 155 | 156 | Pass data from launcher to boot launcher: `-Dorg.glavo.japp.args=` 157 | 158 | ``` 159 | BootArgs { 160 | String japp_file; 161 | u8 base_offset; 162 | u8 boot_metadata_offset; 163 | u8 boot_metadata_size; 164 | BootArgFields fields; 165 | } 166 | ``` 167 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/Main.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp; 17 | 18 | import org.glavo.japp.launcher.Launcher; 19 | import org.glavo.japp.packer.JAppPacker; 20 | import org.glavo.japp.platform.JavaRuntime; 21 | 22 | import java.io.PrintStream; 23 | import java.util.Arrays; 24 | 25 | public final class Main { 26 | private static void printHelpMessage(PrintStream out) { 27 | out.println("Usage: japp [options]"); 28 | out.println("Supported mode:"); 29 | out.println(" japp create"); 30 | out.println(" japp run"); 31 | out.println(" japp list-java"); 32 | } 33 | 34 | public static void main(String[] args) throws Throwable { 35 | if (args.length == 0) { 36 | printHelpMessage(System.out); 37 | return; 38 | } 39 | 40 | String[] commandArgs = Arrays.copyOfRange(args, 1, args.length); 41 | 42 | switch (args[0]) { 43 | case "help": 44 | case "-help": 45 | case "--help": 46 | printHelpMessage(System.out); 47 | return; 48 | case "create": 49 | JAppPacker.main(commandArgs); 50 | break; 51 | case "run": 52 | Launcher.main(commandArgs); 53 | break; 54 | case "list-java": 55 | JavaRuntime.main(commandArgs); 56 | break; 57 | default: 58 | System.err.println("Unsupported mode: " + args[0]); 59 | printHelpMessage(System.err); 60 | System.exit(1); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/condition/AndCondition.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.condition; 17 | 18 | import org.glavo.japp.platform.JAppRuntimeContext; 19 | 20 | import java.util.Iterator; 21 | import java.util.List; 22 | 23 | public final class AndCondition implements Condition { 24 | private final List conditions; 25 | 26 | public AndCondition(List conditions) { 27 | assert conditions.size() >= 2; 28 | this.conditions = conditions; 29 | } 30 | 31 | @Override 32 | public boolean test(JAppRuntimeContext context) { 33 | for (Condition condition : conditions) { 34 | if (!condition.test(context)) { 35 | return false; 36 | } 37 | } 38 | 39 | return true; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | StringBuilder res = new StringBuilder(); 45 | Iterator it = conditions.iterator(); 46 | res.append("and("); 47 | res.append(it.next()); 48 | while (it.hasNext()) { 49 | res.append(", ").append(it.next()); 50 | } 51 | res.append(')'); 52 | return res.toString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/condition/Condition.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.glavo.japp.condition; 18 | 19 | import org.glavo.japp.platform.JAppRuntimeContext; 20 | 21 | import java.util.function.Predicate; 22 | 23 | @FunctionalInterface 24 | public interface Condition extends Predicate { 25 | boolean test(JAppRuntimeContext context); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/condition/JavaCondition.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.condition; 17 | 18 | import org.glavo.japp.platform.JAppRuntimeContext; 19 | 20 | import java.util.Map; 21 | import java.util.StringJoiner; 22 | 23 | public final class JavaCondition implements Condition { 24 | 25 | public static JavaCondition fromMap(Map options) { 26 | String version = options.remove("version"); 27 | String os = options.remove("os"); 28 | String arch = options.remove("arch"); 29 | String libc = options.remove("libc"); 30 | 31 | if (!options.isEmpty()) { 32 | throw new IllegalArgumentException("Unknown options: " + options.keySet()); 33 | } 34 | 35 | return new JavaCondition( 36 | version == null ? null : Integer.parseInt(version), 37 | MatchList.of(os), MatchList.of(arch), MatchList.of(libc) 38 | ); 39 | } 40 | 41 | private final Integer version; 42 | private final MatchList os; 43 | private final MatchList arch; 44 | private final MatchList libc; 45 | 46 | private JavaCondition(Integer version, MatchList os, MatchList arch, MatchList libc) { 47 | this.version = version; 48 | this.os = os; 49 | this.arch = arch; 50 | this.libc = libc; 51 | } 52 | 53 | @Override 54 | @SuppressWarnings("deprecation") 55 | public boolean test(JAppRuntimeContext context) { 56 | if (version != null && context.getJava().getVersion().major() < version) { 57 | return false; 58 | } 59 | 60 | if (os != null && !os.test(context.getJava().getOperatingSystem().getCheckedName())) { 61 | return false; 62 | } 63 | 64 | if (arch != null && !arch.test(context.getJava().getArchitecture().getCheckedName())) { 65 | return false; 66 | } 67 | 68 | if (libc != null && !libc.test(context.getJava().getLibC().toString())) { 69 | return false; 70 | } 71 | 72 | return true; 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | StringJoiner joiner = new StringJoiner(", ", "java(", ")"); 78 | if (version != null) { 79 | joiner.add("version=" + version); 80 | } 81 | if (os != null) { 82 | joiner.add("os=" + os); 83 | } 84 | if (arch != null) { 85 | joiner.add("arch=" + arch); 86 | } 87 | if (libc != null) { 88 | joiner.add("libc=" + libc); 89 | } 90 | return joiner.toString(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/condition/MatchList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.condition; 17 | 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import java.util.function.Predicate; 21 | 22 | public final class MatchList implements Predicate { 23 | 24 | public static MatchList of(String list) { 25 | if (list == null) { 26 | return null; 27 | } 28 | 29 | boolean negative; 30 | if (list.startsWith("!")) { 31 | list = list.substring(1); 32 | negative = true; 33 | } else { 34 | negative = false; 35 | } 36 | 37 | return new MatchList(negative, Arrays.asList(list.split("\\|"))); 38 | } 39 | 40 | private final boolean negative; 41 | private final List values; 42 | 43 | public MatchList(boolean negative, List values) { 44 | this.negative = negative; 45 | this.values = values; 46 | } 47 | 48 | @Override 49 | public boolean test(String s) { 50 | return negative != values.contains(s); 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return (negative ? "!" : "") + values; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/condition/NotCondition.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.condition; 17 | 18 | import org.glavo.japp.platform.JAppRuntimeContext; 19 | 20 | public final class NotCondition implements Condition { 21 | private final Condition original; 22 | 23 | public NotCondition(Condition original) { 24 | this.original = original; 25 | } 26 | 27 | @Override 28 | public boolean test(JAppRuntimeContext context) { 29 | return !original.test(context); 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "not(" + original + ')'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/condition/OrCondition.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.condition; 17 | 18 | import org.glavo.japp.platform.JAppRuntimeContext; 19 | 20 | import java.util.Iterator; 21 | import java.util.List; 22 | 23 | public final class OrCondition implements Condition { 24 | private final List conditions; 25 | 26 | public OrCondition(List conditions) { 27 | assert conditions.size() >= 2; 28 | this.conditions = conditions; 29 | } 30 | 31 | @Override 32 | public boolean test(JAppRuntimeContext context) { 33 | for (Condition condition : conditions) { 34 | if (condition.test(context)) { 35 | return true; 36 | } 37 | } 38 | 39 | return false; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | StringBuilder res = new StringBuilder(); 45 | Iterator it = conditions.iterator(); 46 | res.append("or("); 47 | res.append(it.next()); 48 | while (it.hasNext()) { 49 | res.append(", ").append(it.next()); 50 | } 51 | res.append(')'); 52 | return res.toString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/launcher/EmbeddedLauncher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.launcher; 17 | 18 | import org.glavo.japp.Main; 19 | 20 | import java.nio.file.Paths; 21 | import java.util.Arrays; 22 | import java.util.Collections; 23 | 24 | public final class EmbeddedLauncher { 25 | public static void main(String[] args) throws Throwable { 26 | Launcher.run(Paths.get(Main.class.getProtectionDomain().getCodeSource().getLocation().toURI()), Collections.emptyList(), Arrays.asList(args)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/maven/MavenResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.maven; 17 | 18 | import java.nio.file.Path; 19 | import java.nio.file.Paths; 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | 23 | public final class MavenResolver { 24 | 25 | public static final MavenRepository CENTRAL = new MavenRepository.Remote("central", "https://repo1.maven.org/maven2"); 26 | public static final MavenRepository LOCAL = new MavenRepository.Local("local", Paths.get(System.getProperty("user.home"), ".m2", ".repository")); 27 | 28 | private static final Map repos = new HashMap<>(); 29 | 30 | static { 31 | repos.put(CENTRAL.getName(), CENTRAL); 32 | repos.put(LOCAL.getName(), LOCAL); 33 | } 34 | 35 | public static Path resolve(String repoName, String group, String artifact, String version, String classifier) throws Throwable { 36 | MavenRepository repo; 37 | if (repoName == null) { 38 | repo = CENTRAL; 39 | } else { 40 | repo = repos.get(repoName); 41 | if (repo == null) { 42 | throw new IllegalArgumentException("Unknown repo: " + repoName); 43 | } 44 | } 45 | 46 | return repo.resolve(group, artifact, version, classifier); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/JAppResourceInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer; 17 | 18 | import org.glavo.japp.CompressionMethod; 19 | 20 | import java.nio.file.attribute.FileTime; 21 | 22 | public final class JAppResourceInfo { 23 | final String name; 24 | 25 | FileTime creationTime; 26 | FileTime lastModifiedTime; 27 | FileTime lastAccessTime; 28 | 29 | boolean hasWritten = false; 30 | long offset; 31 | long size; 32 | CompressionMethod method; 33 | long compressedSize; 34 | 35 | Long checksum; 36 | 37 | public JAppResourceInfo(String name) { 38 | this.name = name; 39 | } 40 | 41 | public void setCreationTime(FileTime creationTime) { 42 | this.creationTime = creationTime; 43 | } 44 | 45 | public void setLastModifiedTime(FileTime lastModifiedTime) { 46 | this.lastModifiedTime = lastModifiedTime; 47 | } 48 | 49 | public void setLastAccessTime(FileTime lastAccessTime) { 50 | this.lastAccessTime = lastAccessTime; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/JAppResourcesWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer; 17 | 18 | import org.glavo.japp.launcher.JAppResourceGroupReference; 19 | import org.glavo.japp.packer.compressor.CompressResult; 20 | import org.glavo.japp.util.XxHash64; 21 | 22 | import java.io.IOException; 23 | import java.util.LinkedHashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.TreeMap; 27 | 28 | public final class JAppResourcesWriter implements AutoCloseable { 29 | private final JAppWriter writer; 30 | private final String name; 31 | private final List referenceList; 32 | 33 | private final Map resources = new LinkedHashMap<>(); 34 | private final Map> multiReleaseResources = new TreeMap<>(); 35 | 36 | JAppResourcesWriter(JAppWriter writer, String name, List referenceList) { 37 | this.writer = writer; 38 | this.name = name; 39 | this.referenceList = referenceList; 40 | } 41 | 42 | public void writeResource(JAppResourceInfo resource, byte[] body) throws IOException { 43 | writeResource(-1, resource, body); 44 | } 45 | 46 | public void writeResource(int release, JAppResourceInfo resource, byte[] body) throws IOException { 47 | if (resource.hasWritten) { 48 | throw new AssertionError("Resource " + resource.name + " has been written"); 49 | } 50 | 51 | resource.hasWritten = true; 52 | 53 | Map resources; 54 | if (release == -1) { 55 | resources = this.resources; 56 | } else { 57 | resources = this.multiReleaseResources.computeIfAbsent(release, r -> new LinkedHashMap<>()); 58 | } 59 | 60 | resources.put(resource.name, resource); 61 | resource.offset = writer.getCurrentOffset(); 62 | resource.size = body.length; 63 | resource.checksum = XxHash64.hash(body); 64 | 65 | CompressResult result = writer.compressor.compress(writer, body, resource.name); 66 | resource.method = result.getMethod(); 67 | resource.compressedSize = result.getLength(); 68 | 69 | writer.getOutput().writeBytes(result.getCompressedData(), result.getOffset(), result.getLength()); 70 | } 71 | 72 | private int addGroup(Map group) { 73 | int index = writer.groups.size(); 74 | writer.groups.add(group); 75 | return index; 76 | } 77 | 78 | public void close() { 79 | int baseIndex = addGroup(resources); 80 | TreeMap multiIndexes; 81 | if (!multiReleaseResources.isEmpty()) { 82 | multiIndexes = new TreeMap<>(); 83 | multiReleaseResources.forEach((i, g) -> multiIndexes.put(i, addGroup(g))); 84 | } else { 85 | multiIndexes = null; 86 | } 87 | referenceList.add(new JAppResourceGroupReference.Local(name, baseIndex, multiIndexes)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/ModuleInfoReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer; 17 | 18 | import org.glavo.japp.packer.compressor.classfile.ClassFileReader; 19 | 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.lang.module.ModuleDescriptor; 23 | import java.nio.ByteBuffer; 24 | import java.util.regex.Matcher; 25 | import java.util.regex.Pattern; 26 | 27 | public final class ModuleInfoReader { 28 | private static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))"); 29 | private static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]"); 30 | private static final Pattern REPEATING_DOTS = Pattern.compile("(\\.)(\\1)+"); 31 | private static final Pattern TRAILING_DOTS = Pattern.compile("\\.$"); 32 | 33 | public static String deriveAutomaticModuleName(String jarFileName) { 34 | if (!jarFileName.endsWith(".jar")) { 35 | throw new IllegalArgumentException(jarFileName); 36 | } 37 | 38 | final int end = jarFileName.length() - ".jar".length(); 39 | 40 | int start = 0; 41 | for (; start < end; start++) { 42 | if (jarFileName.charAt(start) != '.') { 43 | break; 44 | } 45 | } 46 | 47 | if (start == end) { 48 | throw new IllegalArgumentException(jarFileName); 49 | } 50 | 51 | String name = jarFileName.substring(start, end); 52 | 53 | // find first occurrence of -${NUMBER}. or -${NUMBER}$ 54 | Matcher matcher = DASH_VERSION.matcher(name); 55 | if (matcher.find()) { 56 | name = name.substring(0, matcher.start()); 57 | } 58 | 59 | name = NON_ALPHANUM.matcher(name).replaceAll("."); 60 | name = REPEATING_DOTS.matcher(name).replaceAll("."); 61 | 62 | // drop trailing dots 63 | int len = name.length(); 64 | if (len > 0 && name.charAt(len - 1) == '.') { 65 | name = TRAILING_DOTS.matcher(name).replaceAll(""); 66 | } 67 | 68 | return name; 69 | } 70 | 71 | public static String readModuleName(InputStream moduleInfo) throws IOException { 72 | String moduleName = new ClassFileReader(ByteBuffer.wrap(moduleInfo.readAllBytes())).getModuleName(); 73 | if (moduleName == null) { 74 | throw new IOException(); 75 | } 76 | return moduleName; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/compressor/CompressContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer.compressor; 17 | 18 | import com.github.luben.zstd.ZstdCompressCtx; 19 | import org.glavo.japp.packer.compressor.classfile.ByteArrayPoolBuilder; 20 | 21 | public interface CompressContext { 22 | ByteArrayPoolBuilder getPool(); 23 | 24 | default ZstdCompressCtx getZstdCompressCtx() { 25 | return new ZstdCompressCtx(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/compressor/CompressResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer.compressor; 17 | 18 | import org.glavo.japp.CompressionMethod; 19 | 20 | import java.nio.ByteBuffer; 21 | import java.util.Arrays; 22 | 23 | public final class CompressResult { 24 | private final CompressionMethod method; 25 | private final byte[] compressedData; 26 | 27 | private final int offset; 28 | private final int length; 29 | 30 | public CompressResult(byte[] compressedData) { 31 | this(CompressionMethod.NONE, compressedData, 0, compressedData.length); 32 | } 33 | 34 | public CompressResult(CompressionMethod method, byte[] compressedData) { 35 | this(method, compressedData, 0, compressedData.length); 36 | } 37 | 38 | public CompressResult(CompressionMethod method, byte[] compressedData, int offset, int length) { 39 | this.method = method; 40 | this.compressedData = compressedData; 41 | this.offset = offset; 42 | this.length = length; 43 | } 44 | 45 | public CompressionMethod getMethod() { 46 | return method; 47 | } 48 | 49 | public ByteBuffer getCompressed() { 50 | return ByteBuffer.wrap(compressedData, offset, length); 51 | } 52 | 53 | public byte[] getCompressedData() { 54 | return compressedData; 55 | } 56 | 57 | public int getOffset() { 58 | return offset; 59 | } 60 | 61 | public int getLength() { 62 | return length; 63 | } 64 | 65 | @Override 66 | public String toString() { 67 | return "CompressResult{" + 68 | "method=" + method + 69 | ", compressedData=" + Arrays.toString(compressedData) + 70 | ", offset=" + offset + 71 | ", length=" + length + 72 | '}'; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/compressor/Compressor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer.compressor; 17 | 18 | import java.io.IOException; 19 | 20 | @FunctionalInterface 21 | public interface Compressor { 22 | 23 | CompressResult compress(CompressContext context, byte[] source) throws IOException; 24 | 25 | default CompressResult compress(CompressContext context, byte[] source, String filePath) throws IOException { 26 | return compress(context, source); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/compressor/Compressors.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer.compressor; 17 | 18 | import org.glavo.japp.CompressionMethod; 19 | import org.glavo.japp.util.ZstdUtils; 20 | import org.glavo.japp.packer.compressor.classfile.ClassFileCompressor; 21 | 22 | public final class Compressors { 23 | 24 | public static final Compressor DEFAULT = new DefaultCompressor(); 25 | 26 | public static final Compressor CLASSFILE = new ClassFileCompressor(); 27 | 28 | public static final Compressor ZSTD = (context, source) -> { 29 | byte[] res = new byte[ZstdUtils.maxCompressedLength(source.length)]; 30 | long n = context.getZstdCompressCtx().compressByteArray(res, 0, res.length, source, 0, source.length); 31 | return new CompressResult(CompressionMethod.ZSTD, res, 0, (int) n); 32 | }; 33 | 34 | private Compressors() { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/compressor/DefaultCompressor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer.compressor; 17 | 18 | import org.glavo.japp.CompressionMethod; 19 | 20 | import java.io.IOException; 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | final class DefaultCompressor implements Compressor { 25 | 26 | private final Map map = new HashMap<>(); 27 | private final CompressionMethod defaultMethod = CompressionMethod.ZSTD; 28 | 29 | public DefaultCompressor() { 30 | map.put("class", CompressionMethod.CLASSFILE); 31 | 32 | for (String ext : new String[]{ 33 | "png", "apng", "jpg", "jpeg", "webp", "heic", "heif", "avif", 34 | "aac", "flac", "mp3", 35 | "mp4", "mkv", "webm", 36 | "gz", "tgz", "xz", "br", "zst", "bz2", "tbz2" 37 | }) { 38 | map.put(ext, CompressionMethod.NONE); 39 | } 40 | } 41 | 42 | @Override 43 | public CompressResult compress(CompressContext context, byte[] source) throws IOException { 44 | return compress(context, source, null); 45 | } 46 | 47 | @Override 48 | public CompressResult compress(CompressContext context, byte[] source, String filePath) throws IOException { 49 | if (source.length <= 16) { 50 | return new CompressResult(source); 51 | } 52 | 53 | String ext; 54 | if (filePath != null) { 55 | int sep = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); 56 | int dot = filePath.lastIndexOf('.'); 57 | ext = dot > sep ? filePath.substring(dot + 1) : null; 58 | } else { 59 | ext = null; 60 | } 61 | 62 | CompressionMethod method = map.getOrDefault(ext, defaultMethod); 63 | CompressResult result; 64 | switch (method) { 65 | case NONE: 66 | result = new CompressResult(source); 67 | break; 68 | case CLASSFILE: 69 | try { 70 | result = Compressors.CLASSFILE.compress(context, source); 71 | } catch (Throwable e) { 72 | // Malformed class file 73 | result = Compressors.ZSTD.compress(context, source); 74 | } 75 | break; 76 | case ZSTD: 77 | result = Compressors.ZSTD.compress(context, source); 78 | break; 79 | default: 80 | throw new AssertionError("Unimplemented compression method: " + method); 81 | } 82 | 83 | return result.getLength() < source.length ? result : new CompressResult(source); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/compressor/classfile/ByteArrayPoolBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer.compressor.classfile; 17 | 18 | import com.github.luben.zstd.Zstd; 19 | import org.glavo.japp.CompressionMethod; 20 | import org.glavo.japp.boot.decompressor.classfile.ByteArrayPool; 21 | import org.glavo.japp.boot.decompressor.zstd.ZstdFrameDecompressor; 22 | import org.glavo.japp.io.LittleEndianDataOutput; 23 | import org.glavo.japp.util.ZstdUtils; 24 | import org.glavo.japp.io.ByteBufferOutputStream; 25 | 26 | import java.io.IOException; 27 | import java.nio.ByteBuffer; 28 | import java.nio.ByteOrder; 29 | import java.util.Arrays; 30 | import java.util.HashMap; 31 | 32 | public final class ByteArrayPoolBuilder { 33 | private static final class ByteArrayWrapper { 34 | final byte[] bytes; 35 | final int hash; 36 | 37 | private ByteArrayWrapper(byte[] bytes) { 38 | this.bytes = bytes; 39 | this.hash = Arrays.hashCode(bytes); 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | return hash; 45 | } 46 | 47 | @Override 48 | public boolean equals(Object obj) { 49 | if (!(obj instanceof ByteArrayWrapper)) { 50 | return false; 51 | } 52 | 53 | ByteArrayWrapper other = (ByteArrayWrapper) obj; 54 | return Arrays.equals(this.bytes, other.bytes); 55 | } 56 | } 57 | 58 | private final HashMap map = new HashMap<>(); 59 | private ByteBuffer bytes = ByteBuffer.allocate(8192).order(ByteOrder.LITTLE_ENDIAN); 60 | private ByteBuffer sizes = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN); 61 | 62 | private void growIfNeed(int s) { 63 | if (bytes.remaining() < s) { 64 | int position = bytes.position(); 65 | int nextLen = Math.max(bytes.limit() * 2, position + s); 66 | bytes = ByteBuffer.wrap(Arrays.copyOf(bytes.array(), nextLen)).position(position); 67 | } 68 | 69 | if (!sizes.hasRemaining()) { 70 | int position = sizes.position(); 71 | sizes = ByteBuffer.wrap(Arrays.copyOf(sizes.array(), position * 2)) 72 | .position(position) 73 | .order(ByteOrder.LITTLE_ENDIAN); 74 | } 75 | } 76 | 77 | public int add(byte[] bytes) { 78 | assert bytes.length <= 0xffff; 79 | 80 | ByteArrayWrapper wrapper = new ByteArrayWrapper(bytes); 81 | 82 | Integer index = map.get(wrapper); 83 | if (index != null) { 84 | return index; 85 | } 86 | 87 | index = map.size(); 88 | map.put(wrapper, index); 89 | 90 | growIfNeed(bytes.length); 91 | this.sizes.putShort((short) bytes.length); 92 | this.bytes.put(bytes); 93 | 94 | return index; 95 | } 96 | 97 | public void writeTo(LittleEndianDataOutput output) throws IOException { 98 | int count = map.size(); 99 | int uncompressedBytesSize = bytes.position(); 100 | 101 | CompressionMethod compressionMethod; 102 | byte[] compressed = new byte[ZstdUtils.maxCompressedLength(uncompressedBytesSize)]; 103 | long compressedBytesSize = Zstd.compressByteArray(compressed, 0, compressed.length, bytes.array(), 0, uncompressedBytesSize, 8); 104 | if (compressedBytesSize < uncompressedBytesSize) { 105 | compressionMethod = CompressionMethod.ZSTD; 106 | } else { 107 | compressionMethod = CompressionMethod.NONE; 108 | compressed = bytes.array(); 109 | compressedBytesSize = uncompressedBytesSize; 110 | } 111 | 112 | output.writeByte(ByteArrayPool.MAGIC_NUMBER); 113 | output.writeByte(compressionMethod.id()); 114 | output.writeShort((short) 0); 115 | output.writeInt(count); 116 | output.writeInt(uncompressedBytesSize); 117 | output.writeInt((int) compressedBytesSize); 118 | output.writeBytes(sizes.array(), 0, sizes.position()); 119 | output.writeBytes(compressed, 0, (int) compressedBytesSize); 120 | } 121 | 122 | public ByteArrayPool toPool() throws IOException { 123 | ByteBufferOutputStream output = new ByteBufferOutputStream(); 124 | writeTo(output); 125 | return ByteArrayPool.readFrom(output.getByteBuffer().flip(), new ZstdFrameDecompressor()); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/processor/ClassPathProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer.processor; 17 | 18 | import org.glavo.japp.packer.JAppWriter; 19 | 20 | import java.util.Map; 21 | 22 | public abstract class ClassPathProcessor { 23 | private static ClassPathProcessor getProcessor(String type) { 24 | if (type == null) { 25 | return LocalClassPathProcessor.INSTANCE; 26 | } 27 | switch (type) { 28 | case "local": 29 | return LocalClassPathProcessor.INSTANCE; 30 | case "maven": 31 | return new MavenClassPathProcessor(); 32 | default: 33 | throw new IllegalArgumentException("Unknown type: " + type); 34 | } 35 | } 36 | 37 | public static void process(JAppWriter writer, String pathList, boolean isModulePath) throws Throwable { 38 | if (pathList == null || pathList.isEmpty()) { 39 | return; 40 | } 41 | 42 | PathListParser parser = new PathListParser(pathList); 43 | while (parser.scanNext()) { 44 | Map options = parser.options; 45 | String path = parser.path; 46 | 47 | ClassPathProcessor processor = getProcessor(options.remove("type")); 48 | processor.process(writer, path, isModulePath, options); 49 | } 50 | } 51 | 52 | public abstract void process(JAppWriter packer, String path, boolean isModulePath, Map options) throws Throwable; 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/processor/MavenClassPathProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer.processor; 17 | 18 | import org.glavo.japp.launcher.JAppResourceGroupReference; 19 | import org.glavo.japp.maven.MavenResolver; 20 | import org.glavo.japp.packer.JAppWriter; 21 | import org.glavo.japp.packer.ModuleInfoReader; 22 | 23 | import java.lang.module.ModuleFinder; 24 | import java.lang.module.ModuleReference; 25 | import java.nio.file.Path; 26 | import java.util.Map; 27 | import java.util.Set; 28 | import java.util.regex.Matcher; 29 | import java.util.regex.Pattern; 30 | 31 | public final class MavenClassPathProcessor extends ClassPathProcessor { 32 | 33 | private final Pattern pattern = Pattern.compile("(?[^/]+)/(?[^/]+)/(?[^/]+)(/(?[^/]+))?"); 34 | 35 | @Override 36 | public void process(JAppWriter packer, String path, boolean isModulePath, Map options) throws Throwable { 37 | boolean bundle = !"false".equals(options.remove("bundle")); 38 | String repo = options.remove("repository"); 39 | boolean verify = !"false".equals(options.remove("verify")); // TODO 40 | 41 | if (!options.isEmpty()) { 42 | throw new IllegalArgumentException("Unrecognized options: " + options.keySet()); 43 | } 44 | 45 | Matcher matcher = pattern.matcher(path); 46 | if (!matcher.matches()) { 47 | throw new IllegalArgumentException("Invalid path: " + path); 48 | } 49 | 50 | String group = matcher.group("group"); 51 | String artifact = matcher.group("artifact"); 52 | String version = matcher.group("version"); 53 | String classifier = matcher.group("classifier"); 54 | 55 | Path file = MavenResolver.resolve(repo, group, artifact, version, classifier); 56 | 57 | if (bundle) { 58 | LocalClassPathProcessor.addJar(packer, file, isModulePath); 59 | } else { 60 | String name; 61 | if (isModulePath) { 62 | ModuleFinder finder = ModuleFinder.of(file); // TODO: need opt 63 | Set all = finder.findAll(); 64 | assert all.size() == 1; 65 | name = all.iterator().next().descriptor().name(); 66 | } else { 67 | name = file.getFileName().toString(); 68 | } 69 | 70 | packer.addReference( 71 | new JAppResourceGroupReference.Maven(name, repo, group, artifact, version, classifier), 72 | isModulePath 73 | ); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/packer/processor/PathListParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer.processor; 17 | 18 | import java.io.File; 19 | import java.util.LinkedHashMap; 20 | import java.util.Map; 21 | 22 | final class PathListParser { 23 | static final char pathSeparatorChar = File.pathSeparatorChar; 24 | 25 | private final String list; 26 | private int offset = 0; 27 | 28 | Map options; 29 | String path; 30 | 31 | PathListParser(String list) { 32 | this.list = list; 33 | } 34 | 35 | private void ensureNotAtEnd() { 36 | if (offset >= list.length()) { 37 | throw new IllegalArgumentException("Unexpected end of input"); 38 | } 39 | } 40 | 41 | private void skipWhitespace() { 42 | while (offset < list.length() && list.charAt(offset) == ' ') { 43 | offset++; 44 | } 45 | } 46 | 47 | private boolean isLiteralChar(char ch) { 48 | switch (ch) { 49 | case '@': 50 | case '#': 51 | case '%': 52 | case '+': 53 | case '-': 54 | case '*': 55 | case '/': 56 | case '!': 57 | return true; 58 | default: 59 | return Character.isJavaIdentifierPart(ch); 60 | } 61 | } 62 | 63 | private String nextLiteral() { 64 | if (offset >= list.length()) { 65 | return ""; 66 | } 67 | 68 | String res; 69 | char ch = list.charAt(offset); 70 | if (ch == '\'' || ch == '"') { 71 | int end = list.indexOf(ch, offset + 1); 72 | if (end < 0) { 73 | throw new IllegalArgumentException(); 74 | } 75 | 76 | res = list.substring(offset + 1, end); 77 | offset = end + 1; 78 | } else if (isLiteralChar(ch)) { 79 | int end = offset + 1; 80 | while (end < list.length() && isLiteralChar(list.charAt(end))) { 81 | end++; 82 | } 83 | res = list.substring(offset, end); 84 | offset = end; 85 | } else { 86 | throw new IllegalArgumentException(); 87 | } 88 | 89 | return res; 90 | } 91 | 92 | private void readPair() { 93 | String name = nextLiteral(); 94 | if (name.isEmpty()) { 95 | throw new IllegalArgumentException("Option name cannot be empty"); 96 | } 97 | 98 | skipWhitespace(); 99 | ensureNotAtEnd(); 100 | 101 | if (list.charAt(offset++) != '=') { 102 | throw new IllegalArgumentException(); 103 | } 104 | 105 | skipWhitespace(); 106 | String value = nextLiteral(); 107 | 108 | if (options.put(name, value) != null) { 109 | throw new IllegalArgumentException("Duplicate option: " + name); 110 | } 111 | } 112 | 113 | boolean scanNext() { 114 | if (offset >= list.length()) { 115 | return false; 116 | } 117 | 118 | char ch = 0; 119 | while (offset < list.length() && (ch = list.charAt(offset)) == pathSeparatorChar) { 120 | offset++; 121 | } 122 | 123 | if (offset >= list.length()) { 124 | return false; 125 | } 126 | 127 | options = new LinkedHashMap<>(); 128 | if (ch == '[') { 129 | offset++; 130 | skipWhitespace(); 131 | ensureNotAtEnd(); 132 | 133 | boolean isFirst = true; 134 | while ((ch = list.charAt(offset)) != ']') { 135 | if (isFirst) { 136 | isFirst = false; 137 | } else { 138 | if (ch != ',') { 139 | throw new IllegalArgumentException(); 140 | } 141 | offset++; 142 | skipWhitespace(); 143 | ensureNotAtEnd(); 144 | } 145 | 146 | readPair(); 147 | skipWhitespace(); 148 | ensureNotAtEnd(); 149 | } 150 | 151 | offset++; // skip ']' 152 | } 153 | 154 | int end = list.indexOf(pathSeparatorChar, offset); 155 | if (end < 0) { 156 | path = list.substring(offset); 157 | offset = list.length(); 158 | } else { 159 | path = list.substring(offset, end); 160 | offset = end + 1; 161 | } 162 | 163 | return true; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/platform/Architecture.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.platform; 17 | 18 | import java.util.Locale; 19 | 20 | public enum Architecture { 21 | X86(false, "x86"), 22 | X86_64(true, "x86-64"), 23 | IA64(true, "IA-64"), 24 | SPARC(false, "SPARC"), 25 | SPARCV9(true, "SPARC V9"), 26 | ARM(false, "ARM"), 27 | AARCH64(true, "AArch64"), 28 | MIPS(false, "MIPS (Big-Endian)"), 29 | MIPS64(true, "MIPS64 (Big-Endian)"), 30 | MIPSEL(false, "MIPS (Little-Endian)"), 31 | MIPS64EL(true, "MIPS64 (Little-Endian)"), 32 | PPC(false, "PowerPC (Big-Endian)"), 33 | PPC64(true, "PowerPC-64 (Big-Endian)"), 34 | PPCLE(false, "PowerPC (Little-Endian)"), 35 | PPC64LE(true, "PowerPC-64 (Little-Endian)"), 36 | S390(false, "S390"), 37 | S390X(true, "S390x"), 38 | RISCV32(false, "RISC-V 32"), 39 | RISCV64(true, "RISC-V 64"), 40 | LOONGARCH32(false, "LoongArch32"), 41 | LOONGARCH64(true, "LoongArch64"); 42 | 43 | private final String checkedName; 44 | private final String displayName; 45 | private final boolean is64Bit; 46 | 47 | Architecture(boolean is64Bit, String displayName) { 48 | this.checkedName = this.name().toLowerCase(Locale.ROOT).replace("_", "-"); 49 | this.displayName = displayName; 50 | this.is64Bit = is64Bit; 51 | } 52 | 53 | public static Architecture parseArchitecture(String value) { 54 | value = value.trim().toLowerCase(Locale.ROOT); 55 | 56 | switch (value) { 57 | case "x8664": 58 | case "x86-64": 59 | case "x86_64": 60 | case "amd64": 61 | case "ia32e": 62 | case "em64t": 63 | case "x64": 64 | return X86_64; 65 | case "x8632": 66 | case "x86-32": 67 | case "x86_32": 68 | case "x86": 69 | case "i86pc": 70 | case "i386": 71 | case "i486": 72 | case "i586": 73 | case "i686": 74 | case "ia32": 75 | case "x32": 76 | return X86; 77 | case "arm64": 78 | case "aarch64": 79 | return AARCH64; 80 | case "arm": 81 | case "arm32": 82 | return ARM; 83 | case "mips64": 84 | return MIPS64; 85 | case "mips64el": 86 | return MIPS64EL; 87 | case "mips": 88 | case "mips32": 89 | return MIPS; 90 | case "mipsel": 91 | case "mips32el": 92 | return MIPSEL; 93 | case "riscv32": 94 | return RISCV32; 95 | case "riscv64": 96 | return RISCV64; 97 | case "ia64": 98 | case "ia64w": 99 | case "itanium64": 100 | return IA64; 101 | case "sparcv9": 102 | case "sparc64": 103 | return SPARCV9; 104 | case "sparc": 105 | case "sparc32": 106 | return SPARC; 107 | case "ppc64": 108 | case "powerpc64": 109 | return PPC64; 110 | case "ppc64le": 111 | case "powerpc64le": 112 | return PPC64LE; 113 | case "ppc": 114 | case "ppc32": 115 | case "powerpc": 116 | case "powerpc32": 117 | return PPC; 118 | case "ppcle": 119 | case "ppc32le": 120 | case "powerpcle": 121 | case "powerpc32le": 122 | return PPCLE; 123 | case "s390": 124 | return S390; 125 | case "s390x": 126 | return S390X; 127 | case "loongarch32": 128 | return LOONGARCH32; 129 | case "loongarch64": 130 | return LOONGARCH64; 131 | default: 132 | if (value.startsWith("armv7")) { 133 | return ARM; 134 | } 135 | if (value.startsWith("armv8") || value.startsWith("armv9")) { 136 | return AARCH64; 137 | } 138 | } 139 | 140 | throw new IllegalArgumentException(); 141 | } 142 | 143 | public boolean is64Bit() { 144 | return is64Bit; 145 | } 146 | 147 | public String getCheckedName() { 148 | return checkedName; 149 | } 150 | 151 | public String getDisplayName() { 152 | return displayName; 153 | } 154 | 155 | @Override 156 | public String toString() { 157 | return displayName; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/platform/JAppRuntimeContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.platform; 17 | 18 | import org.glavo.japp.launcher.JAppConfigGroup; 19 | 20 | public final class JAppRuntimeContext { 21 | 22 | public static JAppRuntimeContext search(JAppConfigGroup config) { 23 | for (JavaRuntime java : JavaRuntime.getAllJava()) { 24 | JAppRuntimeContext context = new JAppRuntimeContext(java); 25 | if (config.canApply(context)) { 26 | return context; 27 | } 28 | } 29 | 30 | return null; 31 | } 32 | 33 | private final JavaRuntime java; 34 | 35 | public JAppRuntimeContext(JavaRuntime java) { 36 | this.java = java; 37 | } 38 | 39 | public JavaRuntime getJava() { 40 | return java; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/platform/LibC.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.platform; 17 | 18 | import java.util.Locale; 19 | 20 | public enum LibC { 21 | DEFAULT, MUSL; 22 | 23 | public static LibC parseLibC(String value) { 24 | switch (value) { 25 | case "": 26 | case "default": 27 | case "gnu": 28 | return DEFAULT; 29 | case "musl": 30 | return MUSL; 31 | default: 32 | throw new IllegalArgumentException(value); 33 | } 34 | } 35 | 36 | private final String checkedName = this.name().toLowerCase(Locale.ROOT); 37 | 38 | public String getCheckedName() { 39 | return checkedName; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return getCheckedName(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/glavo/japp/platform/OperatingSystem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.platform; 17 | 18 | import java.io.IOException; 19 | import java.nio.file.Path; 20 | import java.util.Locale; 21 | 22 | public enum OperatingSystem { 23 | WINDOWS("Windows"), 24 | LINUX("Linux"), 25 | MACOS("macOS"); 26 | 27 | private final String checkedName; 28 | private final String displayName; 29 | 30 | OperatingSystem(String displayName) { 31 | this.checkedName = this.name().toLowerCase(Locale.ROOT); 32 | this.displayName = displayName; 33 | } 34 | 35 | public static OperatingSystem parseOperatingSystem(String name) { 36 | name = name.trim().toLowerCase(Locale.ROOT); 37 | if (name.contains("mac") || name.contains("darwin")) 38 | return MACOS; 39 | else if (name.contains("win")) 40 | return WINDOWS; 41 | else if (name.contains("linux")) 42 | return LINUX; 43 | else 44 | throw new IllegalArgumentException(name); 45 | } 46 | 47 | public String getCheckedName() { 48 | return checkedName; 49 | } 50 | 51 | public String getDisplayName() { 52 | return displayName; 53 | } 54 | 55 | public Path findJavaExecutable(Path javaHome) throws IOException { 56 | if (this == WINDOWS) { 57 | return javaHome.resolve("bin/java.exe"); 58 | } else { 59 | return javaHome.resolve("bin/java"); 60 | } 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return displayName; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/resources/org/glavo/japp/packer/header.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # TODO: In future we should look for japp launcher in the PATH 4 | exec "%japp.project.directory%/bin/japp.sh" run "${BASH_SOURCE[0]}" "$@" 5 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/boot/JAppResourceTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot; 17 | 18 | public class JAppResourceTest { 19 | 20 | public void test() { 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/boot/decompressor/zstd/ZstdTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.boot.decompressor.zstd; 17 | 18 | import com.github.luben.zstd.Zstd; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import java.nio.ByteBuffer; 22 | import java.nio.file.Paths; 23 | import java.util.Arrays; 24 | import java.util.Collections; 25 | import java.util.Random; 26 | import java.util.zip.ZipEntry; 27 | import java.util.zip.ZipFile; 28 | 29 | import static org.junit.jupiter.api.Assertions.*; 30 | 31 | public class ZstdTest { 32 | 33 | private static void testDecompress(byte[] bytes, boolean checksum) throws Throwable { 34 | byte[] compressed; 35 | if (checksum) { 36 | byte[] tmp = new byte[bytes.length * 2 + 128]; 37 | long len = Zstd.compress(tmp, bytes, Zstd.defaultCompressionLevel(), true); 38 | compressed = Arrays.copyOf(tmp, Math.toIntExact(len)); 39 | } else { 40 | compressed = Zstd.compress(bytes); 41 | } 42 | 43 | ZstdFrameDecompressor decompressor = new ZstdFrameDecompressor(); 44 | 45 | byte[] decompressed = new byte[bytes.length]; 46 | int decompressedLen = decompressor.decompress(compressed, 0, compressed.length, decompressed, 0, decompressed.length); 47 | assertArrayEquals(bytes, decompressed); 48 | assertEquals(bytes.length, decompressedLen); 49 | 50 | Arrays.fill(decompressed, (byte) 0); 51 | ByteBuffer compressedBuffer = ByteBuffer.wrap(compressed); 52 | ByteBuffer decompressedBuffer = ByteBuffer.wrap(decompressed); 53 | 54 | decompressedLen = decompressor.decompress(compressedBuffer, decompressedBuffer); 55 | assertFalse(compressedBuffer.hasRemaining()); 56 | assertFalse(decompressedBuffer.hasRemaining()); 57 | assertArrayEquals(bytes, decompressed); 58 | assertEquals(bytes.length, decompressedLen); 59 | 60 | compressedBuffer = ByteBuffer.allocateDirect(compressed.length); 61 | compressedBuffer.put(compressed); 62 | compressedBuffer.flip(); 63 | decompressedBuffer = ByteBuffer.allocateDirect(bytes.length); 64 | 65 | decompressedLen = decompressor.decompress(compressedBuffer, decompressedBuffer); 66 | assertFalse(compressedBuffer.hasRemaining()); 67 | assertFalse(decompressedBuffer.hasRemaining()); 68 | 69 | decompressedBuffer.flip(); 70 | decompressedBuffer.get(decompressed); 71 | 72 | assertArrayEquals(bytes, decompressed); 73 | assertEquals(bytes.length, decompressedLen); 74 | } 75 | 76 | @Test 77 | void testDecompressor() throws Throwable { 78 | for (int len = 0; len <= 128; len++) { 79 | for (int seed = 0; seed < 100; seed++) { 80 | byte[] bytes = new byte[len]; 81 | new Random(seed).nextBytes(bytes); 82 | try { 83 | testDecompress(bytes, false); 84 | testDecompress(bytes, true); 85 | } catch (Throwable e) { 86 | throw new AssertionError(String.format("seed=%s, len=%s", seed, len), e); 87 | } 88 | } 89 | } 90 | 91 | try (ZipFile zf = new ZipFile(Paths.get(Zstd.class.getProtectionDomain().getCodeSource().getLocation().toURI()).toFile())) { 92 | for (ZipEntry entry : Collections.list(zf.entries())) { 93 | if (!entry.isDirectory()) { 94 | byte[] bytes = zf.getInputStream(entry).readAllBytes(); 95 | try { 96 | testDecompress(bytes, false); 97 | testDecompress(bytes, true); 98 | } catch (Throwable e) { 99 | throw new AssertionError("entry=" + entry.getName()); 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/classfile/ByteArrayPoolTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.classfile; 17 | 18 | import org.glavo.japp.boot.decompressor.classfile.ByteArrayPool; 19 | import org.glavo.japp.packer.compressor.classfile.ByteArrayPoolBuilder; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | import java.util.Arrays; 25 | 26 | import static java.nio.charset.StandardCharsets.US_ASCII; 27 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 28 | import static org.junit.jupiter.api.Assertions.assertEquals; 29 | 30 | public class ByteArrayPoolTest { 31 | 32 | private static byte[] testString(int index) { 33 | return ("str" + index).getBytes(US_ASCII); 34 | } 35 | 36 | void test(int n) throws IOException { 37 | ByteArrayPoolBuilder builder = new ByteArrayPoolBuilder(); 38 | 39 | for (int i = 0; i < n; i++) { 40 | assertEquals(i, builder.add(testString(i))); 41 | assertEquals(i, builder.add(testString(i))); 42 | } 43 | 44 | ByteArrayPool pool = builder.toPool(); 45 | 46 | for (int i = 0; i < n; i++) { 47 | byte[] testString = testString(i); 48 | 49 | ByteBuffer buffer = ByteBuffer.allocate(20); 50 | assertEquals(testString.length, pool.get(i, buffer)); 51 | assertEquals(testString.length, buffer.position()); 52 | 53 | byte[] out = new byte[testString.length]; 54 | buffer.flip().get(out); 55 | assertArrayEquals(testString, out); 56 | 57 | Arrays.fill(out, (byte) 0); 58 | pool.get(i).get(out); 59 | assertArrayEquals(testString, out); 60 | } 61 | } 62 | 63 | @Test 64 | void test() throws IOException { 65 | test(0); 66 | test(10); 67 | test(100); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/launcher/EndZipTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.launcher; 17 | 18 | import com.github.luben.zstd.Zstd; 19 | import org.glavo.japp.io.IOUtils; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.params.ParameterizedTest; 22 | import org.junit.jupiter.params.provider.MethodSource; 23 | 24 | import java.io.IOException; 25 | import java.net.URISyntaxException; 26 | import java.nio.ByteBuffer; 27 | import java.nio.ByteOrder; 28 | import java.nio.channels.FileChannel; 29 | import java.nio.file.Path; 30 | import java.util.stream.Stream; 31 | 32 | import static org.junit.jupiter.api.Assertions.assertEquals; 33 | 34 | public class EndZipTest { 35 | 36 | private static Stream jars() { 37 | return Stream.of(Test.class, Zstd.class).map(clazz -> { 38 | try { 39 | return Path.of(clazz.getProtectionDomain().getCodeSource().getLocation().toURI()); 40 | } catch (URISyntaxException e) { 41 | throw new AssertionError(e); 42 | } 43 | }); 44 | } 45 | 46 | @ParameterizedTest 47 | @MethodSource("jars") 48 | void testGetSize(Path zipFile) throws IOException { 49 | try (FileChannel channel = FileChannel.open(zipFile)) { 50 | long fileSize = channel.size(); 51 | 52 | int endBufferSize = (int) Math.max(fileSize, 8192); 53 | ByteBuffer endBuffer = ByteBuffer.allocate(endBufferSize).order(ByteOrder.LITTLE_ENDIAN); 54 | 55 | channel.position(fileSize - endBufferSize); 56 | IOUtils.readFully(channel, endBuffer); 57 | 58 | endBuffer.flip(); 59 | 60 | assertEquals(fileSize, JAppLauncherMetadata.getEndZipSize(endBuffer)); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/packer/ModuleInfoReaderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer; 17 | 18 | import com.github.luben.zstd.Zstd; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.params.ParameterizedTest; 21 | import org.junit.jupiter.params.provider.Arguments; 22 | import org.junit.jupiter.params.provider.MethodSource; 23 | 24 | import java.io.ByteArrayInputStream; 25 | import java.io.File; 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | import java.util.Map; 29 | import java.util.stream.Stream; 30 | import java.util.zip.ZipFile; 31 | 32 | import static org.junit.jupiter.api.Assertions.assertEquals; 33 | 34 | public final class ModuleInfoReaderTest { 35 | @Test 36 | public void deriveAutomaticModuleNameTest() { 37 | assertEquals("a", ModuleInfoReader.deriveAutomaticModuleName("a.jar")); 38 | assertEquals("a", ModuleInfoReader.deriveAutomaticModuleName("a-0.1.0.jar")); 39 | assertEquals("a", ModuleInfoReader.deriveAutomaticModuleName("...a-0.1.0.jar")); 40 | assertEquals("a", ModuleInfoReader.deriveAutomaticModuleName("...a...-0.1.0.jar")); 41 | assertEquals("a.b", ModuleInfoReader.deriveAutomaticModuleName("a-b-0.1.0.jar")); 42 | assertEquals("a.b", ModuleInfoReader.deriveAutomaticModuleName("a--b-0.1.0.jar")); 43 | } 44 | 45 | static Stream readModuleNameTestArguments() { 46 | return Map.of( 47 | "org.junit.jupiter.api", Test.class, 48 | "com.github.luben.zstd_jni", Zstd.class 49 | ).entrySet().stream().map(entry -> { 50 | 51 | byte[] moduleInfo; 52 | try (ZipFile zipFile = new ZipFile(new File(entry.getValue().getProtectionDomain().getCodeSource().getLocation().toURI()))) { 53 | try (InputStream inputStream = zipFile.getInputStream(zipFile.getEntry("module-info.class"))) { 54 | moduleInfo = inputStream.readAllBytes(); 55 | } 56 | } catch (Exception e) { 57 | throw new AssertionError(e); 58 | } 59 | 60 | return Arguments.of(entry.getKey(), moduleInfo); 61 | }); 62 | } 63 | 64 | @ParameterizedTest 65 | @MethodSource("readModuleNameTestArguments") 66 | public void readModuleNameTest(String moduleName, byte[] moduleInfo) throws IOException { 67 | assertEquals(moduleName, ModuleInfoReader.readModuleName(new ByteArrayInputStream(moduleInfo))); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/packer/processor/PathListParserTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.packer.processor; 17 | 18 | import org.junit.jupiter.api.Assertions; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import java.io.File; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | import java.util.Map; 25 | 26 | public class PathListParserTest { 27 | static final String PS = File.pathSeparator; 28 | 29 | private record Pair(Map options, String path) { 30 | } 31 | 32 | private static List parse(String list) { 33 | List res = new ArrayList<>(); 34 | PathListParser parser = new PathListParser(list); 35 | while (parser.scanNext()) { 36 | res.add(new Pair(parser.options, parser.path)); 37 | } 38 | return res; 39 | } 40 | 41 | @Test 42 | public void test() { 43 | Assertions.assertEquals(List.of(), parse("")); 44 | Assertions.assertEquals(List.of(), parse(PS)); 45 | Assertions.assertEquals(List.of(), parse(PS + PS)); 46 | Assertions.assertEquals( 47 | List.of(new Pair(Map.of(), "A"), new Pair(Map.of(), "B"), new Pair(Map.of(), "C")), 48 | parse(String.join(PS, "A", "B", "", "C", "")) 49 | ); 50 | Assertions.assertEquals( 51 | List.of(new Pair(Map.of(), "A"), 52 | new Pair(Map.of("type", "maven", "repo", "local"), "B"), 53 | new Pair(Map.of("type", "local"), "C"), 54 | new Pair(Map.of("type", "local"), "")), 55 | parse(String.join(PS, "[]A", "[type=maven,repo=local]B", "[type='local']C", "[type=local]", "")) 56 | ); 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/testcase/HelloWorldTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.testcase; 17 | 18 | import java.util.List; 19 | import java.util.stream.Stream; 20 | 21 | public final class HelloWorldTest implements JAppTestTemplate { 22 | public static final String FILE = JAppTestHelper.getTestCase("helloworld"); 23 | public static final String MAIN_CLASS = "org.glavo.japp.testcase.helloworld.HelloWorld"; 24 | 25 | @Override 26 | public Stream tests() { 27 | return Stream.of( 28 | newTest("module path", List.of("--module-path", FILE, MAIN_CLASS), List.of("Hello World!")), 29 | newTest("classpath", List.of("--classpath", FILE, MAIN_CLASS), List.of("Hello World!")), 30 | newTest("module path with end zip", List.of("--embed-launcher", "--module-path", FILE, MAIN_CLASS), List.of("Hello World!")) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/testcase/JAppTestHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.testcase; 17 | 18 | import org.junit.jupiter.api.Assertions; 19 | 20 | import java.io.Closeable; 21 | import java.io.IOException; 22 | import java.nio.charset.StandardCharsets; 23 | import java.nio.file.Files; 24 | import java.nio.file.Path; 25 | import java.util.ArrayList; 26 | import java.util.Collections; 27 | import java.util.List; 28 | 29 | public final class JAppTestHelper { 30 | private static final String jar = System.getProperty("japp.jar"); 31 | private static final boolean isWindows = System.getProperty("os.name").startsWith("Win"); 32 | 33 | private static String runJApp(String mode, List args) throws IOException { 34 | ArrayList list = new ArrayList<>(); 35 | list.add(System.getProperty("java.home") + (isWindows ? "\\bin\\java.exe" : "/bin/java")); 36 | list.add("-Dsun.stdout.encoding=UTF-8"); 37 | list.add("-Dsun.stderr.encoding=UTF-8"); 38 | list.add("-Dstdout.encoding=UTF-8"); 39 | list.add("-Dstderr.encoding=UTF-8"); 40 | list.add("-jar"); 41 | list.add(jar); 42 | list.add(mode); 43 | list.addAll(args); 44 | 45 | try { 46 | Process process = Runtime.getRuntime().exec(list.toArray(new String[0])); 47 | int res = process.waitFor(); 48 | if (res != 0) { 49 | throw new RuntimeException("Process exit code is " + res + ", stderr=" + 50 | new String(process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8)); 51 | } 52 | return new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); 53 | } catch (InterruptedException e) { 54 | throw new AssertionError(e); 55 | } 56 | } 57 | 58 | public static final class FileHolder implements Closeable { 59 | 60 | public final Path file; 61 | 62 | public FileHolder(Path file) { 63 | this.file = file; 64 | } 65 | 66 | @Override 67 | public void close() throws IOException { 68 | Files.deleteIfExists(file); 69 | } 70 | } 71 | 72 | public static String getTestCase(String name) { 73 | String file = System.getProperty("japp.testcase." + name); 74 | if (file == null) { 75 | throw new AssertionError(name); 76 | } 77 | 78 | return file; 79 | } 80 | 81 | public static FileHolder create(String... args) throws IOException { 82 | Path targetFile = Files.createTempFile("japp-test-", ".japp").toAbsolutePath(); 83 | 84 | ArrayList argsList = new ArrayList<>(); 85 | argsList.add("-o"); 86 | argsList.add(targetFile.toString()); 87 | Collections.addAll(argsList, args); 88 | runJApp("create", argsList); 89 | 90 | return new FileHolder(targetFile); 91 | } 92 | 93 | public static String launch(Path file, String... args) throws IOException { 94 | ArrayList argsList = new ArrayList<>(); 95 | argsList.add(file.toAbsolutePath().normalize().toString()); 96 | Collections.addAll(argsList, args); 97 | return runJApp("run", argsList); 98 | } 99 | 100 | public static void assertLines(String value, String... lines) throws IOException { 101 | StringBuilder builder = new StringBuilder(); 102 | for (String line : lines) { 103 | builder.append(line).append(System.lineSeparator()); 104 | } 105 | 106 | Assertions.assertEquals(builder.toString(), value); 107 | } 108 | 109 | public static void test(List args, List lines) throws IOException { 110 | try (JAppTestHelper.FileHolder holder = JAppTestHelper.create(args.toArray(String[]::new))) { 111 | assertLines(JAppTestHelper.launch(holder.file), lines.toArray(String[]::new)); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/testcase/JAppTestTemplate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.testcase; 17 | 18 | import org.junit.jupiter.api.DynamicTest; 19 | import org.junit.jupiter.api.TestFactory; 20 | 21 | import java.util.List; 22 | import java.util.stream.Stream; 23 | 24 | public interface JAppTestTemplate { 25 | Stream tests(); 26 | 27 | default TestArgument newTest(String name, List argument, List lines) { 28 | return new TestArgument(name, argument, lines); 29 | } 30 | 31 | @TestFactory 32 | default Stream testFactory() { 33 | return tests().map(argument -> DynamicTest.dynamicTest(argument.name, () -> { 34 | try (JAppTestHelper.FileHolder holder = JAppTestHelper.create(argument.argument.toArray(String[]::new))) { 35 | JAppTestHelper.assertLines(JAppTestHelper.launch(holder.file), argument.lines.toArray(String[]::new)); 36 | } 37 | })); 38 | } 39 | 40 | class TestArgument { 41 | public final String name; 42 | public final List argument; 43 | public final List lines; 44 | 45 | TestArgument(String name, List argument, List lines) { 46 | this.name = name; 47 | this.argument = argument; 48 | this.lines = lines; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/testcase/ModulePathTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.testcase; 17 | 18 | import java.util.List; 19 | import java.util.stream.Stream; 20 | 21 | public final class ModulePathTest implements JAppTestTemplate { 22 | 23 | public static final String FILE = JAppTestHelper.getTestCase("modulepath"); 24 | 25 | @Override 26 | public Stream tests() { 27 | return Stream.of( 28 | newTest("test", 29 | List.of("--module-path", FILE, "org.glavo.japp.testcase.modulepath.ModulePath"), 30 | List.of( 31 | "japp:/modules/org.glavo.japp.testcase.modulepath/org/glavo/japp/testcase/modulepath/ModulePath.class", 32 | "japp:/modules/com.google.gson/com/google/gson/Gson.class", 33 | "japp:/modules/org.apache.commons.lang3/org/apache/commons/lang3/ObjectUtils.class" 34 | ) 35 | ) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/util/CompressedNumberTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.util; 17 | 18 | import org.junit.jupiter.api.Assertions; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import java.nio.ByteBuffer; 22 | import java.util.Random; 23 | 24 | public class CompressedNumberTest { 25 | 26 | private static void testCompressInt(int v) { 27 | ByteBuffer buffer = ByteBuffer.allocate(5); 28 | CompressedNumber.putInt(buffer, v); 29 | buffer.flip(); 30 | int cv = CompressedNumber.getInt(buffer); 31 | 32 | Assertions.assertEquals(v, cv); 33 | } 34 | 35 | @Test 36 | void testCompressInt() { 37 | for (int i = 0; i <= 256; i++) { 38 | testCompressInt(i); 39 | } 40 | 41 | Random random = new Random(0); 42 | for (int i = 0; i < 256; i++) { 43 | testCompressInt(random.nextInt(Integer.MAX_VALUE)); 44 | } 45 | 46 | testCompressInt(Integer.MAX_VALUE); 47 | 48 | Assertions.assertThrows(AssertionError.class, () -> testCompressInt(-1)); 49 | Assertions.assertThrows(AssertionError.class, () -> testCompressInt(-Integer.MAX_VALUE)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/util/MUTF8Test.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.util; 17 | 18 | import org.junit.jupiter.params.ParameterizedTest; 19 | import org.junit.jupiter.params.provider.MethodSource; 20 | 21 | import java.io.ByteArrayOutputStream; 22 | import java.io.DataOutputStream; 23 | import java.io.IOException; 24 | import java.util.Arrays; 25 | import java.util.stream.Stream; 26 | 27 | import static org.junit.jupiter.api.Assertions.assertEquals; 28 | 29 | public final class MUTF8Test { 30 | private static Stream strs() { 31 | return Stream.of( 32 | "", 33 | "\0\0\0", 34 | "Hello World!", 35 | "Hello àáâãäå", 36 | "Hello 测试字符串", 37 | "测试测试ABC测试测试\0测试" 38 | ); 39 | } 40 | 41 | @ParameterizedTest 42 | @MethodSource("strs") 43 | public void testDecode(String str) throws IOException { 44 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 45 | try (DataOutputStream dataOutput = new DataOutputStream(byteArrayOutputStream)) { 46 | dataOutput.writeUTF(str); 47 | } 48 | 49 | byte[] arr = byteArrayOutputStream.toByteArray(); 50 | 51 | assertEquals(str, MUTF8.stringFromMUTF8(Arrays.copyOfRange(arr, 2, arr.length))); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/org/glavo/japp/util/XxHash64Test.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.util; 17 | 18 | import org.junit.jupiter.params.ParameterizedTest; 19 | import org.junit.jupiter.params.provider.MethodSource; 20 | 21 | import java.nio.ByteBuffer; 22 | import java.util.Random; 23 | import java.util.stream.IntStream; 24 | 25 | import static org.junit.jupiter.api.Assertions.assertEquals; 26 | 27 | public class XxHash64Test { 28 | private static IntStream testArguments() { 29 | return IntStream.concat( 30 | IntStream.rangeClosed(0, 32), 31 | IntStream.iterate(33, it -> it < 512, it -> it + 7) 32 | ).flatMap(it -> IntStream.of(it, it + 8192)); 33 | } 34 | 35 | @ParameterizedTest 36 | @MethodSource("testArguments") 37 | public void test(int length) { 38 | byte[] data = new byte[length]; 39 | new Random(0).nextBytes(data); 40 | 41 | ByteBuffer nativeBuffer = ByteBuffer.allocateDirect(length); 42 | nativeBuffer.put(data); 43 | nativeBuffer.position(0); 44 | 45 | long expected = org.lwjgl.util.xxhash.XXHash.XXH64(nativeBuffer, 0L); 46 | nativeBuffer.position(0); 47 | 48 | assertEquals(expected, XxHash64.hashByteBufferWithoutUpdate(ByteBuffer.wrap(data))); 49 | assertEquals(expected, XxHash64.hashByteBufferWithoutUpdate(nativeBuffer)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test-case/HelloWorld/build.gradle.kts: -------------------------------------------------------------------------------- 1 | tasks.compileJava { 2 | options.release.set(9) 3 | } -------------------------------------------------------------------------------- /test-case/HelloWorld/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module org.glavo.japp.testcase.helloworld { 2 | } -------------------------------------------------------------------------------- /test-case/HelloWorld/src/main/java/org/glavo/japp/testcase/helloworld/HelloWorld.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.testcase.helloworld; 17 | 18 | public final class HelloWorld { 19 | public static void main(String[] args) { 20 | System.out.println("Hello World!"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test-case/ModulePath/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | // https://mvnrepository.com/artifact/com.google.code.gson/gson 3 | implementation("com.google.code.gson:gson:2.10.1") 4 | 5 | // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 6 | implementation("org.apache.commons:commons-lang3:3.14.0") 7 | } 8 | 9 | tasks.jar { 10 | manifest.attributes( 11 | "Automatic-Module-Name" to "org.glavo.japp.testcase.modulepath" 12 | ) 13 | } -------------------------------------------------------------------------------- /test-case/ModulePath/src/main/java/org/glavo/japp/testcase/modulepath/ModulePath.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Glavo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.glavo.japp.testcase.modulepath; 17 | 18 | import com.google.gson.Gson; 19 | import org.apache.commons.lang3.ObjectUtils; 20 | 21 | public final class ModulePath { 22 | public static void main(String[] args) { 23 | System.out.println(ModulePath.class.getResource("ModulePath.class")); 24 | System.out.println(Gson.class.getResource("Gson.class")); 25 | System.out.println(ObjectUtils.class.getResource("ObjectUtils.class")); 26 | } 27 | } 28 | --------------------------------------------------------------------------------