-
Notifications
You must be signed in to change notification settings - Fork 480
fix: Cross-platform file open handler for .unit files (macOS double-click support) #528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d52fe4b
8460753
760e45d
59a229e
8ce370a
52997ed
d78c2cc
730f2c5
4075dde
0315537
be47e56
58b4a77
8fc1d7b
e27addb
7d8d8c6
5ac029a
94e54ea
fb008c7
9416914
38bdd1d
d0b2b0f
84d0c27
1183d8f
120b699
e61846b
a752e72
ffe2f2f
38f90db
54a5df2
f2a95a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,113 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package cc.unitmesh.agent.artifact | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.coroutines.Dispatchers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.coroutines.withContext | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.io.* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.zip.ZipEntry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.zip.ZipInputStream | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.zip.ZipOutputStream | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Android implementation of ArtifactBundlePacker | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Uses java.util.zip for ZIP operations (same as JVM) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actual class ArtifactBundlePacker { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actual suspend fun pack(bundle: ArtifactBundle, outputPath: String): PackResult = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| withContext(Dispatchers.IO) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val outputFile = File(outputPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| outputFile.parentFile?.mkdirs() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ZipOutputStream(FileOutputStream(outputFile)).use { zipOut -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add ARTIFACT.md | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val artifactMd = bundle.generateArtifactMd() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| addZipEntry(zipOut, ArtifactBundle.ARTIFACT_MD, artifactMd) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add package.json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val packageJson = bundle.generatePackageJson() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| addZipEntry(zipOut, ArtifactBundle.PACKAGE_JSON, packageJson) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add main content | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val mainFileName = bundle.getMainFileName() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| addZipEntry(zipOut, mainFileName, bundle.mainContent) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add additional files | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bundle.files.forEach { (fileName, content) -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| addZipEntry(zipOut, fileName, content) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add context.json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val contextJson = ArtifactBundleUtils.serializeBundle(bundle) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| addZipEntry(zipOut, ArtifactBundle.CONTEXT_JSON, contextJson) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PackResult.Success(outputPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e: Exception) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PackResult.Error("Failed to pack bundle: ${e.message}", e) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actual suspend fun unpack(inputPath: String): UnpackResult = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| withContext(Dispatchers.IO) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val files = mutableMapOf<String, String>() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ZipInputStream(FileInputStream(inputPath)).use { zipIn -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var entry: ZipEntry? = zipIn.nextEntry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while (entry != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!entry.isDirectory) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val content = zipIn.readBytes().toString(Charsets.UTF_8) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| files[entry.name] = content | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| zipIn.closeEntry() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entry = zipIn.nextEntry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val bundle = ArtifactBundleUtils.reconstructBundle(files) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (bundle != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UnpackResult.Success(bundle) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UnpackResult.Error("Failed to reconstruct bundle from ZIP contents") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e: Exception) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UnpackResult.Error("Failed to unpack bundle: ${e.message}", e) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actual suspend fun extractToDirectory(inputPath: String, outputDir: String): PackResult = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| withContext(Dispatchers.IO) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val outputDirectory = File(outputDir) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| outputDirectory.mkdirs() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ZipInputStream(FileInputStream(inputPath)).use { zipIn -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var entry: ZipEntry? = zipIn.nextEntry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while (entry != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!entry.isDirectory) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val outputFile = File(outputDirectory, entry.name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| outputFile.parentFile?.mkdirs() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileOutputStream(outputFile).use { fileOut -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| zipIn.copyTo(fileOut) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| zipIn.closeEntry() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entry = zipIn.nextEntry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+85
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Zip Slip vulnerability: validate entry paths before extraction. The code directly uses 🔎 Proposed fix to prevent Zip Slip ZipInputStream(FileInputStream(inputPath)).use { zipIn ->
var entry: ZipEntry? = zipIn.nextEntry
while (entry != null) {
if (!entry.isDirectory) {
val outputFile = File(outputDirectory, entry.name)
+ // Prevent Zip Slip: ensure the output file is within the target directory
+ if (!outputFile.canonicalPath.startsWith(outputDirectory.canonicalPath + File.separator)) {
+ throw SecurityException("Zip entry is outside of the target dir: ${entry.name}")
+ }
outputFile.parentFile?.mkdirs()
FileOutputStream(outputFile).use { fileOut ->
zipIn.copyTo(fileOut)
}
}
zipIn.closeEntry()
entry = zipIn.nextEntry
}
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PackResult.Success(outputDir) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e: Exception) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PackResult.Error("Failed to extract bundle: ${e.message}", e) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private fun addZipEntry(zipOut: ZipOutputStream, fileName: String, content: String) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val entry = ZipEntry(fileName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| zipOut.putNextEntry(entry) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| zipOut.write(content.toByteArray(Charsets.UTF_8)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| zipOut.closeEntry() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add
dependsOn("jvmMainClasses")for task reliability.Unlike the
runNanoDslScenarioHarnesstask (line 330), this task doesn't declare a dependency onjvmMainClasses. This may cause failures when running the task on a clean build.🔎 Proposed fix
tasks.register<JavaExec>("generateTestUnit") { group = "verification" description = "Generate a test .unit file for verification" + dependsOn("jvmMainClasses") val jvmCompilation = kotlin.jvm().compilations.getByName("main") classpath(jvmCompilation.output, configurations["jvmRuntimeClasspath"]) mainClass.set("cc.unitmesh.agent.artifact.GenerateTestUnitKt") }📝 Committable suggestion
🤖 Prompt for AI Agents