feat(die): integrate DiE analysis and result parsing
This commit is contained in:
parent
3b57f89943
commit
ae8f5b87a6
14
Dockerfile.die
Normal file
14
Dockerfile.die
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
ARG DIE_VERSION=3.21
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends wget ca-certificates \
|
||||||
|
&& wget -q https://github.com/horsicq/DIE-engine/releases/download/${DIE_VERSION}/die_${DIE_VERSION}_Ubuntu_24.04_amd64.deb \
|
||||||
|
&& apt-get install -y ./die_${DIE_VERSION}_Ubuntu_24.04_amd64.deb \
|
||||||
|
&& rm die_${DIE_VERSION}_Ubuntu_24.04_amd64.deb \
|
||||||
|
&& apt-get purge -y wget \
|
||||||
|
&& apt-get autoremove -y \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/diec"]
|
||||||
13
src/main/java/ai/decompile/common/config/JacksonConfig.java
Normal file
13
src/main/java/ai/decompile/common/config/JacksonConfig.java
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package ai.decompile.common.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JacksonConfig {
|
||||||
|
@Bean
|
||||||
|
public ObjectMapper objectMapper() {
|
||||||
|
return new ObjectMapper();
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/java/ai/decompile/die/model/DieResult.java
Normal file
20
src/main/java/ai/decompile/die/model/DieResult.java
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package ai.decompile.die.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record DieResult(@JsonProperty("detects") List<Detect> detects) {
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record Detect(
|
||||||
|
@JsonProperty("filetype") String filetype, @JsonProperty("values") List<Value> values) {}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record Value(
|
||||||
|
@JsonProperty("info") String info,
|
||||||
|
@JsonProperty("string") String string,
|
||||||
|
@JsonProperty("type") String type,
|
||||||
|
@JsonProperty("name") String name) {}
|
||||||
|
}
|
||||||
124
src/main/java/ai/decompile/die/model/DieResultParser.java
Normal file
124
src/main/java/ai/decompile/die/model/DieResultParser.java
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package ai.decompile.die.model;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class DieResultParser {
|
||||||
|
private static final Pattern COMPILER_PATTERN =
|
||||||
|
Pattern.compile("Compiler:\\s*(.+)", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern OS_PATTERN =
|
||||||
|
Pattern.compile("Operation system:\\s*(.+)", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern ARCH_BITS_PATTERN = Pattern.compile("\\[([^,\\]]+),\\s*(\\d+)-bit");
|
||||||
|
|
||||||
|
private static final Map<String, String> FILETYPE_ARCH_HINTS =
|
||||||
|
Map.ofEntries(
|
||||||
|
Map.entry("NE", "x86, 16-bit"),
|
||||||
|
Map.entry("LE", "x86, 32-bit"),
|
||||||
|
Map.entry("LX", "x86, 32-bit"),
|
||||||
|
Map.entry("PE", "x86, 32-bit"),
|
||||||
|
Map.entry("PE32", "x86, 32-bit"),
|
||||||
|
Map.entry("PE64", "x86-64, 64-bit"),
|
||||||
|
Map.entry("PE32+", "x86-64, 64-bit"),
|
||||||
|
Map.entry("MS-DOS", "x86, 16-bit"));
|
||||||
|
|
||||||
|
public ParsedInfo parse(DieResult result) {
|
||||||
|
if (Objects.isNull(result) || Objects.isNull(result.detects()) || result.detects().isEmpty()) {
|
||||||
|
return new ParsedInfo(null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
String fileType = null;
|
||||||
|
String architecture = null;
|
||||||
|
String compiler = null;
|
||||||
|
|
||||||
|
for (var detect : result.detects()) {
|
||||||
|
if (Objects.isNull(fileType) && StringUtils.isNotBlank(detect.filetype())) {
|
||||||
|
fileType = detect.filetype();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.nonNull(detect.values())) {
|
||||||
|
for (var value : detect.values()) {
|
||||||
|
var text = firstNonBlank(value.string(), value.info(), value.name());
|
||||||
|
|
||||||
|
if (Objects.isNull(compiler)) {
|
||||||
|
compiler = tryExtractCompiler(value, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.isNull(architecture)) {
|
||||||
|
architecture = tryExtractArchitecture(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.nonNull(fileType) && Objects.nonNull(architecture) && Objects.nonNull(compiler)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.isNull(architecture) && Objects.nonNull(fileType)) {
|
||||||
|
architecture = FILETYPE_ARCH_HINTS.get(fileType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ParsedInfo(fileType, architecture, compiler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String tryExtractCompiler(DieResult.Value value, String text) {
|
||||||
|
if (Objects.isNull(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matched = extractFirstMatch(text, COMPILER_PATTERN);
|
||||||
|
if (Objects.nonNull(matched)) {
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("compiler".equalsIgnoreCase(value.type()) && StringUtils.isNotBlank(value.name())) {
|
||||||
|
var version = StringUtils.isNotBlank(value.info()) ? "[" + value.info() + "]" : "";
|
||||||
|
return value.name() + version;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String tryExtractArchitecture(String text) {
|
||||||
|
if (Objects.isNull(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var osInfo = extractFirstMatch(text, OS_PATTERN);
|
||||||
|
if (Objects.isNull(osInfo)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matcher = ARCH_BITS_PATTERN.matcher(osInfo);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1).trim() + ", " + matcher.group(2) + "-bit";
|
||||||
|
}
|
||||||
|
|
||||||
|
return osInfo.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractFirstMatch(String text, Pattern pattern) {
|
||||||
|
var matcher = pattern.matcher(text);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstNonBlank(String... values) {
|
||||||
|
for (var v : values) {
|
||||||
|
if (StringUtils.isNotBlank(v)) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ParsedInfo(String fileType, String architecture, String compiler) {}
|
||||||
|
}
|
||||||
2
src/main/java/ai/decompile/die/model/package-info.java
Normal file
2
src/main/java/ai/decompile/die/model/package-info.java
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@org.springframework.modulith.NamedInterface("model")
|
||||||
|
package ai.decompile.die.model;
|
||||||
4
src/main/java/ai/decompile/die/package-info.java
Normal file
4
src/main/java/ai/decompile/die/package-info.java
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
@org.springframework.modulith.ApplicationModule(
|
||||||
|
displayName = "DiE",
|
||||||
|
allowedDependencies = {"docker", "common"})
|
||||||
|
package ai.decompile.die;
|
||||||
66
src/main/java/ai/decompile/die/service/DiecService.java
Normal file
66
src/main/java/ai/decompile/die/service/DiecService.java
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package ai.decompile.die.service;
|
||||||
|
|
||||||
|
import ai.decompile.die.model.DieResult;
|
||||||
|
import ai.decompile.docker.service.ContainerService;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.github.dockerjava.api.model.Bind;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(name = "app.docker.enabled", havingValue = "true")
|
||||||
|
public class DiecService {
|
||||||
|
private final ContainerService containers;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Value("${app.die.image}")
|
||||||
|
private String imageName;
|
||||||
|
|
||||||
|
@Value("${app.storage.upload-dir}")
|
||||||
|
private String uploadDir;
|
||||||
|
|
||||||
|
@Value("${app.die.timeout-seconds}")
|
||||||
|
private long timeoutSeconds;
|
||||||
|
|
||||||
|
public DieResult analyze(String filePath) {
|
||||||
|
var absolutePath = Path.of(uploadDir).resolve(filePath).toAbsolutePath().normalize();
|
||||||
|
var filename = absolutePath.getFileName().toString();
|
||||||
|
var hostPath = absolutePath.getParent().toString();
|
||||||
|
|
||||||
|
var dockerfile = new File("Dockerfile.die");
|
||||||
|
containers.ensureImage(imageName, dockerfile);
|
||||||
|
|
||||||
|
var bind = Bind.parse(hostPath + ":/input:ro");
|
||||||
|
|
||||||
|
var containerId =
|
||||||
|
containers.createContainer(imageName, new String[] {"--json", "/input/" + filename}, bind);
|
||||||
|
|
||||||
|
try {
|
||||||
|
containers.startContainer(containerId);
|
||||||
|
|
||||||
|
var exitCode = containers.waitForExit(containerId, timeoutSeconds);
|
||||||
|
var stdout = containers.captureLogs(containerId, 2);
|
||||||
|
|
||||||
|
if (exitCode != 0) {
|
||||||
|
log.error("DiE container exited with code {}: {}", exitCode, stdout);
|
||||||
|
throw new RuntimeException(
|
||||||
|
"DiE container exited with code " + exitCode + ". Output: " + stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectMapper.readValue(stdout, DieResult.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("DiE analysis failed for file {}: {}", filePath, e.getMessage());
|
||||||
|
throw new RuntimeException("DiE analysis failed: " + e.getMessage(), e);
|
||||||
|
} finally {
|
||||||
|
containers.stopAndRemove(containerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/main/java/ai/decompile/die/service/package-info.java
Normal file
2
src/main/java/ai/decompile/die/service/package-info.java
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@org.springframework.modulith.NamedInterface("service")
|
||||||
|
package ai.decompile.die.service;
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package ai.decompile.docker.config;
|
||||||
|
|
||||||
|
import com.github.dockerjava.api.DockerClient;
|
||||||
|
import com.github.dockerjava.core.DockerClientBuilder;
|
||||||
|
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
|
||||||
|
import java.net.URI;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnProperty(name = "app.docker.enabled", havingValue = "true", matchIfMissing = true)
|
||||||
|
public class DockerClientConfig {
|
||||||
|
@Bean
|
||||||
|
public DockerClient dockerClient(@Value("${app.die.docker-host}") String dockerHost) {
|
||||||
|
var httpClient =
|
||||||
|
new ApacheDockerHttpClient.Builder().dockerHost(URI.create(dockerHost)).build();
|
||||||
|
|
||||||
|
return DockerClientBuilder.getInstance().withDockerHttpClient(httpClient).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/main/java/ai/decompile/docker/package-info.java
Normal file
3
src/main/java/ai/decompile/docker/package-info.java
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@org.springframework.modulith.ApplicationModule(
|
||||||
|
type = org.springframework.modulith.ApplicationModule.Type.OPEN)
|
||||||
|
package ai.decompile.docker;
|
||||||
155
src/main/java/ai/decompile/docker/service/ContainerService.java
Normal file
155
src/main/java/ai/decompile/docker/service/ContainerService.java
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package ai.decompile.docker.service;
|
||||||
|
|
||||||
|
import com.github.dockerjava.api.DockerClient;
|
||||||
|
import com.github.dockerjava.api.async.ResultCallback;
|
||||||
|
import com.github.dockerjava.api.command.BuildImageResultCallback;
|
||||||
|
import com.github.dockerjava.api.command.WaitContainerResultCallback;
|
||||||
|
import com.github.dockerjava.api.exception.NotFoundException;
|
||||||
|
import com.github.dockerjava.api.model.Bind;
|
||||||
|
import com.github.dockerjava.api.model.BuildResponseItem;
|
||||||
|
import com.github.dockerjava.api.model.Frame;
|
||||||
|
import com.github.dockerjava.api.model.HostConfig;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(name = "app.docker.enabled", havingValue = "true")
|
||||||
|
public class ContainerService {
|
||||||
|
private final DockerClient dockerClient;
|
||||||
|
|
||||||
|
public void ensureImage(String imageTag, File dockerfile) {
|
||||||
|
try {
|
||||||
|
dockerClient.inspectImageCmd(imageTag).exec();
|
||||||
|
log.debug("Image '{}' found locally", imageTag);
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
log.info("Image '{}' not found, building from {}", imageTag, dockerfile.getName());
|
||||||
|
buildImage(imageTag, dockerfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createContainer(String image, String[] cmd, Bind... binds) {
|
||||||
|
return dockerClient
|
||||||
|
.createContainerCmd(image)
|
||||||
|
.withCmd(cmd)
|
||||||
|
.withHostConfig(HostConfig.newHostConfig().withBinds(binds))
|
||||||
|
.exec()
|
||||||
|
.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startContainer(String containerId) {
|
||||||
|
dockerClient.startContainerCmd(containerId).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int waitForExit(String containerId, long timeoutSeconds) {
|
||||||
|
return dockerClient
|
||||||
|
.waitContainerCmd(containerId)
|
||||||
|
.exec(new WaitContainerResultCallback())
|
||||||
|
.awaitStatusCode(timeoutSeconds, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String captureLogs(String containerId, long timeoutSeconds) {
|
||||||
|
var collector = new FrameCollector();
|
||||||
|
try (collector) {
|
||||||
|
dockerClient
|
||||||
|
.logContainerCmd(containerId)
|
||||||
|
.withStdOut(true)
|
||||||
|
.withStdErr(true)
|
||||||
|
.withFollowStream(true)
|
||||||
|
.exec(collector);
|
||||||
|
|
||||||
|
collector.awaitCompletion(timeoutSeconds, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to capture container logs for {}: {}", containerId, e.getMessage());
|
||||||
|
}
|
||||||
|
return collector.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopAndRemove(String containerId) {
|
||||||
|
try {
|
||||||
|
dockerClient.stopContainerCmd(containerId).withTimeout(10).exec();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug(
|
||||||
|
"Failed to stop container {} (may already be stopped): {}", containerId, e.getMessage());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
dockerClient.removeContainerCmd(containerId).withForce(true).exec();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug(
|
||||||
|
"Failed to remove container {} (may already be removed): {}",
|
||||||
|
containerId,
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildImage(String imageTag, File dockerfile) {
|
||||||
|
var absoluteDockerfile = dockerfile.getAbsoluteFile();
|
||||||
|
if (!absoluteDockerfile.exists()) {
|
||||||
|
throw new RuntimeException("Dockerfile not found at " + absoluteDockerfile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> tags = new HashSet<>();
|
||||||
|
tags.add(imageTag);
|
||||||
|
|
||||||
|
var callback =
|
||||||
|
dockerClient
|
||||||
|
.buildImageCmd(absoluteDockerfile)
|
||||||
|
.withTags(tags)
|
||||||
|
.exec(
|
||||||
|
new BuildImageResultCallback() {
|
||||||
|
@Override
|
||||||
|
public void onNext(BuildResponseItem item) {
|
||||||
|
if (Objects.nonNull(item.getStream())) {
|
||||||
|
log.debug("Build: {}", item.getStream().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.isErrorIndicated() && Objects.nonNull(item.getErrorDetail())) {
|
||||||
|
log.error("Build error: {}", item.getErrorDetail().getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onNext(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
callback.awaitCompletion();
|
||||||
|
log.info("Image '{}' built successfully", imageTag);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new RuntimeException("Interrupted while building image " + imageTag, e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to build image " + imageTag + ": " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FrameCollector extends ResultCallback.Adapter<Frame> implements Closeable {
|
||||||
|
private final StringBuilder buffer = new StringBuilder();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(Frame frame) {
|
||||||
|
buffer.append(new String(frame.getPayload(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,11 @@
|
|||||||
@org.springframework.modulith.ApplicationModule(
|
@org.springframework.modulith.ApplicationModule(
|
||||||
displayName = "Job",
|
displayName = "Job",
|
||||||
allowedDependencies = {"workspace::events", "workspace::entities", "common"})
|
allowedDependencies = {
|
||||||
|
"workspace::events",
|
||||||
|
"workspace::entities",
|
||||||
|
"workspace::services",
|
||||||
|
"die::service",
|
||||||
|
"die::model",
|
||||||
|
"common"
|
||||||
|
})
|
||||||
package ai.decompile.job;
|
package ai.decompile.job;
|
||||||
|
|||||||
@ -1,14 +1,28 @@
|
|||||||
package ai.decompile.job.service.handler;
|
package ai.decompile.job.service.handler;
|
||||||
|
|
||||||
|
import ai.decompile.die.model.DieResultParser;
|
||||||
|
import ai.decompile.die.service.DiecService;
|
||||||
import ai.decompile.job.model.entity.Job;
|
import ai.decompile.job.model.entity.Job;
|
||||||
import ai.decompile.job.model.enums.JobStatus;
|
import ai.decompile.job.model.enums.JobStatus;
|
||||||
import ai.decompile.job.model.enums.JobType;
|
import ai.decompile.job.model.enums.JobType;
|
||||||
|
import ai.decompile.workspace.service.BinaryService;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Log4j2
|
@Log4j2
|
||||||
@Component
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(name = "app.docker.enabled", havingValue = "true")
|
||||||
public class AnalyzeFileHandler implements JobHandler {
|
public class AnalyzeFileHandler implements JobHandler {
|
||||||
|
private final DiecService diecService;
|
||||||
|
private final DieResultParser dieResultParser;
|
||||||
|
private final BinaryService binaryService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void process(Job job) {
|
public void process(Job job) {
|
||||||
log.info(
|
log.info(
|
||||||
@ -17,13 +31,35 @@ public class AnalyzeFileHandler implements JobHandler {
|
|||||||
job.getBinaryId(),
|
job.getBinaryId(),
|
||||||
job.getProjectId());
|
job.getProjectId());
|
||||||
|
|
||||||
|
var filePath = binaryService.getBinaryFilePath(job.getBinaryId());
|
||||||
|
var result = diecService.analyze(filePath);
|
||||||
|
var parsed = dieResultParser.parse(result);
|
||||||
|
|
||||||
|
binaryService.updateAnalysisInfo(
|
||||||
|
job.getBinaryId(), parsed.fileType(), parsed.architecture(), parsed.compiler());
|
||||||
|
|
||||||
|
job.setResult(toJson(Map.of("raw", result, "info", parsed)));
|
||||||
job.setStatus(JobStatus.COMPLETED);
|
job.setStatus(JobStatus.COMPLETED);
|
||||||
|
|
||||||
log.info("Job {} completed", job.getId());
|
log.info(
|
||||||
|
"Job {} completed. fileType={}, arch={}, compiler={}",
|
||||||
|
job.getId(),
|
||||||
|
parsed.fileType(),
|
||||||
|
parsed.architecture(),
|
||||||
|
parsed.compiler());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JobType supportedType() {
|
public JobType supportedType() {
|
||||||
return JobType.ANALYZE_FILE;
|
return JobType.ANALYZE_FILE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String toJson(Object obj) {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(obj);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to serialize result to JSON", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ public record BinaryResponse(
|
|||||||
long fileSize,
|
long fileSize,
|
||||||
String format,
|
String format,
|
||||||
String architecture,
|
String architecture,
|
||||||
|
String compiler,
|
||||||
@NotBlank String filePath,
|
@NotBlank String filePath,
|
||||||
@NotNull Instant updatedAt) {
|
@NotNull Instant updatedAt) {
|
||||||
public static BinaryResponse from(Binary binary) {
|
public static BinaryResponse from(Binary binary) {
|
||||||
@ -23,6 +24,7 @@ public record BinaryResponse(
|
|||||||
binary.getFileSize(),
|
binary.getFileSize(),
|
||||||
binary.getFormat(),
|
binary.getFormat(),
|
||||||
binary.getArchitecture(),
|
binary.getArchitecture(),
|
||||||
|
binary.getCompiler(),
|
||||||
binary.getFilePath(),
|
binary.getFilePath(),
|
||||||
binary.getUpdatedAt());
|
binary.getUpdatedAt());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,12 +25,15 @@ public class Binary {
|
|||||||
@Column(name = "file_size", nullable = false)
|
@Column(name = "file_size", nullable = false)
|
||||||
private long fileSize;
|
private long fileSize;
|
||||||
|
|
||||||
@Column(length = 20)
|
@Column(length = 30)
|
||||||
private String format;
|
private String format;
|
||||||
|
|
||||||
@Column(length = 20)
|
@Column(length = 30)
|
||||||
private String architecture;
|
private String architecture;
|
||||||
|
|
||||||
|
@Column(length = 100)
|
||||||
|
private String compiler;
|
||||||
|
|
||||||
@Column(name = "file_path", nullable = false, length = 500)
|
@Column(name = "file_path", nullable = false, length = 500)
|
||||||
private String filePath;
|
private String filePath;
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,16 @@ public class BinaryService {
|
|||||||
return BinaryResponse.from(binary);
|
return BinaryResponse.from(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateAnalysisInfo(
|
||||||
|
UUID binaryId, String format, String architecture, String compiler) {
|
||||||
|
var binary = getBinary(binaryId);
|
||||||
|
binary.setFormat(format);
|
||||||
|
binary.setArchitecture(architecture);
|
||||||
|
binary.setCompiler(compiler);
|
||||||
|
binaryRepository.save(binary);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteBinary(UUID binaryId) {
|
public void deleteBinary(UUID binaryId) {
|
||||||
var binary = getBinary(binaryId);
|
var binary = getBinary(binaryId);
|
||||||
@ -90,6 +100,10 @@ public class BinaryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getBinaryFilePath(UUID binaryId) {
|
||||||
|
return getBinary(binaryId).getFilePath();
|
||||||
|
}
|
||||||
|
|
||||||
private Binary getBinary(UUID binaryId) {
|
private Binary getBinary(UUID binaryId) {
|
||||||
return binaryRepository
|
return binaryRepository
|
||||||
.findById(binaryId)
|
.findById(binaryId)
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
@org.springframework.modulith.NamedInterface("services")
|
||||||
|
package ai.decompile.workspace.service;
|
||||||
@ -1,4 +1,7 @@
|
|||||||
spring:
|
spring:
|
||||||
|
threads:
|
||||||
|
virtual:
|
||||||
|
enabled: true
|
||||||
application:
|
application:
|
||||||
name: decompile-ai
|
name: decompile-ai
|
||||||
datasource:
|
datasource:
|
||||||
@ -27,6 +30,12 @@ app:
|
|||||||
exchange: decompile.jobs
|
exchange: decompile.jobs
|
||||||
queue: decompile.jobs.queue
|
queue: decompile.jobs.queue
|
||||||
routing-key: decompile.jobs.created
|
routing-key: decompile.jobs.created
|
||||||
|
docker:
|
||||||
|
enabled: true
|
||||||
|
die:
|
||||||
|
image: decompile-ai/diec:latest
|
||||||
|
timeout-seconds: 60
|
||||||
|
docker-host: unix:///var/run/docker.sock
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
|
|||||||
@ -4,8 +4,9 @@ CREATE TABLE binaries
|
|||||||
project_id UUID NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
project_id UUID NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||||
filename VARCHAR(255) NOT NULL,
|
filename VARCHAR(255) NOT NULL,
|
||||||
file_size BIGINT NOT NULL,
|
file_size BIGINT NOT NULL,
|
||||||
format VARCHAR(20),
|
format VARCHAR(30),
|
||||||
architecture VARCHAR(20),
|
architecture VARCHAR(30),
|
||||||
|
compiler VARCHAR(100),
|
||||||
file_path VARCHAR(500) NOT NULL,
|
file_path VARCHAR(500) NOT NULL,
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
|||||||
@ -2,8 +2,10 @@ package ai.decompile.decompile_ai;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
class DecompileAiApplicationTests {
|
class DecompileAiApplicationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -37,7 +37,15 @@ class BinaryControllerTest {
|
|||||||
var binaryId = UUID.randomUUID();
|
var binaryId = UUID.randomUUID();
|
||||||
var response =
|
var response =
|
||||||
new BinaryResponse(
|
new BinaryResponse(
|
||||||
binaryId, projectId, "test.exe", 1024L, null, null, "abc_test.exe", Instant.now());
|
binaryId,
|
||||||
|
projectId,
|
||||||
|
"test.exe",
|
||||||
|
1024L,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"abc_test.exe",
|
||||||
|
Instant.now());
|
||||||
when(binaryService.getBinaries(eq(projectId), isNull())).thenReturn(List.of(response));
|
when(binaryService.getBinaries(eq(projectId), isNull())).thenReturn(List.of(response));
|
||||||
|
|
||||||
mockMvc
|
mockMvc
|
||||||
@ -72,6 +80,7 @@ class BinaryControllerTest {
|
|||||||
512L,
|
512L,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
"abc_filtered.exe",
|
"abc_filtered.exe",
|
||||||
Instant.now());
|
Instant.now());
|
||||||
when(binaryService.getBinaries(eq(projectId), eq("filter"))).thenReturn(List.of(response));
|
when(binaryService.getBinaries(eq(projectId), eq("filter"))).thenReturn(List.of(response));
|
||||||
@ -102,7 +111,7 @@ class BinaryControllerTest {
|
|||||||
new MockMultipartFile("file", "test.exe", "application/octet-stream", new byte[] {1, 2, 3});
|
new MockMultipartFile("file", "test.exe", "application/octet-stream", new byte[] {1, 2, 3});
|
||||||
var response =
|
var response =
|
||||||
new BinaryResponse(
|
new BinaryResponse(
|
||||||
binaryId, projectId, "test.exe", 3L, null, null, "abc_test.exe", Instant.now());
|
binaryId, projectId, "test.exe", 3L, null, null, null, "abc_test.exe", Instant.now());
|
||||||
when(binaryService.uploadBinary(eq(projectId), any())).thenReturn(response);
|
when(binaryService.uploadBinary(eq(projectId), any())).thenReturn(response);
|
||||||
|
|
||||||
mockMvc
|
mockMvc
|
||||||
|
|||||||
@ -25,3 +25,5 @@ app:
|
|||||||
exchange: decompile.jobs.test
|
exchange: decompile.jobs.test
|
||||||
queue: decompile.jobs.test.queue
|
queue: decompile.jobs.test.queue
|
||||||
routing-key: decompile.jobs.test.created
|
routing-key: decompile.jobs.test.created
|
||||||
|
docker:
|
||||||
|
enabled: false
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user