Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/runtime.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/installer/corehost/cli/apphost/bundle')
-rw-r--r--src/installer/corehost/cli/apphost/bundle/dir_utils.cpp142
-rw-r--r--src/installer/corehost/cli/apphost/bundle/dir_utils.h24
-rw-r--r--src/installer/corehost/cli/apphost/bundle/extractor.cpp250
-rw-r--r--src/installer/corehost/cli/apphost/bundle/extractor.h52
-rw-r--r--src/installer/corehost/cli/apphost/bundle/file_entry.cpp35
-rw-r--r--src/installer/corehost/cli/apphost/bundle/file_entry.h72
-rw-r--r--src/installer/corehost/cli/apphost/bundle/file_type.h32
-rw-r--r--src/installer/corehost/cli/apphost/bundle/header.cpp53
-rw-r--r--src/installer/corehost/cli/apphost/bundle/header.h96
-rw-r--r--src/installer/corehost/cli/apphost/bundle/manifest.cpp19
-rw-r--r--src/installer/corehost/cli/apphost/bundle/manifest.h24
-rw-r--r--src/installer/corehost/cli/apphost/bundle/marker.cpp33
-rw-r--r--src/installer/corehost/cli/apphost/bundle/marker.h32
-rw-r--r--src/installer/corehost/cli/apphost/bundle/reader.cpp98
-rw-r--r--src/installer/corehost/cli/apphost/bundle/reader.h72
-rw-r--r--src/installer/corehost/cli/apphost/bundle/runner.cpp65
-rw-r--r--src/installer/corehost/cli/apphost/bundle/runner.h40
17 files changed, 1139 insertions, 0 deletions
diff --git a/src/installer/corehost/cli/apphost/bundle/dir_utils.cpp b/src/installer/corehost/cli/apphost/bundle/dir_utils.cpp
new file mode 100644
index 00000000000..eede9529812
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/dir_utils.cpp
@@ -0,0 +1,142 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#include "dir_utils.h"
+#include "error_codes.h"
+#include "utils.h"
+
+using namespace bundle;
+
+bool dir_utils_t::has_dirs_in_path(const pal::string_t& path)
+{
+ return path.find_last_of(DIR_SEPARATOR) != pal::string_t::npos;
+}
+
+void dir_utils_t::create_directory_tree(const pal::string_t &path)
+{
+ if (path.empty())
+ {
+ return;
+ }
+
+ if (pal::directory_exists(path))
+ {
+ return;
+ }
+
+ if (has_dirs_in_path(path))
+ {
+ create_directory_tree(get_directory(path));
+ }
+
+ if (!pal::mkdir(path.c_str(), 0700)) // Owner - rwx
+ {
+ if (pal::directory_exists(path))
+ {
+ // The directory was created since we last checked.
+ return;
+ }
+
+ trace::error(_X("Failure processing application bundle."));
+ trace::error(_X("Failed to create directory [%s] for extracting bundled files."), path.c_str());
+ throw StatusCode::BundleExtractionIOError;
+ }
+}
+
+void dir_utils_t::remove_directory_tree(const pal::string_t& path)
+{
+ if (path.empty())
+ {
+ return;
+ }
+
+ std::vector<pal::string_t> dirs;
+ pal::readdir_onlydirectories(path, &dirs);
+
+ for (const pal::string_t &dir : dirs)
+ {
+ pal::string_t dir_path = path;
+ append_path(&dir_path, dir.c_str());
+
+ remove_directory_tree(dir_path);
+ }
+
+ std::vector<pal::string_t> files;
+ pal::readdir(path, &files);
+
+ for (const pal::string_t &file : files)
+ {
+ pal::string_t file_path = path;
+ append_path(&file_path, file.c_str());
+
+ if (!pal::remove(file_path.c_str()))
+ {
+ trace::warning(_X("Failed to remove temporary file [%s]."), file_path.c_str());
+ }
+ }
+
+ if (!pal::rmdir(path.c_str()))
+ {
+ trace::warning(_X("Failed to remove temporary directory [%s]."), path.c_str());
+ }
+}
+
+// Fixup a path to have current platform's directory separator.
+void dir_utils_t::fixup_path_separator(pal::string_t& path)
+{
+ const pal::char_t bundle_dir_separator = '/';
+
+ if (bundle_dir_separator != DIR_SEPARATOR)
+ {
+ for (size_t pos = path.find(bundle_dir_separator);
+ pos != pal::string_t::npos;
+ pos = path.find(bundle_dir_separator, pos))
+ {
+ path[pos] = DIR_SEPARATOR;
+ }
+ }
+}
+
+// Retry the rename operation with some wait in between the attempts.
+// This is an attempt to workaround for possible file locking caused by AV software.
+
+bool dir_utils_t::rename_with_retries(pal::string_t& old_name, pal::string_t& new_name, bool& dir_exists)
+{
+ for (int retry_count=0; retry_count < 500; retry_count++)
+ {
+ if (pal::rename(old_name.c_str(), new_name.c_str()) == 0)
+ {
+ return true;
+ }
+ bool should_retry = errno == EACCES;
+
+ if (pal::directory_exists(new_name))
+ {
+ // Check directory_exists() on each run, because a concurrent process may have
+ // created the new_name directory.
+ //
+ // The rename() operation above fails with errono == EACCESS if
+ // * Directory new_name already exists, or
+ // * Paths are invalid paths, or
+ // * Due to locking/permission problems.
+ // Therefore, we need to perform the directory_exists() check again.
+
+ dir_exists = true;
+ return false;
+ }
+
+ if (should_retry)
+ {
+ trace::info(_X("Retrying Rename [%s] to [%s] due to EACCES error"), old_name.c_str(), new_name.c_str());
+ pal::sleep(100);
+ continue;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ return false;
+}
diff --git a/src/installer/corehost/cli/apphost/bundle/dir_utils.h b/src/installer/corehost/cli/apphost/bundle/dir_utils.h
new file mode 100644
index 00000000000..ffe0acffb60
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/dir_utils.h
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#ifndef __DIR_UTIL_H__
+#define __DIR_UTIL_H__
+
+#include <cstdint>
+#include "pal.h"
+
+namespace bundle
+{
+ class dir_utils_t
+ {
+ public:
+ static bool has_dirs_in_path(const pal::string_t &path);
+ static void remove_directory_tree(const pal::string_t &path);
+ static void create_directory_tree(const pal::string_t &path);
+ static void fixup_path_separator(pal::string_t& path);
+ static bool rename_with_retries(pal::string_t& old_name, pal::string_t& new_name, bool &new_dir_exists);
+ };
+}
+
+#endif // __DIR_UTIL_H__
diff --git a/src/installer/corehost/cli/apphost/bundle/extractor.cpp b/src/installer/corehost/cli/apphost/bundle/extractor.cpp
new file mode 100644
index 00000000000..71bb2259e9d
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/extractor.cpp
@@ -0,0 +1,250 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#include "extractor.h"
+#include "error_codes.h"
+#include "dir_utils.h"
+#include "pal.h"
+#include "utils.h"
+
+using namespace bundle;
+
+pal::string_t& extractor_t::extraction_dir()
+{
+ if (m_extraction_dir.empty())
+ {
+ // Compute the final extraction location as:
+ // m_extraction_dir = $DOTNET_BUNDLE_EXTRACT_BASE_DIR/<app>/<id>/...
+ //
+ // If DOTNET_BUNDLE_EXTRACT_BASE_DIR is not set in the environment,
+ // a default is choosen within the temporary directory.
+
+ if (!pal::getenv(_X("DOTNET_BUNDLE_EXTRACT_BASE_DIR"), &m_extraction_dir))
+ {
+ if (!pal::get_default_bundle_extraction_base_dir(m_extraction_dir))
+ {
+ trace::error(_X("Failure processing application bundle."));
+ trace::error(_X("Failed to determine location for extracting embedded files."));
+ trace::error(_X("DOTNET_BUNDLE_EXTRACT_BASE_DIR is not set, and a read-write temp-directory couldn't be created."));
+ throw StatusCode::BundleExtractionFailure;
+ }
+ }
+
+ pal::string_t host_name = strip_executable_ext(get_filename(m_bundle_path));
+ append_path(&m_extraction_dir, host_name.c_str());
+ append_path(&m_extraction_dir, m_bundle_id.c_str());
+
+ trace::info(_X("Files embedded within the bundled will be extracted to [%s] directory."), m_extraction_dir.c_str());
+ }
+
+ return m_extraction_dir;
+}
+
+pal::string_t& extractor_t::working_extraction_dir()
+{
+ if (m_working_extraction_dir.empty())
+ {
+ // Compute the working extraction location for this process,
+ // before the extracted files are committed to the final location
+ // working_extraction_dir = $DOTNET_BUNDLE_EXTRACT_BASE_DIR/<app>/<proc-id-hex>
+
+ m_working_extraction_dir = get_directory(extraction_dir());
+ pal::char_t pid[32];
+ pal::snwprintf(pid, 32, _X("%x"), pal::get_pid());
+ append_path(&m_working_extraction_dir, pid);
+
+ trace::info(_X("Temporary directory used to extract bundled files is [%s]."), m_working_extraction_dir.c_str());
+ }
+
+ return m_working_extraction_dir;
+}
+
+// Create a file to be extracted out on disk, including any intermediate sub-directories.
+FILE* extractor_t::create_extraction_file(const pal::string_t& relative_path)
+{
+ pal::string_t file_path = working_extraction_dir();
+ append_path(&file_path, relative_path.c_str());
+
+ // working_extraction_dir is assumed to exist,
+ // so we only create sub-directories if relative_path contains directories
+ if (dir_utils_t::has_dirs_in_path(relative_path))
+ {
+ dir_utils_t::create_directory_tree(get_directory(file_path));
+ }
+
+ FILE* file = pal::file_open(file_path.c_str(), _X("wb"));
+
+ if (file == nullptr)
+ {
+ trace::error(_X("Failure processing application bundle."));
+ trace::error(_X("Failed to open file [%s] for writing."), file_path.c_str());
+ throw StatusCode::BundleExtractionIOError;
+ }
+
+ return file;
+}
+
+// Extract one file from the bundle to disk.
+void extractor_t::extract(const file_entry_t &entry, reader_t &reader)
+{
+ FILE* file = create_extraction_file(entry.relative_path());
+ reader.set_offset(entry.offset());
+ size_t size = entry.size();
+
+ if (fwrite(reader, 1, size, file) != size)
+ {
+ trace::error(_X("Failure extracting contents of the application bundle."));
+ trace::error(_X("I/O failure when writing extracted files."));
+ throw StatusCode::BundleExtractionIOError;
+ }
+
+ fclose(file);
+}
+
+void extractor_t::begin()
+{
+ // Files are extracted to a specific deterministic location on disk
+ // on first run, and are available for reuse by subsequent similar runs.
+ //
+ // The extraction should be fault tolerant with respect to:
+ // * Failures/crashes during extraction which result in partial-extraction
+ // * Race between two or more processes concurrently attempting extraction
+ //
+ // In order to solve these issues, we implement a extraction as a two-phase approach:
+ // 1) Files embedded in a bundle are extracted to a process-specific temporary
+ // extraction location (working_extraction_dir)
+ // 2) Upon successful extraction, working_extraction_dir is renamed to the actual
+ // extraction location (extraction_dir)
+ //
+ // This effectively creates a file-lock to protect against races and failed extractions.
+
+
+ dir_utils_t::create_directory_tree(working_extraction_dir());
+}
+
+void extractor_t::clean()
+{
+ dir_utils_t::remove_directory_tree(working_extraction_dir());
+}
+
+void extractor_t::commit_dir()
+{
+ // Commit an entire new extraction to the final extraction directory
+ // Retry the move operation with some wait in between the attempts. This is to workaround for possible file locking
+ // caused by AV software. Basically the extraction process above writes a bunch of executable files to disk
+ // and some AV software may decide to scan them on write. If this happens the files will be locked which blocks
+ // our ablity to move them.
+
+ bool extracted_by_concurrent_process = false;
+ bool extracted_by_current_process =
+ dir_utils_t::rename_with_retries(working_extraction_dir(), extraction_dir(), extracted_by_concurrent_process);
+
+ if (extracted_by_concurrent_process)
+ {
+ // Another process successfully extracted the dependencies
+ trace::info(_X("Extraction completed by another process, aborting current extraction."));
+ clean();
+ }
+
+ if (!extracted_by_current_process && !extracted_by_concurrent_process)
+ {
+ trace::error(_X("Failure processing application bundle."));
+ trace::error(_X("Failed to commit extracted files to directory [%s]."), extraction_dir().c_str());
+ throw StatusCode::BundleExtractionFailure;
+ }
+
+ trace::info(_X("Completed new extraction."));
+}
+
+void extractor_t::commit_file(const pal::string_t& relative_path)
+{
+ // Commit individual files to the final extraction directory.
+
+ pal::string_t working_file_path = working_extraction_dir();
+ append_path(&working_file_path, relative_path.c_str());
+
+ pal::string_t final_file_path = extraction_dir();
+ append_path(&final_file_path, relative_path.c_str());
+
+ if (dir_utils_t::has_dirs_in_path(relative_path))
+ {
+ dir_utils_t::create_directory_tree(get_directory(final_file_path));
+ }
+
+ bool extracted_by_concurrent_process = false;
+ bool extracted_by_current_process =
+ dir_utils_t::rename_with_retries(working_file_path, final_file_path, extracted_by_concurrent_process);
+
+ if (extracted_by_concurrent_process)
+ {
+ // Another process successfully extracted the dependencies
+ trace::info(_X("Extraction completed by another process, aborting current extraction."));
+ }
+
+ if (!extracted_by_current_process && !extracted_by_concurrent_process)
+ {
+ trace::error(_X("Failure processing application bundle."));
+ trace::error(_X("Failed to commit extracted files to directory [%s]."), extraction_dir().c_str());
+ throw StatusCode::BundleExtractionFailure;
+ }
+
+ trace::info(_X("Extraction recovered [%s]"), relative_path.c_str());
+}
+
+void extractor_t::extract_new(reader_t& reader)
+{
+ begin();
+ for (const file_entry_t& entry : m_manifest.files)
+ {
+ extract(entry, reader);
+ }
+ commit_dir();
+}
+
+// Verify an existing extraction contains all files listed in the bundle manifest.
+// If some files are missing, extract them individually.
+void extractor_t::verify_recover_extraction(reader_t& reader)
+{
+ pal::string_t& ext_dir = extraction_dir();
+ bool recovered = false;
+
+ for (const file_entry_t& entry : m_manifest.files)
+ {
+ pal::string_t file_path = ext_dir;
+ append_path(&file_path, entry.relative_path().c_str());
+
+ if (!pal::file_exists(file_path))
+ {
+ if (!recovered)
+ {
+ recovered = true;
+ begin();
+ }
+
+ extract(entry, reader);
+ commit_file(entry.relative_path());
+ }
+ }
+
+ if (recovered)
+ {
+ clean();
+ }
+}
+
+pal::string_t& extractor_t::extract(reader_t& reader)
+{
+ if (pal::directory_exists(extraction_dir()))
+ {
+ trace::info(_X("Reusing existing extraction of application bundle."));
+ verify_recover_extraction(reader);
+ }
+ else
+ {
+ trace::info(_X("Starting new extraction of application bundle."));
+ extract_new(reader);
+ }
+
+ return m_extraction_dir;
+}
diff --git a/src/installer/corehost/cli/apphost/bundle/extractor.h b/src/installer/corehost/cli/apphost/bundle/extractor.h
new file mode 100644
index 00000000000..73b75ecdd09
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/extractor.h
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#ifndef __EXTRACTOR_H__
+#define __EXTRACTOR_H__
+
+#include "reader.h"
+#include "manifest.h"
+
+namespace bundle
+{
+ class extractor_t
+ {
+ public:
+ extractor_t(const pal::string_t &bundle_id,
+ const pal::string_t& bundle_path,
+ const manifest_t &manifest)
+ :m_extraction_dir(),
+ m_working_extraction_dir(),
+ m_manifest(manifest)
+ {
+ m_bundle_id = bundle_id;
+ m_bundle_path = bundle_path;
+ }
+
+ pal::string_t& extract(reader_t& reader);
+
+ private:
+ pal::string_t& extraction_dir();
+ pal::string_t& working_extraction_dir();
+
+ void extract_new(reader_t& reader);
+ void verify_recover_extraction(reader_t& reader);
+
+ FILE* create_extraction_file(const pal::string_t& relative_path);
+ void extract(const file_entry_t& entry, reader_t& reader);
+
+ void begin();
+ void commit_file(const pal::string_t& relative_path);
+ void commit_dir();
+ void clean();
+
+ pal::string_t m_bundle_id;
+ pal::string_t m_bundle_path;
+ pal::string_t m_extraction_dir;
+ pal::string_t m_working_extraction_dir;
+ const manifest_t& m_manifest;
+ };
+}
+
+#endif // __EXTRACTOR_H__
diff --git a/src/installer/corehost/cli/apphost/bundle/file_entry.cpp b/src/installer/corehost/cli/apphost/bundle/file_entry.cpp
new file mode 100644
index 00000000000..ba4b235fa93
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/file_entry.cpp
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#include "file_entry.h"
+#include "trace.h"
+#include "dir_utils.h"
+#include "error_codes.h"
+
+using namespace bundle;
+
+bool file_entry_t::is_valid() const
+{
+ return m_offset > 0 && m_size >= 0 &&
+ static_cast<file_type_t>(m_type) < file_type_t::__last;
+}
+
+file_entry_t file_entry_t::read(reader_t &reader)
+{
+ // First read the fixed-sized portion of file-entry
+ const file_entry_fixed_t* fixed_data = reinterpret_cast<const file_entry_fixed_t*>(reader.read_direct(sizeof(file_entry_fixed_t)));
+ file_entry_t entry(fixed_data);
+
+ if (!entry.is_valid())
+ {
+ trace::error(_X("Failure processing application bundle; possible file corruption."));
+ trace::error(_X("Invalid FileEntry detected."));
+ throw StatusCode::BundleExtractionFailure;
+ }
+
+ reader.read_path_string(entry.m_relative_path);
+ dir_utils_t::fixup_path_separator(entry.m_relative_path);
+
+ return entry;
+}
diff --git a/src/installer/corehost/cli/apphost/bundle/file_entry.h b/src/installer/corehost/cli/apphost/bundle/file_entry.h
new file mode 100644
index 00000000000..efe405cc9a6
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/file_entry.h
@@ -0,0 +1,72 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#ifndef __FILE_ENTRY_H__
+#define __FILE_ENTRY_H__
+
+#include "file_type.h"
+#include "reader.h"
+
+namespace bundle
+{
+ // FileEntry: Records information about embedded files.
+ //
+ // The bundle manifest records the following meta-data for each
+ // file embedded in the bundle:
+ // Fixed size portion (file_entry_fixed_t)
+ // - Offset
+ // - Size
+ // - File Entry Type
+ // Variable Size portion
+ // - relative path (7-bit extension encoded length prefixed string)
+
+#pragma pack(push, 1)
+ struct file_entry_fixed_t
+ {
+ int64_t offset;
+ int64_t size;
+ file_type_t type;
+ };
+#pragma pack(pop)
+
+ class file_entry_t
+ {
+ public:
+ file_entry_t()
+ : m_offset(0)
+ , m_size(0)
+ , m_type(file_type_t::__last)
+ , m_relative_path()
+ {
+ }
+
+ file_entry_t(const file_entry_fixed_t *fixed_data)
+ :m_relative_path()
+ {
+ // File_entries in the bundle-manifest are expected to be used
+ // beyond startup (for loading files directly from bundle, lazy extraction, etc.).
+ // The contents of fixed_data are copied on to file_entry in order to
+ // avoid memory mapped IO later.
+
+ m_offset = fixed_data->offset;
+ m_size = fixed_data->size;
+ m_type = fixed_data->type;
+ }
+
+ const pal::string_t relative_path() const { return m_relative_path; }
+ int64_t offset() const { return m_offset; }
+ int64_t size() const { return m_size; }
+ file_type_t type() const { return m_type; }
+
+ static file_entry_t read(reader_t &reader);
+
+ private:
+ int64_t m_offset;
+ int64_t m_size;
+ file_type_t m_type;
+ pal::string_t m_relative_path; // Path of an embedded file, relative to the extraction directory.
+ bool is_valid() const;
+ };
+}
+#endif // __FILE_ENTRY_H__
diff --git a/src/installer/corehost/cli/apphost/bundle/file_type.h b/src/installer/corehost/cli/apphost/bundle/file_type.h
new file mode 100644
index 00000000000..acbfca06859
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/file_type.h
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#ifndef __FILE_TYPE_H__
+#define __FILE_TYPE_H__
+
+#include <cstdint>
+
+namespace bundle
+{
+ // FileType: Identifies the type of file embedded into the bundle.
+ //
+ // The bundler differentiates a few kinds of files via the manifest,
+ // with respect to the way in which they'll be used by the runtime.
+ //
+ // Currently all files are extracted out to the disk, but future
+ // implementations will process certain file_types directly from the bundle.
+
+ enum file_type_t : uint8_t
+ {
+ unknown,
+ assembly,
+ native_binary,
+ deps_json,
+ runtime_config_json,
+ symbols,
+ __last
+ };
+}
+
+#endif // __FILE_TYPE_H__
diff --git a/src/installer/corehost/cli/apphost/bundle/header.cpp b/src/installer/corehost/cli/apphost/bundle/header.cpp
new file mode 100644
index 00000000000..23695bb93f1
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/header.cpp
@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#include "header.h"
+#include "reader.h"
+#include "error_codes.h"
+#include "trace.h"
+
+using namespace bundle;
+
+// The AppHost expects the bundle_header to be an exact_match for which it was built.
+// The framework accepts backwards compatible header versions.
+bool header_fixed_t::is_valid(bool exact_match) const
+{
+ if (num_embedded_files <= 0)
+ {
+ return false;
+ }
+
+ if (exact_match)
+ {
+ return (major_version == header_t::major_version) && (minor_version == header_t::minor_version);
+ }
+
+ return ((major_version < header_t::major_version) ||
+ (major_version == header_t::major_version && minor_version <= header_t::minor_version));
+}
+
+header_t header_t::read(reader_t& reader, bool need_exact_version)
+{
+ const header_fixed_t* fixed_header = reinterpret_cast<const header_fixed_t*>(reader.read_direct(sizeof(header_fixed_t)));
+
+ if (!fixed_header->is_valid(need_exact_version))
+ {
+ trace::error(_X("Failure processing application bundle."));
+ trace::error(_X("Bundle header version compatibility check failed."));
+
+ throw StatusCode::BundleExtractionFailure;
+ }
+
+ header_t header(fixed_header->num_embedded_files);
+
+ // bundle_id is a component of the extraction path
+ reader.read_path_string(header.m_bundle_id);
+
+ if (fixed_header->major_version > 1)
+ {
+ header.m_v2_header = reinterpret_cast<const header_fixed_v2_t*>(reader.read_direct(sizeof(header_fixed_v2_t)));
+ }
+
+ return header;
+}
diff --git a/src/installer/corehost/cli/apphost/bundle/header.h b/src/installer/corehost/cli/apphost/bundle/header.h
new file mode 100644
index 00000000000..1b1696219dc
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/header.h
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#ifndef __HEADER_H__
+#define __HEADER_H__
+
+#include <cstdint>
+#include "pal.h"
+#include "reader.h"
+
+namespace bundle
+{
+ // The Bundle Header (v1)
+ // Fixed size thunk (header_fixed_t)
+ // - Major Version
+ // - Minor Version
+ // - Number of embedded files
+ // Variable size portion:
+ // - Bundle ID (7-bit extension encoded length prefixed string)
+ // The Bundle Header (v2) [additional content]
+ // Fixed size thunk (header_fixed_v2_t)
+ // - DepsJson Location (Offset, Size)
+ // - RuntimeConfig Location (Offset, Size)
+ // - Flags
+
+#pragma pack(push, 1)
+ struct header_fixed_t
+ {
+ public:
+ uint32_t major_version;
+ uint32_t minor_version;
+ int32_t num_embedded_files;
+
+ bool is_valid(bool exact_match = false) const;
+ };
+#pragma pack(pop)
+
+ // netcoreapp3_compat_mode flag is set on a .net5 app, which chooses to build single-file apps in .netcore3.x compat mode,
+ // This indicates that:
+ // All published files are bundled into the app; some of them will be extracted to disk.
+ // AppContext.BaseDirectory is set to the extraction directory (and not the AppHost directory).
+ enum header_flags_t : uint64_t
+ {
+ none = 0,
+ netcoreapp3_compat_mode = 1
+ };
+
+#pragma pack(push, 1)
+ struct location_t
+ {
+ public:
+ int64_t offset;
+ int64_t size;
+ };
+
+ // header_fixed_v2_t is available in single-file apps targetting .net5+ frameworks.
+ // It stores information that facilitates the host to process contents of the bundle without extraction.
+ //
+ // The location of deps.json and runtimeconfig.json is already available in the Bundle manifest.
+ // However, the data is cached here in order to simplify the bundle-processing performed by hostfxr.
+ struct header_fixed_v2_t
+ {
+ public:
+ location_t deps_json_location;
+ location_t runtimeconfig_json_location;
+ header_flags_t flags;
+ };
+#pragma pack(pop)
+
+ struct header_t
+ {
+ public:
+ header_t(int32_t num_embedded_files = 0)
+ : m_num_embedded_files(num_embedded_files)
+ , m_bundle_id()
+ , m_v2_header(NULL)
+
+ {
+ }
+
+ static header_t read(reader_t& reader, bool need_exact_version);
+ const pal::string_t& bundle_id() { return m_bundle_id; }
+ int32_t num_embedded_files() { return m_num_embedded_files; }
+
+ static const uint32_t major_version = 2;
+ static const uint32_t minor_version = 0;
+
+ private:
+ int32_t m_num_embedded_files;
+ pal::string_t m_bundle_id;
+ const header_fixed_v2_t* m_v2_header;
+
+ };
+}
+#endif // __HEADER_H__
diff --git a/src/installer/corehost/cli/apphost/bundle/manifest.cpp b/src/installer/corehost/cli/apphost/bundle/manifest.cpp
new file mode 100644
index 00000000000..6de65d3a96b
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/manifest.cpp
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#include "manifest.h"
+
+using namespace bundle;
+
+manifest_t manifest_t::read(reader_t& reader, int32_t num_files)
+{
+ manifest_t manifest;
+
+ for (int32_t i = 0; i < num_files; i++)
+ {
+ manifest.files.emplace_back(file_entry_t::read(reader));
+ }
+
+ return manifest;
+}
diff --git a/src/installer/corehost/cli/apphost/bundle/manifest.h b/src/installer/corehost/cli/apphost/bundle/manifest.h
new file mode 100644
index 00000000000..ee8cd1edab5
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/manifest.h
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#ifndef __MANIFEST_H__
+#define __MANIFEST_H__
+
+#include <list>
+#include "file_entry.h"
+
+namespace bundle
+{
+ // Bundle Manifest contains:
+ // Series of file entries (for each embedded file)
+
+ class manifest_t
+ {
+ public:
+ std::vector<file_entry_t> files;
+
+ static manifest_t read(reader_t &reader, int32_t num_files);
+ };
+}
+#endif // __MANIFEST_H__
diff --git a/src/installer/corehost/cli/apphost/bundle/marker.cpp b/src/installer/corehost/cli/apphost/bundle/marker.cpp
new file mode 100644
index 00000000000..d2302923226
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/marker.cpp
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#include "marker.h"
+#include "pal.h"
+#include "trace.h"
+#include "utils.h"
+
+using namespace bundle;
+
+int64_t marker_t::header_offset()
+{
+ // Contains the bundle_placeholder default value at compile time.
+ // If this is a single-file bundle, the last 8 bytes are replaced
+ // bundle-header replaced by "dotnet publish" with the offset
+ // where the bundle_header is located.
+ static volatile uint8_t placeholder[] =
+ {
+ // 8 bytes represent the bundle header-offset
+ // Zero for non-bundle apphosts (default).
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 64 bytes represent the bundle signature: SHA-256 for ".net core bundle"
+ 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
+ 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
+ 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
+ 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
+ };
+
+ volatile marker_t* marker = reinterpret_cast<volatile marker_t *>(placeholder);
+
+ return marker->locator.bundle_header_offset;
+}
diff --git a/src/installer/corehost/cli/apphost/bundle/marker.h b/src/installer/corehost/cli/apphost/bundle/marker.h
new file mode 100644
index 00000000000..52e185fbe79
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/marker.h
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#ifndef __MARKER_H__
+#define __MARKER_H__
+
+#include <cstdint>
+
+namespace bundle
+{
+#pragma pack(push, 1)
+ union marker_t
+ {
+ public:
+ uint8_t placeholder[40];
+ struct
+ {
+ int64_t bundle_header_offset;
+ uint8_t signature[32];
+ } locator;
+
+ static int64_t header_offset();
+ static bool is_bundle()
+ {
+ return header_offset() != 0;
+ }
+ };
+#pragma pack(pop)
+
+}
+#endif // __MARKER_H__
diff --git a/src/installer/corehost/cli/apphost/bundle/reader.cpp b/src/installer/corehost/cli/apphost/bundle/reader.cpp
new file mode 100644
index 00000000000..e2fa9b6574c
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/reader.cpp
@@ -0,0 +1,98 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#include "reader.h"
+#include "error_codes.h"
+#include "trace.h"
+
+using namespace bundle;
+
+const int8_t* reader_t::add_without_overflow(const int8_t* ptr, int64_t len)
+{
+ const int8_t* new_ptr = ptr + len;
+
+ // The following check will fail in case len < 0 (which is also an error while reading)
+ // even if the actual arthmetic didn't overflow.
+ if (new_ptr < ptr)
+ {
+ trace::error(_X("Failure processing application bundle; possible file corruption."));
+ trace::error(_X("Arithmetic overflow computing bundle-bounds."));
+ throw StatusCode::BundleExtractionFailure;
+ }
+
+ return new_ptr;
+}
+
+void reader_t::set_offset(int64_t offset)
+{
+ if (offset < 0 || offset >= m_bound)
+ {
+ trace::error(_X("Failure processing application bundle; possible file corruption."));
+ trace::error(_X("Arithmetic overflow while reading bundle."));
+ throw StatusCode::BundleExtractionFailure;
+ }
+
+ m_ptr = m_base_ptr + offset;
+}
+
+void reader_t::bounds_check(int64_t len)
+{
+ const int8_t* post_read_ptr = add_without_overflow(m_ptr, len);
+
+ // It is legal for post_read_ptr == m_bound_ptr after reading the last byte.
+ if (m_ptr < m_base_ptr || post_read_ptr > m_bound_ptr)
+ {
+ trace::error(_X("Failure processing application bundle; possible file corruption."));
+ trace::error(_X("Bounds check failed while reading the bundle."));
+ throw StatusCode::BundleExtractionFailure;
+ }
+}
+
+// Handle the relatively uncommon scenario where the bundle ID or
+// the relative-path of a file within the bundle is longer than 127 bytes
+size_t reader_t::read_path_length()
+{
+ size_t length = 0;
+
+ int8_t first_byte = read();
+
+ // If the high bit is set, it means there are more bytes to read.
+ if ((first_byte & 0x80) == 0)
+ {
+ length = first_byte;
+ }
+ else
+ {
+ int8_t second_byte = read();
+
+ if (second_byte & 0x80)
+ {
+ // There can be no more than two bytes in path_length
+ trace::error(_X("Failure processing application bundle; possible file corruption."));
+ trace::error(_X("Path length encoding read beyond two bytes."));
+
+ throw StatusCode::BundleExtractionFailure;
+ }
+
+ length = (second_byte << 7) | (first_byte & 0x7f);
+ }
+
+ if (length <= 0 || length > PATH_MAX)
+ {
+ trace::error(_X("Failure processing application bundle; possible file corruption."));
+ trace::error(_X("Path length is zero or too long."));
+ throw StatusCode::BundleExtractionFailure;
+ }
+
+ return length;
+}
+
+void reader_t::read_path_string(pal::string_t &str)
+{
+ size_t size = read_path_length();
+ std::unique_ptr<uint8_t[]> buffer{ new uint8_t[size + 1] };
+ read(buffer.get(), size);
+ buffer[size] = 0; // null-terminator
+ pal::clr_palstring(reinterpret_cast<const char*>(buffer.get()), &str);
+}
diff --git a/src/installer/corehost/cli/apphost/bundle/reader.h b/src/installer/corehost/cli/apphost/bundle/reader.h
new file mode 100644
index 00000000000..1824ece515a
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/reader.h
@@ -0,0 +1,72 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#ifndef __READER_H__
+#define __READER_H__
+
+#include <cstdint>
+#include "pal.h"
+
+namespace bundle
+{
+ // Helper class for reading sequentially from the memory-mapped bundle file.
+ struct reader_t
+ {
+ reader_t(const int8_t* base_ptr, int64_t bound)
+ : m_base_ptr(base_ptr)
+ , m_ptr(base_ptr)
+ , m_bound(bound)
+ , m_bound_ptr(add_without_overflow(base_ptr, bound))
+ {
+ }
+
+ public:
+
+ void set_offset(int64_t offset);
+
+ operator const int8_t*() const
+ {
+ return m_ptr;
+ }
+
+ int8_t read()
+ {
+ bounds_check();
+ return *m_ptr++;
+ }
+
+ // Copy len bytes from m_ptr to dest
+ void read(void* dest, int64_t len)
+ {
+ bounds_check(len);
+ memcpy(dest, m_ptr, len);
+ m_ptr += len;
+ }
+
+ // Return a pointer to the requested bytes within the memory-mapped file.
+ // Skip over len bytes.
+ const int8_t* read_direct(int64_t len)
+ {
+ bounds_check(len);
+ const int8_t *ptr = m_ptr;
+ m_ptr += len;
+ return ptr;
+ }
+
+ size_t read_path_length();
+ void read_path_string(pal::string_t &str);
+
+ private:
+
+ void bounds_check(int64_t len = 1);
+ static const int8_t* add_without_overflow(const int8_t* ptr, int64_t len);
+
+ const int8_t* const m_base_ptr;
+ const int8_t* m_ptr;
+ const int64_t m_bound;
+ const int8_t* const m_bound_ptr;
+ };
+}
+
+#endif // __READER_H__
diff --git a/src/installer/corehost/cli/apphost/bundle/runner.cpp b/src/installer/corehost/cli/apphost/bundle/runner.cpp
new file mode 100644
index 00000000000..50d74235d7a
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/runner.cpp
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#include <memory>
+#include "extractor.h"
+#include "runner.h"
+#include "trace.h"
+#include "header.h"
+#include "marker.h"
+#include "manifest.h"
+
+using namespace bundle;
+
+void runner_t::map_host()
+{
+ m_bundle_map = (int8_t *) pal::map_file_readonly(m_bundle_path, m_bundle_length);
+
+ if (m_bundle_map == nullptr)
+ {
+ trace::error(_X("Failure processing application bundle."));
+ trace::error(_X("Couldn't memory map the bundle file for reading."));
+ throw StatusCode::BundleExtractionIOError;
+ }
+}
+
+void runner_t::unmap_host()
+{
+ if (!pal::unmap_file(m_bundle_map, m_bundle_length))
+ {
+ trace::warning(_X("Failed to unmap bundle after extraction."));
+ }
+}
+
+// Current support for executing single-file bundles involves
+// extraction of embedded files to actual files on disk.
+// This method implements the file extraction functionality at startup.
+StatusCode runner_t::extract()
+{
+ try
+ {
+ map_host();
+ reader_t reader(m_bundle_map, m_bundle_length);
+
+ // Read the bundle header
+ reader.set_offset(marker_t::header_offset());
+ header_t header = header_t::read(reader, /* need_exact_version: */ true);
+
+ // Read the bundle manifest
+ // Reader is at the correct offset
+ manifest_t manifest = manifest_t::read(reader, header.num_embedded_files());
+
+ // Extract the files
+ extractor_t extractor(header.bundle_id(), m_bundle_path, manifest);
+ m_extraction_dir = extractor.extract(reader);
+
+ unmap_host();
+ return StatusCode::Success;
+ }
+ catch (StatusCode e)
+ {
+ return e;
+ }
+}
+
diff --git a/src/installer/corehost/cli/apphost/bundle/runner.h b/src/installer/corehost/cli/apphost/bundle/runner.h
new file mode 100644
index 00000000000..07dadede0fd
--- /dev/null
+++ b/src/installer/corehost/cli/apphost/bundle/runner.h
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#ifndef __RUNNER_H__
+#define __RUNNER_H__
+
+#include "error_codes.h"
+
+namespace bundle
+{
+ class runner_t
+ {
+ public:
+ runner_t(const pal::string_t& bundle_path)
+ : m_bundle_path(bundle_path)
+ , m_bundle_map(nullptr)
+ , m_bundle_length(0)
+ {
+ }
+
+ StatusCode extract();
+
+ pal::string_t extraction_dir()
+ {
+ return m_extraction_dir;
+ }
+
+ private:
+ void map_host();
+ void unmap_host();
+
+ pal::string_t m_bundle_path;
+ pal::string_t m_extraction_dir;
+ int8_t* m_bundle_map;
+ size_t m_bundle_length;
+ };
+}
+
+#endif // __RUNNER_H__