Add utility classes for Ghidra

Bug: 276748671
Test: Build CTS, run a test using GhidraUtils class and observe behavior.

Change-Id: Idbfe357559b303d113be343c164ad8d658708719
Merged-In: Idbfe357559b303d113be343c164ad8d658708719
diff --git a/libraries/sts-common-util/host-side/Android.bp b/libraries/sts-common-util/host-side/Android.bp
index 495caf3..aa9689f 100644
--- a/libraries/sts-common-util/host-side/Android.bp
+++ b/libraries/sts-common-util/host-side/Android.bp
@@ -18,6 +18,7 @@
 
 java_library_host {
     name: "sts-host-util",
+    java_resources: [":sts_host_util_java_resources"],
     defaults: ["cts_error_prone_rules"],
 
     srcs: [
@@ -40,6 +41,13 @@
     ],
 }
 
+// Workaround: java_resource_dirs ignores *.java files
+filegroup {
+    name: "sts_host_util_java_resources",
+    path: "res/",
+    srcs: ["res/**/*.java"],
+}
+
 java_library_host {
     name: "sts-libtombstone_proto-java",
     visibility: [
diff --git a/libraries/sts-common-util/host-side/res/ghidra-scripts/FunctionOffsetPostScript.java b/libraries/sts-common-util/host-side/res/ghidra-scripts/FunctionOffsetPostScript.java
new file mode 100644
index 0000000..8aa5649
--- /dev/null
+++ b/libraries/sts-common-util/host-side/res/ghidra-scripts/FunctionOffsetPostScript.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ghidra.app.script.GhidraScript;
+import ghidra.program.model.listing.Function;
+import ghidra.program.model.listing.FunctionIterator;
+
+import java.io.ObjectOutputStream;
+import java.math.BigInteger;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class FunctionOffsetPostScript extends GhidraScript {
+
+    public void run() throws Exception {
+        String spaceSeparatedFunctionNames = propertiesFileParams.getValue("functionNames");
+        List<String> listOfFunctions = Arrays.asList(spaceSeparatedFunctionNames.split("\\s+"));
+        List<BigInteger> output = new ArrayList<>();
+
+        // Find the function offsets
+        for (String function : listOfFunctions) {
+            FunctionIterator functionIterator = currentProgram.getListing().getFunctions(true);
+            BigInteger offset = null;
+            while (functionIterator.hasNext()) {
+                Function nextFunction = functionIterator.next();
+                if (!nextFunction.getName().equals(function)) {
+                    continue; // Skip to the next iteration if the function name doesn't match
+                }
+
+                // If the function name matches, calculate the offset
+                offset =
+                        nextFunction
+                                .getEntryPoint()
+                                .subtract(currentProgram.getImageBase().getOffset())
+                                .getOffsetAsBigInteger();
+                break;
+            }
+
+            // 'output' is appended in the same order as 'listOfFunctions' contains the function
+            // names. If an offset is not found, null is appended.
+            output.add(offset);
+        }
+        try (Socket socket =
+                        new Socket(
+                                "localhost",
+                                Integer.parseInt(propertiesFileParams.getValue("port")));
+                ObjectOutputStream outputStream =
+                        new ObjectOutputStream(socket.getOutputStream()); ) {
+            outputStream.writeObject(output);
+        }
+    }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraFunctionOffsets.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraFunctionOffsets.java
new file mode 100644
index 0000000..a48b4eb
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraFunctionOffsets.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common;
+
+import static com.android.sts.common.GhidraScriptRunner.POST_SCRIPT_TIMEOUT;
+
+import static java.util.stream.Collectors.joining;
+
+import com.android.tradefed.device.ITestDevice;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.ObjectInputStream;
+import java.math.BigInteger;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A utility class containing constants and functions for a Ghidra post-script (located at
+ * res/ghidra-scripts/FunctionOffsetPostScript.java) used to get function offsets
+ */
+public class GhidraFunctionOffsets {
+    public static final String POST_SCRIPT_CLASS_NAME = "FunctionOffsetPostScript";
+    public static final String POST_SCRIPT_CONTENT_RESOURCE_PATH =
+            "ghidra-scripts/" + POST_SCRIPT_CLASS_NAME + ".java";
+
+    /**
+     * Converts the output of {@code getFunctionOffsets} to a string with space separated offsets.
+     *
+     * @param device The ITestDevice to pull the binary from.
+     * @param binaryName The name of the binary file present in device.
+     * @param binaryPath The path to the binary file in device .
+     * @param callingClass The name of the calling class.
+     * @param functions The list of function names.
+     * @return A string containing space separated function offsets.
+     */
+    public static String getFunctionOffsetsAsCmdLineArgs(
+            ITestDevice device,
+            String binaryName,
+            String binaryPath,
+            String callingClass,
+            List<String> functions,
+            String analyzeHeadlessPath)
+            throws Exception {
+        return String.join(
+                " ",
+                getFunctionOffsets(
+                                device,
+                                binaryName,
+                                binaryPath,
+                                callingClass,
+                                functions,
+                                analyzeHeadlessPath)
+                        .stream()
+                        .map(BigInteger::toString)
+                        .toArray(String[]::new));
+    }
+
+    /**
+     * Retrieves the function offsets from the given binary.
+     *
+     * @param device The ITestDevice to pull the binary from.
+     * @param binaryName The name of the binary file present in device.
+     * @param binaryPath The path to the binary file in device .
+     * @param callingClass The name of the calling class.
+     * @param functions The list of function names.
+     * @return A list of BigIntegers containing function offsets in the same order as @param
+     *     functions.
+     */
+    public static List<BigInteger> getFunctionOffsets(
+            ITestDevice device,
+            String binaryName,
+            String binaryPath,
+            String callingClass,
+            List<String> functions,
+            String analyzeHeadlessPath)
+            throws Exception {
+        // Set up a server socket to listen for output from post script
+        final Map<String, List<BigInteger>> mapOfResults = new HashMap<String, List<BigInteger>>();
+        final Semaphore outputReceived = new Semaphore(0);
+        final ServerSocket serverSocket = new ServerSocket(0);
+        serverSocket.setReuseAddress(true /* on */);
+        serverSocket.setSoTimeout((int) POST_SCRIPT_TIMEOUT);
+        int port = serverSocket.getLocalPort();
+        Thread clientThread =
+                new Thread(
+                        () -> {
+                            try (Socket clientSocket =
+                                            serverSocket.accept(); // blocks till connected
+                                    ObjectInputStream inputStream =
+                                            new ObjectInputStream(
+                                                    clientSocket.getInputStream()); ) {
+                                mapOfResults.put(
+                                        callingClass, (List<BigInteger>) inputStream.readObject());
+                            } catch (Exception e) {
+                                mapOfResults.put(callingClass, null);
+                            } finally {
+                                outputReceived.release();
+                            }
+                        });
+
+        GhidraScriptRunner ghidraScriptRunner =
+                new GhidraScriptRunner(
+                                device,
+                                binaryName,
+                                binaryPath,
+                                callingClass,
+                                POST_SCRIPT_CLASS_NAME + ".properties",
+                                null,
+                                POST_SCRIPT_CLASS_NAME + ".java",
+                                analyzeHeadlessPath)
+                        .postScript(
+                                readResource(POST_SCRIPT_CONTENT_RESOURCE_PATH),
+                                new HashMap<String, String>(
+                                        ImmutableMap.of(
+                                                "functionNames",
+                                                String.join(" ", functions),
+                                                "port",
+                                                String.valueOf(port))));
+        try (AutoCloseable ghidraScriptRunnerAc = ghidraScriptRunner.run()) {
+            clientThread.start();
+            if (!outputReceived.tryAcquire(POST_SCRIPT_TIMEOUT, TimeUnit.MILLISECONDS)) {
+                throw new TimeoutException(
+                        "FunctionOffsetPostScript timed out. Output of Ghidra: "
+                                + ghidraScriptRunner.getOutputStream().toString("UTF-8"));
+            }
+            return mapOfResults.get(callingClass);
+        }
+    }
+
+    private static String readResource(String fullResourceName) throws Exception {
+        try (InputStream in =
+                GhidraFunctionOffsets.class
+                        .getClassLoader()
+                        .getResourceAsStream(fullResourceName)) {
+            return new BufferedReader(new InputStreamReader(in))
+                    .lines()
+                    .collect(joining(System.lineSeparator()));
+        }
+    }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraPreparer.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraPreparer.java
new file mode 100644
index 0000000..1faf062
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraPreparer.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common;
+
+import static com.android.tradefed.util.FileUtil.chmodRWXRecursively;
+import static com.android.tradefed.util.FileUtil.createNamedTempDir;
+import static com.android.tradefed.util.FileUtil.recursiveDelete;
+import static com.android.tradefed.util.ZipUtil.extractZip;
+
+import com.android.sts.common.GitHubUtils.GitHubRepo;
+import com.android.sts.common.util.GhidraBusinessLogicHandler;
+import com.android.tradefed.build.BuildRetrievalError;
+import com.android.tradefed.build.FileDownloadCache;
+import com.android.tradefed.build.FileDownloadCacheFactory;
+import com.android.tradefed.build.IFileDownloader;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.targetprep.BaseTargetPreparer;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.net.HttpHelper;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Stream;
+import java.util.zip.ZipFile;
+
+/** Preparer to download Ghidra, unzip it and set the path to the executable */
+@OptionClass(alias = "ghidra-preparer")
+public class GhidraPreparer extends BaseTargetPreparer {
+    private static final Object LOCK_SETUP = new Object(); // Locks the setup
+    private static final String GHIDRA_REPO_OWNER = "NationalSecurityAgency";
+    private static final String GHIDRA_REPO_NAME = "ghidra";
+    public static final String PROPERTY_KEY = "ghidra_analyze_headless";
+    private File mGhidraZipDir = null; // Stores the ghidra zip directory
+    private File mGhidraZipFile = null; // Stores the ghidra zip file
+    private File mCacheDir = null; // Refers to the ghidra cache directory
+    private URI mGhidraZipUri = null; // Stores the url to download ghidra zip
+    private String mPreviousPropertyVal = null;
+
+    @Option(
+            name = "ghidra-tag",
+            description = "Overrides 'gitReleaseTagName' of GhidraBusinessLogicHandler.")
+    private String mTagName = null;
+
+    @Option(
+            name = "ghidra-asset",
+            description = "Overrides 'gitReleaseAssetName' of GhidraBusinessLogicHandler.")
+    private String mAssetName = null;
+
+    /** {@inheritDoc} */
+    @Override
+    public void setUp(TestInformation testInformation)
+            throws DeviceNotAvailableException, BuildError, TargetSetupError {
+        synchronized (LOCK_SETUP) {
+            try {
+                // Fetch value of property 'ghidra_analyze_headless'
+                mPreviousPropertyVal = testInformation.properties().get(PROPERTY_KEY);
+
+                // Fetch the ghidra zip name and url to download ghidra
+                Map.Entry<String, URI> assetNameToUri = getZipNameAndUri();
+                String ghidraZipName = assetNameToUri.getKey();
+                mGhidraZipUri = assetNameToUri.getValue();
+
+                // Create required directories, download ghidra and extract the zip at
+                // /tmp/tradefed_ghidra/<mGhidraZipDir>/<ghidra_zip_here>
+                File ghidraDir = Paths.get("tradefed_ghidra", ghidraZipName).toFile();
+                mGhidraZipDir = createNamedTempDir(ghidraDir.getPath());
+                mCacheDir = createNamedTempDir("ghidra_cache");
+
+                // If 'analyzeHeadless' already exists add path to properties and return.
+                String analyzeHeadlessPath = getAnalyzeHeadlessPath();
+                if (analyzeHeadlessPath != null) {
+                    testInformation.properties().put(PROPERTY_KEY, analyzeHeadlessPath);
+                    return;
+                }
+
+                // Download and extract ghidra zip
+                downloadAndExtractGhidra();
+
+                // Add path of 'analyzeHeadless' to properties
+                analyzeHeadlessPath = getAnalyzeHeadlessPath();
+                if (analyzeHeadlessPath != null) {
+                    testInformation.properties().put(PROPERTY_KEY, analyzeHeadlessPath);
+                } else {
+                    throw new TargetSetupError(
+                            String.format(
+                                    "Failed to fetch 'analyzeHeadless' location. Ghidra"
+                                        + " 'analyzeHeadless' is not found. Please download ghidra"
+                                        + " manually at /tmp/tradefed_ghidra/%s/ from %s",
+                                    assetNameToUri.getKey() /* Ghidra zip name */,
+                                    assetNameToUri.getValue() /* Download link */));
+                }
+            } catch (Exception e) {
+                // Remove the ghidra directory for the current version
+                if (mGhidraZipDir != null) {
+                    recursiveDelete(mGhidraZipDir);
+                    mGhidraZipDir = null;
+                }
+
+                // Remove the cache directory
+                if (mCacheDir != null) {
+                    recursiveDelete(mCacheDir);
+                    mCacheDir = null;
+                }
+                throw new TargetSetupError(
+                        "Set up failed.", e, null /* deviceDescriptor */, false /* deviceSide */);
+            }
+        }
+    }
+
+    private void downloadAndExtractGhidra() throws Exception {
+        // Download Ghidra zip
+        mGhidraZipFile = new File(mGhidraZipDir, mGhidraZipDir.getName());
+        if (!mGhidraZipFile.exists()) {
+            FileDownloadCache fileDownloadCache =
+                    FileDownloadCacheFactory.getInstance().getCache(mCacheDir);
+            fileDownloadCache.fetchRemoteFile(
+                    new GhidraFileDownloader(), mGhidraZipUri.toString(), mGhidraZipFile);
+        }
+
+        // Unzip Ghidra zip and delete the zip
+        extractZip(new ZipFile(mGhidraZipFile), mGhidraZipDir);
+        recursiveDelete(mGhidraZipFile);
+
+        // Chmod rwx 'mGhidraZipDir'
+        chmodRWXRecursively(mGhidraZipDir);
+    }
+
+    @Override
+    public void tearDown(TestInformation testInformation, Throwable e) {
+        // Restore the previous property value
+        if (mPreviousPropertyVal != null) {
+            testInformation.properties().put(PROPERTY_KEY, mPreviousPropertyVal);
+        } else {
+            testInformation.properties().remove(PROPERTY_KEY);
+        }
+    }
+
+    /** Fetch the first entry from the map of assetName to URI */
+    private Map.Entry<String, URI> getZipNameAndUri() throws IOException, URISyntaxException {
+        Optional<String> ghidraReleaseTagName = GhidraBusinessLogicHandler.getGitReleaseTagName();
+        Optional<String> ghidraReleaseAssetName = GhidraBusinessLogicHandler.getReleaseAssetName();
+        if (mTagName != null) {
+            ghidraReleaseTagName = Optional.of(mTagName);
+        }
+        if (mAssetName != null) {
+            ghidraReleaseAssetName = Optional.of(mAssetName);
+        }
+
+        // Fetch the assetName to uri map
+        Map<String, URI> mapOfAssetNameToUris =
+                new GitHubRepo(GHIDRA_REPO_OWNER, GHIDRA_REPO_NAME)
+                        .getReleaseAssetUris(ghidraReleaseTagName);
+
+        // Get map entry corresponding to 'ghidraReleaseAssetName'
+        if (ghidraReleaseAssetName.isPresent()) {
+            String assetName = ghidraReleaseAssetName.get();
+            URI assetUri = mapOfAssetNameToUris.get(assetName);
+            if (assetUri == null) {
+                throw new IllegalStateException(
+                        "The asset name:" + assetName + " was not found in ghidra release.");
+            }
+            return new AbstractMap.SimpleEntry(assetName, assetUri);
+        }
+
+        // Throw if more than one entry was found in the map
+        if (mapOfAssetNameToUris.size() != 1) {
+            throw new IllegalStateException(
+                    "More than one entries found in 'mapOfAssetNameToUris'. Entries: "
+                            + mapOfAssetNameToUris.toString());
+        }
+
+        // Return the first entry from the map
+        return mapOfAssetNameToUris.entrySet().iterator().next();
+    }
+
+    /** If found returns the path to 'analyzeHeadless' else returns null. */
+    private String getAnalyzeHeadlessPath() {
+        Optional<Path> pathToAnalyzeHeadless = Optional.empty();
+        try (Stream<Path> walkStream = Files.walk(mGhidraZipDir.toPath())) {
+            pathToAnalyzeHeadless =
+                    walkStream
+                            .filter(path -> path.toFile().isFile())
+                            .filter(path -> path.toString().endsWith("/analyzeHeadless"))
+                            .findFirst();
+        } catch (Exception e) {
+            // Ignore exceptions
+        }
+        return pathToAnalyzeHeadless.isPresent() ? pathToAnalyzeHeadless.get().toString() : null;
+    }
+
+    private static class GhidraFileDownloader implements IFileDownloader {
+
+        /** {@inheritDoc} */
+        @Override
+        public File downloadFile(String remoteFilePath) throws BuildRetrievalError {
+            throw new UnsupportedOperationException();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void downloadFile(String relativeRemotePath, File destFile)
+                throws BuildRetrievalError {
+            try {
+                // Download ghidra zip
+                new HttpHelper().doGet(relativeRemotePath, new FileOutputStream(destFile));
+            } catch (Exception e) {
+                throw new BuildRetrievalError("Downloading ghidra zip failed.", e);
+            }
+        }
+    }
+}
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraScriptRunner.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraScriptRunner.java
new file mode 100644
index 0000000..6911e9b
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/ghidra/GhidraScriptRunner.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common;
+
+import static com.android.sts.common.CommandUtil.runAndCheck;
+import static com.android.tradefed.util.FileUtil.chmodRWXRecursively;
+import static com.android.tradefed.util.FileUtil.createNamedTempDir;
+import static com.android.tradefed.util.FileUtil.recursiveDelete;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.RunUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+
+/** class for running Ghidra scripts. */
+public class GhidraScriptRunner {
+    public static final long POST_SCRIPT_TIMEOUT = 90 * 1000L; // 90 seconds
+    private ByteArrayOutputStream mOutputOfGhidra;
+    private ITestDevice mDevice;
+    private String mBinaryName;
+    private String mBinaryPath;
+    private String mAnalyzeHeadlessPath;
+    private Optional<String> mPropertiesFileName = Optional.empty();
+    private Optional<String> mPreScriptFileName = Optional.empty();
+    private Optional<String> mPostScriptFileName = Optional.empty();
+    private Optional<String> mPreScriptContent = Optional.empty();
+    private Optional<String> mPostScriptContent = Optional.empty();
+    private Map<String, String> mPropertiesFileContentsMap = Collections.emptyMap();
+    private String mCallingClass;
+    private boolean mEnableAnalysis = false;
+    private File mPulledLibSaveFolder;
+
+    /**
+     * Constructor for GhidraScriptRunner. When using this constructor from the same class
+     * concurrently, make sure to append a unique suffix to the {@code callingClass} parameter to
+     * avoid conflicts.
+     *
+     * @param device The ITestDevice to pull the binary from.
+     * @param binaryName The name of the binary file present in device.
+     * @param binaryPath The path to the binary file in device .
+     * @param callingClass The name of the calling class.
+     * @param propertiesFileName The file name of properties file associated with post script. eg.
+     *     function_offset_post_script.properties
+     * @param preScriptFileName The file name of pre script file.
+     * @param postScriptFileName The file name of post script file. eg.
+     *     function_offset_post_script.java
+     */
+    public GhidraScriptRunner(
+            ITestDevice device,
+            String binaryName,
+            String binaryPath,
+            String callingClass,
+            String propertiesFileName,
+            String preScriptFileName,
+            String postScriptFileName,
+            String analyzeHeadlessPath) {
+        mDevice = device;
+        mBinaryName = binaryName;
+        mBinaryPath = binaryPath;
+        mCallingClass = callingClass;
+        mAnalyzeHeadlessPath = analyzeHeadlessPath;
+        mPropertiesFileName = Optional.ofNullable(propertiesFileName);
+        mPreScriptFileName = Optional.ofNullable(preScriptFileName);
+        mPostScriptFileName = Optional.ofNullable(postScriptFileName);
+    }
+
+    /**
+     * Set analysis flag during Ghidra script execution.
+     *
+     * @return This GhidraScriptRunner instance with analysis enabled.
+     */
+    public GhidraScriptRunner enableAnalysis() {
+        mEnableAnalysis = true;
+        return this;
+    }
+
+    /**
+     * Return ByteArrayOutputStream.
+     *
+     * @return mOutputOfGhidra.
+     */
+    public ByteArrayOutputStream getOutputStream() {
+        return mOutputOfGhidra;
+    }
+
+    /**
+     * Specify a post-script its properties to be executed after Ghidra analysis.
+     *
+     * @param contents The contents of the post-script.
+     * @param propertiesContent The map of key value pairs to write in properties file
+     * @return This GhidraScriptRunner instance with post-script enabled and configured.
+     */
+    public GhidraScriptRunner postScript(String contents, Map<String, String> propertiesContent) {
+        mPostScriptContent = Optional.ofNullable(contents);
+
+        if (!propertiesContent.isEmpty()) {
+            mPropertiesFileContentsMap = propertiesContent;
+        }
+        return this;
+    }
+
+    /**
+     * Specify a pre-script to be executed before Ghidra analysis.
+     *
+     * @param contents The contents of the pre-script.
+     * @return This GhidraScriptRunner instance with pre-script enabled and configured.
+     */
+    public GhidraScriptRunner preScript(String contents) {
+        mPreScriptContent = Optional.ofNullable(contents);
+        return this;
+    }
+
+    /**
+     * Run Ghidra with the specified options and scripts.
+     *
+     * @return an AutoCloseable for cleaning up temporary files after script execution.
+     */
+    public AutoCloseable run() throws Exception {
+        return runWithTimeout(POST_SCRIPT_TIMEOUT);
+    }
+
+    /**
+     * Run Ghidra with the specified options and scripts, with a timeout.
+     *
+     * @param timeout The timeout value in milliseconds.
+     * @return an AutoCloseable for cleaning up temporary files after script execution.
+     */
+    public AutoCloseable runWithTimeout(long timeout) throws Exception {
+        try {
+            // Get the language using readelf
+            String deviceSerial = mDevice.getSerialNumber().replace(":", "");
+            String pulledLibSaveFolderString = mCallingClass + "_" + deviceSerial + "_files";
+            String language = getLanguage(mDevice, mBinaryPath, mBinaryName);
+            mPulledLibSaveFolder =
+                    createNamedTempDir(
+                            Paths.get(mAnalyzeHeadlessPath).getParent().toFile(),
+                            pulledLibSaveFolderString);
+
+            // Pull binary from the device to the folder
+            if (!mDevice.pullFile(
+                    mBinaryPath + "/" + mBinaryName,
+                    new File(mPulledLibSaveFolder + "/" + mBinaryName))) {
+                throw new Exception(
+                        "Pulling " + mBinaryPath + "/" + mBinaryName + " was not successful");
+            }
+
+            // Create script related files and chmod rwx them
+            if (!mPropertiesFileContentsMap.isEmpty() && mPropertiesFileName.isPresent()) {
+                createPropertiesFile(
+                        mPulledLibSaveFolder,
+                        mPropertiesFileName.get(),
+                        mPropertiesFileContentsMap);
+            }
+            if (mPreScriptContent.isPresent() && mPreScriptFileName.isPresent()) {
+                createScriptFile(
+                        mPulledLibSaveFolder, mPreScriptFileName.get(), mPreScriptContent.get());
+            }
+            if (mPostScriptContent.isPresent() && mPostScriptFileName.isPresent()) {
+                createScriptFile(
+                        mPulledLibSaveFolder, mPostScriptFileName.get(), mPostScriptContent.get());
+            }
+            if (!chmodRWXRecursively(mPulledLibSaveFolder)) {
+                throw new Exception("chmodRWX failed for " + mPulledLibSaveFolder.toString());
+            }
+
+            // Analyze the pulled binary using Ghidra headless analyzer
+            List<String> cmd =
+                    createCommandList(
+                            mAnalyzeHeadlessPath,
+                            mCallingClass,
+                            deviceSerial,
+                            mPulledLibSaveFolder.getPath(),
+                            mBinaryName,
+                            mPreScriptFileName.isPresent() ? mPreScriptFileName.get() : "",
+                            mPostScriptFileName.isPresent() ? mPostScriptFileName.get() : "",
+                            language,
+                            mEnableAnalysis);
+            mOutputOfGhidra = new ByteArrayOutputStream();
+            Process ghidraProcess = RunUtil.getDefault().runCmdInBackground(cmd, mOutputOfGhidra);
+            if (!ghidraProcess.isAlive()) {
+                throw new Exception("Ghidra process died. Output:" + mOutputOfGhidra);
+            }
+
+            if (mOutputOfGhidra.toString("UTF-8").contains("Enter path to JDK home directory")) {
+                throw new Exception(
+                        "JDK 17+ (64-bit) not found in the system PATH. Please add it to your"
+                                + " PATH environment variable.");
+            }
+            return () -> recursiveDelete(mPulledLibSaveFolder);
+        } catch (Exception e) {
+            recursiveDelete(mPulledLibSaveFolder);
+            throw e;
+        }
+    }
+
+    /**
+     * Creates a ghidra script file with the specified content in the given folder.
+     *
+     * @param folder The folder where the script file will be created.
+     * @param fileName The name of the script file.
+     * @param content The content to be written into the script file.
+     */
+    private static void createScriptFile(File folder, String fileName, String content)
+            throws Exception {
+        try (FileWriter fileWriter = new FileWriter(new File(folder, fileName))) {
+            fileWriter.write(content);
+        }
+    }
+
+    /**
+     * Creates a ghidra script properties file with the specified content in the given folder.
+     *
+     * @param folder The folder where the script file will be created.
+     * @param fileName The name of the script file.
+     * @param map The map of key value pairs to be written into the properties file.
+     */
+    private static void createPropertiesFile(File folder, String fileName, Map<String, String> map)
+            throws Exception {
+        File propertiesFile = new File(folder, fileName);
+        try (FileWriter fileWriter = new FileWriter(propertiesFile);
+                FileInputStream fileInputStream = new FileInputStream(propertiesFile)) {
+            propertiesFile.createNewFile();
+
+            Properties properties = new Properties();
+            if (propertiesFile.exists()) {
+                properties.load(fileInputStream);
+            } else {
+                throw new Exception("Unable to create ghidra script properties file");
+            }
+
+            // Populate properties file
+            map.forEach(properties::setProperty);
+            properties.store(fileWriter, "");
+        }
+    }
+
+    /**
+     * Uses the value under 'Machine' from the output of 'readelf -h' command on the specified
+     * binary file to determine the '-processor' parameter (language id) to be passed when invoking
+     * ghidra.
+     *
+     * <p>For e.g. readelf -h <binary_name>
+     *
+     * <p>ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
+     *
+     * <p>[...]
+     *
+     * <p>Machine: arm64 <--- ABI or processor type
+     *
+     * <p>Version: 0x1
+     *
+     * <p>[...]
+     *
+     * <p>The language id can be found in processor-specific .ldefs files located at:
+     * https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Processors/
+     * <proc_name>/data/languages/<proc_name>.ldefs
+     *
+     * <p>where 'proc_name' can be AARCH64, ARM, x86 which are the only ABIs that Android currently
+     * supports as per https://developer.android.com/ndk/guides/abis
+     *
+     * <p>For e.g. Following code snippet from AARCH64.ldefs shows the language 'id' of 'AARCH64'
+     * machine type ie 'arm64'.
+     *
+     * <p><language processor="AARCH64" endian="little" size="64" variant="v8A" version="1.6"
+     * slafile="AARCH64.sla" processorspec="AARCH64.pspec" manualindexfile="../manuals/AARCH64.idx"
+     * id="AARCH64:LE:64:v8A"> <--- 'id' denotes the language id.
+     *
+     * <p>TODO: Utilize ghidra pre script for setting language automatically.
+     *
+     * @param device The ITestDevice representing the testing device.
+     * @param binaryPath The path to the directory containing the binary file.
+     * @param binaryName The name of the binary file.
+     * @return The language of the binary in the format "ARCH:ENDIAN:BITS:VARIANT"
+     */
+    private static String getLanguage(ITestDevice device, String binaryPath, String binaryName)
+            throws Exception {
+        String language =
+                runAndCheck(
+                                device,
+                                "readelf -h " + binaryPath + "/" + binaryName + " | grep Machine")
+                        .getStdout()
+                        .trim()
+                        .split(":\\s*")[1]
+                        .trim();
+        switch (language) {
+            case "arm":
+                return "ARM:LE:32:v8";
+            case "arm64":
+                return "AARCH64:LE:64:v8A";
+            case "386":
+                return "x86:LE:32:default";
+            case "x86-64":
+                return "x86:LE:64:default";
+            case "riscv":
+                return "RISCV:LE:64:RV64GC";
+            default:
+                throw new Exception("Unsupported Machine: " + language);
+        }
+    }
+
+    /**
+     * Creates a list of command-line arguments for invoking Ghidra's analyzeHeadless tool.
+     *
+     * @param ghidraBinaryLocation The analyzerHeadless location.
+     * @param callingClass The name of the calling class.
+     * @param deviceSerialNumber The serial number of the target device.
+     * @param tempFileName The temporary folder name in which target binary is pulled.
+     * @param binaryName The name of the binary file to run analyzeHeadless on.
+     * @param preScriptFileName The file name of the pre script.
+     * @param postScriptFileName The file name of the post script.
+     * @param lang The processor language for analysis. eg ARM:LE:32:v8
+     * @param analysis Flag indicating whether analysis should be performed.
+     * @return A list of command-line arguments for Ghidra analyzeHeadless tool.
+     */
+    private static List<String> createCommandList(
+            String ghidraBinaryLocation,
+            String callingClass,
+            String deviceSerialNumber,
+            String tempFileName,
+            String binaryName,
+            String preScriptFileName,
+            String postScriptFileName,
+            String lang,
+            boolean analysis) {
+        boolean preScript = !preScriptFileName.isEmpty();
+        boolean postScript = !postScriptFileName.isEmpty();
+        return List.of(
+                ghidraBinaryLocation,
+                tempFileName,
+                callingClass + "_ghidra_project_" + deviceSerialNumber,
+                "-import",
+                tempFileName + "/" + binaryName,
+                "-scriptPath",
+                tempFileName,
+                postScript ? "-postScript" : "",
+                postScript ? postScriptFileName : "",
+                preScript ? "-preScript" : "",
+                preScript ? preScriptFileName : "",
+                "-processor",
+                lang,
+                analysis ? "" : "-noanalysis",
+                "-deleteProject");
+    }
+}
diff --git a/libraries/sts-common-util/util/src/com/android/sts/common/util/GhidraBusinessLogicHandler.java b/libraries/sts-common-util/util/src/com/android/sts/common/util/GhidraBusinessLogicHandler.java
new file mode 100644
index 0000000..1b3e0c1
--- /dev/null
+++ b/libraries/sts-common-util/util/src/com/android/sts/common/util/GhidraBusinessLogicHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sts.common.util;
+
+import java.util.Optional;
+
+/** GCL-accessible Business Logic utility for GhidraPreparer */
+public class GhidraBusinessLogicHandler {
+    private static String gitReleaseTagName; // eg. "Ghidra_11.0.1_build"
+    private static String gitReleaseAssetName; // eg. "ghidra_11.0.1_PUBLIC_20240130.zip"
+
+    public static Optional<String> getGitReleaseTagName() {
+        return Optional.ofNullable(gitReleaseTagName);
+    }
+
+    public static Optional<String> getReleaseAssetName() {
+        return Optional.ofNullable(gitReleaseAssetName);
+    }
+
+    public void setGitReleaseTagName(String tagName, String assetName) {
+        gitReleaseTagName = tagName;
+        gitReleaseAssetName = assetName;
+    }
+}