This patch adds support for building EROFS filesystems from OCI-compliant
container registries, enabling users to create EROFS images directly from
container images stored in registries like Docker Hub, Quay.io, etc.

The implementation includes:
- OCI remote backend with registry authentication support
- Manifest parsing for Docker v2 and OCI v1 formats
- Layer extraction and tar processing integration
- Multi-platform image selection capability
- Both anonymous and authenticated registry access
- Comprehensive build system integration

New mkfs.erofs option: --oci=registry/repo:tag[,options]

Supported options:
- platform=os/arch (default: linux/amd64)
- layer=N (extract specific layer, default: all layers)
- anonymous (use anonymous access)
- username/password (basic authentication)

Signed-off-by: Chengyu Zhu <hud...@cyzhu.com>
Signed-off-by: Changzhi Xie <s...@qq.com>
---
 configure.ac       |  45 +++
 lib/Makefile.am    |   8 +-
 lib/liberofs_oci.h |  73 +++++
 lib/remotes/oci.c  | 665 +++++++++++++++++++++++++++++++++++++++++++++
 mkfs/main.c        | 155 ++++++++++-
 5 files changed, 944 insertions(+), 2 deletions(-)
 create mode 100644 lib/liberofs_oci.h
 create mode 100644 lib/remotes/oci.c

diff --git a/configure.ac b/configure.ac
index 7769ac9..4659747 100644
--- a/configure.ac
+++ b/configure.ac
@@ -177,10 +177,19 @@ AC_ARG_WITH(libxml2,
    [AS_HELP_STRING([--with-libxml2],
       [Enable and build with libxml2 support @<:@default=auto@:>@])])
 
+AC_ARG_WITH(json_c,
+   [AS_HELP_STRING([--with-json-c],
+      [Enable and build with json-c support @<:@default=auto@:>@])])
+
 AC_ARG_ENABLE(s3,
    [AS_HELP_STRING([--enable-s3], [enable s3 image generation support 
@<:@default=no@:>@])],
    [enable_s3="$enableval"], [enable_s3="no"])
 
+AC_ARG_ENABLE(oci,
+    AS_HELP_STRING([--enable-oci],
+                   [enable OCI registry based input support 
@<:@default=no@:>@]),
+    [enable_oci="$enableval"],[enable_oci="no"])
+
 AC_ARG_ENABLE(fuse,
    [AS_HELP_STRING([--enable-fuse], [enable erofsfuse @<:@default=no@:>@])],
    [enable_fuse="$enableval"], [enable_fuse="no"])
@@ -624,6 +633,37 @@ AS_IF([test "x$with_libcurl" != "xno"], [
   ])
 ])
 
+# Configure json-c
+have_json_c="no"
+AS_IF([test "x$with_json_c" != "xno"], [
+  PKG_CHECK_MODULES([json_c], [json-c], [
+    saved_LIBS="$LIBS"
+    saved_CPPFLAGS=${CPPFLAGS}
+    CPPFLAGS="${json_c_CFLAGS} ${CPPFLAGS}"
+    LIBS="${json_c_LIBS} $LIBS"
+    AC_CHECK_HEADERS([json-c/json.h],[
+      AC_CHECK_DECL(json_tokener_parse, [have_json_c="yes"],
+        [AC_MSG_ERROR([json-c doesn't work properly])], [[
+#include <json-c/json.h>
+      ]])
+    ])
+    LIBS="${saved_LIBS}"
+    CPPFLAGS="${saved_CPPFLAGS}"
+  ], [
+    AC_MSG_ERROR([Cannot find proper json-c])
+  ])
+])
+
+# Validate dependencies for OCI registry
+AS_IF([test "x$enable_oci" = "xyes"], [
+  AS_IF([test "x$have_libcurl" = "xyes" -a "x$have_json_c" = "xyes"], [
+    have_oci="yes"
+  ], [
+    have_oci="no"
+    AC_MSG_ERROR([OCI registry disabled: missing libcurl or json-c])
+  ])
+], [have_oci="no"])
+
 # Configure openssl
 have_openssl="no"
 AS_IF([test "x$with_openssl" != "xno"], [
@@ -712,6 +752,7 @@ AM_CONDITIONAL([ENABLE_OPENSSL], [test "x${have_openssl}" = 
"xyes"])
 AM_CONDITIONAL([ENABLE_LIBXML2], [test "x${have_libxml2}" = "xyes"])
 AM_CONDITIONAL([ENABLE_S3], [test "x${have_s3}" = "xyes"])
 AM_CONDITIONAL([ENABLE_STATIC_FUSE], [test "x${enable_static_fuse}" = "xyes"])
+AM_CONDITIONAL([ENABLE_OCI], [test "x${have_oci}" = "xyes"])
 
 if test "x$have_uuid" = "xyes"; then
   AC_DEFINE([HAVE_LIBUUID], 1, [Define to 1 if libuuid is found])
@@ -784,6 +825,10 @@ if test "x$have_s3" = "xyes"; then
   AC_DEFINE([S3EROFS_ENABLED], 1, [Define to 1 if s3 is enabled])
 fi
 
+if test "x$have_oci" = "xyes"; then
+  AC_DEFINE([OCIEROFS_ENABLED], 1, [Define to 1 if OCI registry is enabled])
+fi
+
 # Dump maximum block size
 AS_IF([test "x$erofs_cv_max_block_size" = "x"],
       [$erofs_cv_max_block_size = 4096], [])
diff --git a/lib/Makefile.am b/lib/Makefile.am
index b079897..1e930e3 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -40,6 +40,7 @@ liberofs_la_SOURCES = config.c io.c cache.c super.c inode.c 
xattr.c exclude.c \
                      vmdk.c metabox.c
 
 liberofs_la_CFLAGS = -Wall ${libuuid_CFLAGS} -I$(top_srcdir)/include
+liberofs_la_LDFLAGS =
 if ENABLE_LZ4
 liberofs_la_CFLAGS += ${liblz4_CFLAGS}
 liberofs_la_SOURCES += compressor_lz4.c
@@ -71,6 +72,11 @@ if ENABLE_S3
 liberofs_la_SOURCES += remotes/s3.c
 endif
 if ENABLE_EROFS_MT
-liberofs_la_LDFLAGS = -lpthread
+liberofs_la_LDFLAGS += -lpthread
 liberofs_la_SOURCES += workqueue.c
 endif
+if ENABLE_OCI
+liberofs_la_SOURCES += remotes/oci.c
+liberofs_la_CFLAGS += ${libcurl_CFLAGS} ${json_c_CFLAGS}
+liberofs_la_LDFLAGS += ${libcurl_LIBS} ${json_c_LIBS}
+endif
diff --git a/lib/liberofs_oci.h b/lib/liberofs_oci.h
new file mode 100644
index 0000000..8ed98d7
--- /dev/null
+++ b/lib/liberofs_oci.h
@@ -0,0 +1,73 @@
+/* SPDX-License-Identifier: GPL-2.0+ OR Apache-2.0 */
+/*
+ * Copyright (C) 2025 Tencent, Inc.
+ *             http://www.tencent.com/
+ */
+#ifndef __EROFS_OCI_H
+#define __EROFS_OCI_H
+
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct erofs_inode;
+
+/* OCI authentication modes */
+enum oci_auth_mode {
+       OCI_AUTH_ANONYMOUS,     /* No authentication */
+       OCI_AUTH_TOKEN,         /* Bearer token authentication */
+       OCI_AUTH_BASIC,         /* Basic authentication */
+};
+
+/* Maximum lengths for OCI parameters */
+#define OCI_REGISTRY_LEN               256
+#define OCI_REPOSITORY_LEN             256
+#define OCI_TAG_LEN                    64
+#define OCI_PLATFORM_LEN               32
+#define OCI_USERNAME_LEN               64
+#define OCI_PASSWORD_LEN               256
+
+/**
+ * struct erofs_oci - OCI configuration
+ * @registry:         registry hostname (e.g., "registry-1.docker.io")
+ * @repository:       image repository (e.g., "library/ubuntu")
+ * @tag:              image tag or digest (e.g., "latest" or sha256:...)
+ * @platform:         target platform in "os/arch" format (e.g., "linux/amd64")
+ * @username:         username for basic authentication
+ * @password:         password for basic authentication
+ * @auth_mode:        authentication mode to use
+ * @layer_index:      specific layer to extract (-1 for all layers)
+ * @anonymous_access: whether to use anonymous access
+ */
+struct erofs_oci {
+       char registry[OCI_REGISTRY_LEN + 1];
+       char repository[OCI_REPOSITORY_LEN + 1];
+       char tag[OCI_TAG_LEN + 1];
+       char platform[OCI_PLATFORM_LEN + 1];
+       char username[OCI_USERNAME_LEN + 1];
+       char password[OCI_PASSWORD_LEN + 1];
+       
+       enum oci_auth_mode auth_mode;
+       int layer_index;
+       bool anonymous_access;
+};
+
+/**
+ * ocierofs_build_trees - Build file trees from OCI container image layers
+ * @root:     root inode to build the file tree under
+ * @oci:      OCI configuration
+ * @ref:      reference string (unused, kept for API compatibility)
+ * @fillzero: if true, only create inodes without downloading actual data
+ *
+ * Return: 0 on success, negative errno on failure
+ */
+int ocierofs_build_trees(struct erofs_inode *root, struct erofs_oci *oci,
+                        const char *ref, bool fillzero);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __EROFS_OCI_H */
diff --git a/lib/remotes/oci.c b/lib/remotes/oci.c
new file mode 100644
index 0000000..e86af50
--- /dev/null
+++ b/lib/remotes/oci.c
@@ -0,0 +1,665 @@
+/* SPDX-License-Identifier: GPL-2.0+ OR Apache-2.0 */
+/*
+ * Copyright (C) 2025 Tencent, Inc.
+ *             http://www.tencent.com/
+ */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <curl/curl.h>
+#include <json-c/json.h>
+#include "erofs/internal.h"
+#include "erofs/print.h"
+#include "erofs/inode.h"
+#include "erofs/blobchunk.h"
+#include "erofs/diskbuf.h"
+#include "erofs/rebuild.h"
+#include "erofs/tar.h"
+#include "liberofs_oci.h"
+
+/* Constants */
+#define OCI_URL_MAX_LEN                        8192
+#define OCI_AUTH_HEADER_MAX_LEN                1024
+#define OCI_TEMP_FILENAME_MAX_LEN      256
+
+/* Media types */
+#define DOCKER_MEDIATYPE_MANIFEST_V2   
"application/vnd.docker.distribution.manifest.v2+json"
+#define DOCKER_MEDIATYPE_MANIFEST_V1   
"application/vnd.docker.distribution.manifest.v1+json"
+#define DOCKER_MEDIATYPE_MANIFEST_LIST 
"application/vnd.docker.distribution.manifest.list.v2+json"
+#define OCI_MEDIATYPE_MANIFEST         
"application/vnd.oci.image.manifest.v1+json"
+#define OCI_MEDIATYPE_INDEX            
"application/vnd.oci.image.index.v1+json"
+
+/* Registry constants */
+#define DOCKER_REGISTRY                        "docker.io"
+#define DOCKER_API_REGISTRY            "registry-1.docker.io"
+#define QUAY_REGISTRY                  "quay.io"
+
+/* Global CURL handle */
+static CURL *g_curl;
+
+/* HTTP request/response structures */
+struct oci_request {
+       char url[OCI_URL_MAX_LEN];
+       struct curl_slist *headers;
+};
+
+struct oci_response {
+       char *data;
+       size_t size;
+       long http_code;
+};
+
+/* Layer information */
+struct oci_layer {
+       char *digest;
+       u64 size;
+       const char *media_type;
+};
+
+/* Layer streaming context */
+struct oci_stream {
+       struct erofs_tarfile tarfile;
+       FILE *temp_file;
+       char temp_filename[OCI_TEMP_FILENAME_MAX_LEN];
+       int layer_index;
+};
+
+/* Callback for writing response data to memory */
+static size_t oci_write_callback(void *contents, size_t size, size_t nmemb, 
void *userp)
+{
+       size_t realsize = size * nmemb;
+       struct oci_response *resp = userp;
+       char *ptr;
+
+       ptr = realloc(resp->data, resp->size + realsize + 1);
+       if (!ptr)
+               return 0;
+
+       resp->data = ptr;
+       memcpy(&resp->data[resp->size], contents, realsize);
+       resp->size += realsize;
+       resp->data[resp->size] = '\0';
+       return realsize;
+}
+
+/* Callback for writing layer data to file */
+static size_t oci_layer_write_callback(void *contents, size_t size, size_t 
nmemb, void *userp)
+{
+       struct oci_stream *stream = userp;
+       size_t realsize = size * nmemb;
+       
+       if (!stream->temp_file)
+               return 0;
+               
+       if (fwrite(contents, 1, realsize, stream->temp_file) != realsize) {
+               erofs_err("failed to write layer data for layer %d", 
stream->layer_index);
+               return 0;
+       }
+       
+       return realsize;
+}
+
+/* Perform HTTP request */
+static int oci_request_perform(struct oci_request *req, struct oci_response 
*resp)
+{
+       CURLcode res;
+       
+       erofs_dbg("requesting URL: %s", req->url);
+       
+       curl_easy_setopt(g_curl, CURLOPT_URL, req->url);
+       curl_easy_setopt(g_curl, CURLOPT_WRITEDATA, resp);
+       curl_easy_setopt(g_curl, CURLOPT_WRITEFUNCTION, oci_write_callback);
+       
+       if (req->headers)
+               curl_easy_setopt(g_curl, CURLOPT_HTTPHEADER, req->headers);
+
+       res = curl_easy_perform(g_curl);
+       if (res != CURLE_OK) {
+               erofs_err("curl request failed: %s", curl_easy_strerror(res));
+               return -EIO;
+       }
+
+       res = curl_easy_getinfo(g_curl, CURLINFO_RESPONSE_CODE, 
&resp->http_code);
+       if (res != CURLE_OK) {
+               erofs_err("failed to get HTTP response code: %s", 
curl_easy_strerror(res));
+               return -EIO;
+       }
+
+       if (resp->http_code < 200 || resp->http_code >= 300) {
+               erofs_err("HTTP request failed with code %ld", resp->http_code);
+               return -EIO;
+       }
+
+       return 0;
+}
+
+/* Get authentication token */
+static char *oci_get_auth_token(const char *registry, const char *repository,
+                               const char *username, const char *password)
+{
+       struct oci_request req = {};
+       struct oci_response resp = {};
+       json_object *root, *token_obj;
+       const char *token;
+       char *auth_header = NULL;
+       int ret;
+
+       if (!registry || !repository)
+               return ERR_PTR(-EINVAL);
+
+       if (!strcmp(registry, DOCKER_API_REGISTRY) || !strcmp(registry, 
DOCKER_REGISTRY)) {
+               snprintf(req.url, sizeof(req.url), 
+                        
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull";,
+                        repository);
+       } else if (!strcmp(registry, QUAY_REGISTRY)) {
+               snprintf(req.url, sizeof(req.url), 
+                        
"https://%s/v2/auth?service=%s&scope=repository:%s:pull";,
+                        QUAY_REGISTRY, QUAY_REGISTRY, repository);
+       } else {
+               snprintf(req.url, sizeof(req.url), 
+                        "https://%s/token?service=%s&scope=repository:%s:pull";,
+                        registry, registry, repository);
+       }
+
+       if (username && password && *username) {
+               char *userpwd = malloc(strlen(username) + strlen(password) + 2);
+               if (!userpwd)
+                       return ERR_PTR(-ENOMEM);
+                       
+               sprintf(userpwd, "%s:%s", username, password);
+               curl_easy_setopt(g_curl, CURLOPT_USERPWD, userpwd);
+               curl_easy_setopt(g_curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
+               free(userpwd);
+       }
+
+       ret = oci_request_perform(&req, &resp);
+       
+       curl_easy_setopt(g_curl, CURLOPT_USERPWD, NULL);
+       curl_easy_setopt(g_curl, CURLOPT_HTTPAUTH, CURLAUTH_NONE);
+       
+       if (ret)
+               goto out;
+
+       root = json_tokener_parse(resp.data);
+       if (!root) {
+               erofs_err("failed to parse auth response");
+               ret = -EINVAL;
+               goto out;
+       }
+
+       if (!json_object_object_get_ex(root, "token", &token_obj)) {
+               erofs_err("no token found in auth response");
+               ret = -EINVAL;
+               goto out_json;
+       }
+
+       token = json_object_get_string(token_obj);
+       if (!token) {
+               erofs_err("invalid token in auth response");
+               ret = -EINVAL;
+               goto out_json;
+       }
+
+       auth_header = malloc(strlen("Authorization: Bearer ") + strlen(token) + 
1);
+       if (!auth_header) {
+               ret = -ENOMEM;
+               goto out_json;
+       }
+       
+       sprintf(auth_header, "Authorization: Bearer %s", token);
+
+out_json:
+       json_object_put(root);
+out:
+       free(resp.data);
+       return ret ? ERR_PTR(ret) : auth_header;
+}
+
+/* Get manifest digest for specified platform */
+static char *oci_get_manifest_digest(const char *registry, const char 
*repository,
+                                    const char *tag, const char *platform,
+                                    const char *auth_header)
+{
+       struct oci_request req = {};
+       struct oci_response resp = {};
+       json_object *root, *manifests, *manifest, *platform_obj, *arch_obj, 
*os_obj, *digest_obj;
+       json_object *schema_obj, *media_type_obj;
+       char *digest = NULL;
+       const char *api_registry;
+       int ret, len, i;
+
+       if (!registry || !repository || !tag || !platform)
+               return ERR_PTR(-EINVAL);
+
+       api_registry = (!strcmp(registry, DOCKER_REGISTRY)) ? 
DOCKER_API_REGISTRY : registry;
+
+       snprintf(req.url, sizeof(req.url), "https://%s/v2/%s/manifests/%s";,
+                api_registry, repository, tag);
+
+       if (auth_header && strstr(auth_header, "Bearer"))
+               req.headers = curl_slist_append(req.headers, auth_header);
+
+       req.headers = curl_slist_append(req.headers, 
+               "Accept: " DOCKER_MEDIATYPE_MANIFEST_LIST ","
+               OCI_MEDIATYPE_INDEX ","
+               DOCKER_MEDIATYPE_MANIFEST_V1 ","
+               DOCKER_MEDIATYPE_MANIFEST_V2);
+
+       ret = oci_request_perform(&req, &resp);
+       if (ret)
+               goto out;
+
+       root = json_tokener_parse(resp.data);
+       if (!root) {
+               erofs_err("failed to parse manifest JSON");
+               ret = -EINVAL;
+               goto out;
+       }
+
+       /* Handle v1 manifests */
+       if (json_object_object_get_ex(root, "schemaVersion", &schema_obj)) {
+               if (json_object_get_int(schema_obj) == 1) {
+                       digest = strdup(tag);
+                       goto out_json;
+               }
+       }
+
+       /* Handle single v2 manifests */
+       if (json_object_object_get_ex(root, "mediaType", &media_type_obj)) {
+               const char *media_type = json_object_get_string(media_type_obj);
+               if (!strcmp(media_type, DOCKER_MEDIATYPE_MANIFEST_V2) ||
+                   !strcmp(media_type, OCI_MEDIATYPE_MANIFEST)) {
+                       digest = strdup(tag);
+                       goto out_json;
+               }
+       }
+
+       /* Handle manifest lists */
+       if (!json_object_object_get_ex(root, "manifests", &manifests)) {
+               erofs_err("no manifests found in manifest list");
+               ret = -EINVAL;
+               goto out_json;
+       }
+
+       len = json_object_array_length(manifests);
+       for (i = 0; i < len; i++) {
+               manifest = json_object_array_get_idx(manifests, i);
+               
+               if (json_object_object_get_ex(manifest, "platform", 
&platform_obj) &&
+                   json_object_object_get_ex(platform_obj, "architecture", 
&arch_obj) &&
+                   json_object_object_get_ex(platform_obj, "os", &os_obj) &&
+                   json_object_object_get_ex(manifest, "digest", &digest_obj)) 
{
+
+                       const char *arch = json_object_get_string(arch_obj);
+                       const char *os = json_object_get_string(os_obj);
+                       char manifest_platform[64];
+
+                       snprintf(manifest_platform, sizeof(manifest_platform), 
"%s/%s", os, arch);
+                       if (!strcmp(manifest_platform, platform)) {
+                               digest = 
strdup(json_object_get_string(digest_obj));
+                               break;
+                       }
+               }
+       }
+
+       if (!digest)
+               ret = -ENOENT;
+
+out_json:
+       json_object_put(root);
+out:
+       free(resp.data);
+       if (req.headers)
+               curl_slist_free_all(req.headers);
+       
+       return ret ? ERR_PTR(ret) : digest;
+}
+
+/* Get all layer information from manifest */
+static struct oci_layer *oci_get_layers_info(const char *registry, const char 
*repository,
+                                             const char *digest, const char 
*auth_header,
+                                             int *layer_count)
+{
+       struct oci_request req = {};
+       struct oci_response resp = {};
+       json_object *root, *layers, *layer, *digest_obj, *size_obj, 
*media_type_obj;
+       struct oci_layer *layers_info = NULL;
+       const char *api_registry;
+       int ret, len, i;
+
+       if (!registry || !repository || !digest || !layer_count)
+               return ERR_PTR(-EINVAL);
+
+       *layer_count = 0;
+       api_registry = (!strcmp(registry, DOCKER_REGISTRY)) ? 
DOCKER_API_REGISTRY : registry;
+
+       snprintf(req.url, sizeof(req.url), "https://%s/v2/%s/manifests/%s";,
+                api_registry, repository, digest);
+
+       if (auth_header && strstr(auth_header, "Bearer"))
+               req.headers = curl_slist_append(req.headers, auth_header);
+               
+       req.headers = curl_slist_append(req.headers, 
+               "Accept: " OCI_MEDIATYPE_MANIFEST "," 
DOCKER_MEDIATYPE_MANIFEST_V2);
+
+       ret = oci_request_perform(&req, &resp);
+       if (ret)
+               goto out;
+
+       root = json_tokener_parse(resp.data);
+       if (!root) {
+               erofs_err("failed to parse manifest JSON");
+               ret = -EINVAL;
+               goto out;
+       }
+
+       if (!json_object_object_get_ex(root, "layers", &layers) ||
+           json_object_get_type(layers) != json_type_array) {
+               erofs_err("no layers found in manifest");
+               ret = -EINVAL;
+               goto out_json;
+       }
+
+       len = json_object_array_length(layers);
+       if (len == 0) {
+               erofs_err("empty layer list in manifest");
+               ret = -EINVAL;
+               goto out_json;
+       }
+
+       layers_info = calloc(len, sizeof(*layers_info));
+       if (!layers_info) {
+               ret = -ENOMEM;
+               goto out_json;
+       }
+
+       for (i = 0; i < len; i++) {
+               layer = json_object_array_get_idx(layers, i);
+               
+               if (!json_object_object_get_ex(layer, "digest", &digest_obj) ||
+                   !json_object_object_get_ex(layer, "size", &size_obj)) {
+                       erofs_err("failed to parse layer %d", i);
+                       ret = -EINVAL;
+                       goto out_free;
+               }
+                       
+               layers_info[i].digest = 
strdup(json_object_get_string(digest_obj));
+               if (!layers_info[i].digest) {
+                       ret = -ENOMEM;
+                       goto out_free;
+               }
+               
+               layers_info[i].size = json_object_get_int64(size_obj);
+
+               if (json_object_object_get_ex(layer, "mediaType", 
&media_type_obj))
+                       layers_info[i].media_type = 
json_object_get_string(media_type_obj);
+       }
+
+       *layer_count = len;
+       json_object_put(root);
+       free(resp.data);
+       if (req.headers)
+               curl_slist_free_all(req.headers);
+       return layers_info;
+
+out_free:
+       for (int j = 0; j < i; j++)
+               free(layers_info[j].digest);
+       free(layers_info);
+out_json:
+       json_object_put(root);
+out:
+       free(resp.data);
+       if (req.headers)
+               curl_slist_free_all(req.headers);
+       return ERR_PTR(ret);
+}
+
+/* Download and extract a single layer */
+static int oci_extract_layer(struct erofs_oci *oci, struct erofs_inode *root,
+                            const char *layer_digest, const char *auth_header,
+                            int layer_index)
+{
+       struct oci_request req = {};
+       struct oci_stream stream = {};
+       const char *api_registry;
+       int ret, fd;
+
+       if (!oci || !root || !layer_digest || layer_index < 0)
+               return -EINVAL;
+
+       snprintf(stream.temp_filename, sizeof(stream.temp_filename), 
+                "/tmp/oci_layer_%d_%d.tar.gz", getpid(), layer_index);
+       stream.temp_file = fopen(stream.temp_filename, "wb");
+       if (!stream.temp_file) {
+               erofs_err("failed to create temp file: %s", 
stream.temp_filename);
+               return -errno;
+       }
+       stream.layer_index = layer_index;
+
+       api_registry = (!strcmp(oci->registry, DOCKER_REGISTRY)) ? 
DOCKER_API_REGISTRY : oci->registry;
+
+       snprintf(req.url, sizeof(req.url), "https://%s/v2/%s/blobs/%s";,
+                api_registry, oci->repository, layer_digest);
+       
+       if (auth_header && strstr(auth_header, "Bearer"))
+               req.headers = curl_slist_append(req.headers, auth_header);
+       
+       curl_easy_setopt(g_curl, CURLOPT_URL, req.url);
+       curl_easy_setopt(g_curl, CURLOPT_WRITEFUNCTION, 
oci_layer_write_callback);
+       curl_easy_setopt(g_curl, CURLOPT_WRITEDATA, &stream);
+       
+       if (req.headers)
+               curl_easy_setopt(g_curl, CURLOPT_HTTPHEADER, req.headers);
+
+       ret = curl_easy_perform(g_curl);
+       fclose(stream.temp_file);
+       stream.temp_file = NULL;
+
+       if (ret != CURLE_OK) {
+               erofs_err("failed to download layer: %s", 
curl_easy_strerror(ret));
+               ret = -EIO;
+               goto out;
+       }
+
+       fd = open(stream.temp_filename, O_RDONLY);
+       if (fd < 0) {
+               erofs_err("failed to open downloaded layer file");
+               ret = -errno;
+               goto out;
+       }
+
+       memset(&stream.tarfile, 0, sizeof(stream.tarfile));
+       init_list_head(&stream.tarfile.global.xattrs);
+       
+       ret = erofs_iostream_open(&stream.tarfile.ios, fd, 
EROFS_IOS_DECODER_GZIP);
+       if (ret) {
+               erofs_err("failed to initialize tar stream: %s", 
erofs_strerror(ret));
+               close(fd);
+               goto out;
+       }
+
+       while (!(ret = tarerofs_parse_tar(root, &stream.tarfile))) {
+               /* Continue parsing until end of archive */
+       }
+       
+       erofs_iostream_close(&stream.tarfile.ios);
+       
+       if (ret < 0 && ret != -ENODATA) {
+               erofs_err("failed to process tar stream: %s", 
erofs_strerror(ret));
+               goto out;
+       }
+
+       ret = 0;
+
+out:
+       if (req.headers)
+               curl_slist_free_all(req.headers);
+       
+       if (unlink(stream.temp_filename) && errno != ENOENT)
+               erofs_warn("failed to remove temp file: %s", 
stream.temp_filename);
+               
+       return ret;
+}
+
+/* Initialize global CURL handle */
+static int oci_global_init(void)
+{
+       if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK)
+               return -EIO;
+
+       g_curl = curl_easy_init();
+       if (!g_curl) {
+               curl_global_cleanup();
+               return -EIO;
+       }
+
+       curl_easy_setopt(g_curl, CURLOPT_FOLLOWLOCATION, 1L);
+       curl_easy_setopt(g_curl, CURLOPT_CONNECTTIMEOUT, 30L);
+       curl_easy_setopt(g_curl, CURLOPT_TIMEOUT, 120L);
+       curl_easy_setopt(g_curl, CURLOPT_NOSIGNAL, 1L);
+       curl_easy_setopt(g_curl, CURLOPT_USERAGENT, "ocierofs/" 
PACKAGE_VERSION);
+
+       return 0;
+}
+
+/* Cleanup global CURL handle */
+static void oci_global_exit(void)
+{
+       if (g_curl) {
+               curl_easy_cleanup(g_curl);
+               g_curl = NULL;
+       }
+       curl_global_cleanup();
+}
+
+/* Main function to build trees from OCI image */
+int ocierofs_build_trees(struct erofs_inode *root, struct erofs_oci *oci,
+                        const char *ref, bool fillzero)
+{
+       char *auth_header = NULL;
+       char *manifest_digest = NULL;
+       struct oci_layer *layers_info = NULL;
+       int layer_count = 0;
+       int ret, i;
+
+       if (!root || !oci)
+               return -EINVAL;
+
+       ret = oci_global_init();
+       if (ret) {
+               erofs_err("failed to initialize OCI client: %s", 
erofs_strerror(ret));
+               return ret;
+       }
+
+       if (!oci->anonymous_access) {
+               if (oci->username[0] && oci->password[0]) {
+                       auth_header = oci_get_auth_token(oci->registry, 
oci->repository, 
+                                                        oci->username, 
oci->password);
+                       if (IS_ERR(auth_header)) {
+                               erofs_info("token auth failed, trying basic 
auth");
+                               auth_header = NULL;
+                               char *userpwd = malloc(strlen(oci->username) + 
strlen(oci->password) + 2);
+                               if (!userpwd) {
+                                       ret = -ENOMEM;
+                                       goto out;
+                               }
+                               sprintf(userpwd, "%s:%s", oci->username, 
oci->password);
+                               curl_easy_setopt(g_curl, CURLOPT_USERPWD, 
userpwd);
+                               curl_easy_setopt(g_curl, CURLOPT_HTTPAUTH, 
CURLAUTH_BASIC);
+                               free(userpwd);
+                       }
+               } else {
+                       auth_header = oci_get_auth_token(oci->registry, 
oci->repository, NULL, NULL);
+                       if (IS_ERR(auth_header)) {
+                               ret = PTR_ERR(auth_header);
+                               erofs_err("failed to get auth token: %s", 
erofs_strerror(ret));
+                               goto out;
+                       }
+               }
+       }
+       
+       manifest_digest = oci_get_manifest_digest(oci->registry, 
oci->repository,
+                                                 oci->tag, oci->platform, 
auth_header);
+       if (IS_ERR(manifest_digest)) {
+               ret = PTR_ERR(manifest_digest);
+               erofs_err("failed to get manifest digest: %s", 
erofs_strerror(ret));
+               goto out_auth;
+       }
+
+       layers_info = oci_get_layers_info(oci->registry, oci->repository,
+                                         manifest_digest, auth_header, 
&layer_count);
+       if (IS_ERR(layers_info)) {
+               ret = PTR_ERR(layers_info);
+               erofs_err("failed to get layers info: %s", erofs_strerror(ret));
+               goto out_manifest;
+       }
+
+       /* Check if we should extract all layers or just a specific one */
+       if (oci->layer_index >= 0) {
+               if (oci->layer_index >= layer_count) {
+                       erofs_err("layer index %d exceeds available layers 
(%d)", 
+                                 oci->layer_index, layer_count);
+                       ret = -EINVAL;
+                       goto out_layers;
+               }
+               
+               /* Extract only the specified layer */
+               i = oci->layer_index;
+               char *trimmed = 
erofs_trim_for_progressinfo(layers_info[i].digest,
+                               sizeof("Extracting layer  ...") - 1);
+               erofs_update_progressinfo("Extracting layer %d: %s ...", i, 
trimmed);
+               free(trimmed);
+
+               if (!fillzero) {
+                       ret = oci_extract_layer(oci, root, 
layers_info[i].digest, auth_header, i);
+                       if (ret) {
+                               erofs_err("failed to extract layer %d: %s", i, 
erofs_strerror(ret));
+                               goto out_layers;
+                       }
+               }
+       } else {
+               /* Extract all layers */
+               for (i = 0; i < layer_count; i++) {
+                       char *trimmed = 
erofs_trim_for_progressinfo(layers_info[i].digest,
+                                       sizeof("Extracting layer  ...") - 1);
+                       erofs_update_progressinfo("Extracting layer %s ...", 
trimmed);
+                       free(trimmed);
+
+                       if (fillzero)
+                               continue;
+
+                       ret = oci_extract_layer(oci, root, 
layers_info[i].digest, auth_header, i);
+                       if (ret) {
+                               erofs_err("failed to extract layer %d: %s", i, 
erofs_strerror(ret));
+                               goto out_layers;
+                       }
+               }
+       }
+
+       ret = 0;
+
+out_layers:
+       for (i = 0; i < layer_count; i++)
+               free(layers_info[i].digest);
+       free(layers_info);
+out_manifest:
+       free(manifest_digest);
+out_auth:
+       free(auth_header);
+       
+       if (oci->username[0] && oci->password[0] && !auth_header) {
+               curl_easy_setopt(g_curl, CURLOPT_USERPWD, NULL);
+               curl_easy_setopt(g_curl, CURLOPT_HTTPAUTH, CURLAUTH_NONE);
+       }
+       
+out:
+       oci_global_exit();
+       return ret;
+}
diff --git a/mkfs/main.c b/mkfs/main.c
index 804d483..f2b9958 100644
--- a/mkfs/main.c
+++ b/mkfs/main.c
@@ -32,6 +32,7 @@
 #include "../lib/liberofs_uuid.h"
 #include "../lib/liberofs_metabox.h"
 #include "../lib/liberofs_s3.h"
+#include "../lib/liberofs_oci.h"
 #include "../lib/compressor.h"
 
 static struct option long_options[] = {
@@ -95,6 +96,9 @@ static struct option long_options[] = {
        {"vmdk-desc", required_argument, NULL, 532},
 #ifdef S3EROFS_ENABLED
        {"s3", required_argument, NULL, 533},
+#endif
+#ifdef OCIEROFS_ENABLED
+       {"oci", required_argument, NULL, 534},
 #endif
        {0, 0, 0, 0},
 };
@@ -206,6 +210,14 @@ static void usage(int argc, char **argv)
                "   [,passwd_file=Y]    X=endpoint, Y=s3fs-compatible password 
file\n"
                "   [,urlstyle=Z]       S3 API calling style (Z = vhost|path) 
(default: vhost)\n"
                "   [,sig=<2,4>]        S3 API signature version (default: 2)\n"
+#endif
+#ifdef OCIEROFS_ENABLED
+               " --oci=X               generate an image from OCI-compatible 
registry\n"
+               "   [,platform=Y]       X=registry/repo:tag, Y=platform 
(default: linux/amd64)\n"
+               "   [,layer=Z]          Z=layer index to extract (default: 0)\n"
+               "   [,anonymous]        use anonymous access (no 
authentication)\n"
+               "   [,username=U]       U=username for basic authentication\n"
+               "   [,password=P]       P=password for basic authentication\n"
 #endif
                " --tar=X               generate a full or index-only image 
from a tarball(-ish) source\n"
                "                       (X = f|i|headerball; f=full mode, 
i=index mode,\n"
@@ -261,6 +273,10 @@ static u8 metabox_algorithmid;
 static struct erofs_s3 s3cfg;
 #endif
 
+#ifdef OCIEROFS_ENABLED
+static struct erofs_oci ocicfg;
+#endif
+
 enum {
        EROFS_MKFS_DATA_IMPORT_DEFAULT,
        EROFS_MKFS_DATA_IMPORT_FULLDATA,
@@ -272,6 +288,7 @@ static enum {
        EROFS_MKFS_SOURCE_LOCALDIR,
        EROFS_MKFS_SOURCE_TAR,
        EROFS_MKFS_SOURCE_S3,
+       EROFS_MKFS_SOURCE_OCI,
        EROFS_MKFS_SOURCE_REBUILD,
 } source_mode;
 
@@ -668,6 +685,112 @@ static int mkfs_parse_s3_cfg(char *cfg_str)
 }
 #endif
 
+#ifdef OCIEROFS_ENABLED
+static int mkfs_parse_oci_cfg(char *cfg_str)
+{
+       char *p, *q, *opt, *ref_str;
+       char *slash, *colon, *dot;
+       const char *repo_part;
+       size_t len;
+
+       if (source_mode != EROFS_MKFS_SOURCE_LOCALDIR)
+               return -EINVAL;
+       source_mode = EROFS_MKFS_SOURCE_OCI;
+
+       if (!cfg_str) {
+               erofs_err("oci: missing parameter");
+               return -EINVAL;
+       }
+
+       memset(&ocicfg, 0, sizeof(ocicfg));
+       strcpy(ocicfg.platform, "linux/amd64");
+       strcpy(ocicfg.registry, "registry-1.docker.io");
+       strcpy(ocicfg.tag, "latest");
+       ocicfg.layer_index = -1;  /* -1 means extract all layers */
+       ocicfg.auth_mode = OCI_AUTH_TOKEN;
+
+       p = strchr(cfg_str, ',');
+       ref_str = p ? strndup(cfg_str, p - cfg_str) : strdup(cfg_str);
+       if (!ref_str)
+               return -ENOMEM;
+
+       slash = strchr(ref_str, '/');
+       if (slash) {
+               dot = strchr(ref_str, '.');
+               if (dot && dot < slash) {
+                       len = slash - ref_str;
+                       if (len >= sizeof(ocicfg.registry)) {
+                               erofs_err("registry name too long");
+                               goto err_free;
+                       }
+                       strncpy(ocicfg.registry, ref_str, len);
+                       ocicfg.registry[len] = '\0';
+                       repo_part = slash + 1;
+               } else {
+                       repo_part = ref_str;
+               }
+       } else {
+               repo_part = ref_str;
+       }
+
+       colon = strchr(repo_part, ':');
+       if (colon) {
+               len = colon - repo_part;
+               if (len >= sizeof(ocicfg.repository)) {
+                       erofs_err("repository name too long");
+                       goto err_free;
+               }
+               strncpy(ocicfg.repository, repo_part, len);
+               ocicfg.repository[len] = '\0';
+               strncpy(ocicfg.tag, colon + 1, sizeof(ocicfg.tag) - 1);
+       } else {
+               strncpy(ocicfg.repository, repo_part, sizeof(ocicfg.repository) 
- 1);
+       }
+
+       free(ref_str);
+
+       if (!p)
+               return 0;
+
+       opt = p + 1;
+       while (opt) {
+               q = strchr(opt, ',');
+               if (q)
+                       *q = '\0';
+
+               if ((p = strstr(opt, "platform="))) {
+                       strncpy(ocicfg.platform, p + 9, sizeof(ocicfg.platform) 
- 1);
+               } else if ((p = strstr(opt, "layer="))) {
+                       ocicfg.layer_index = atoi(p + 6);
+                       if (ocicfg.layer_index < 0) {
+                               erofs_err("invalid layer index %d", 
ocicfg.layer_index);
+                               return -EINVAL;
+                       }
+               } else if (!strcmp(opt, "anonymous")) {
+                       ocicfg.anonymous_access = true;
+                       ocicfg.auth_mode = OCI_AUTH_ANONYMOUS;
+               } else if ((p = strstr(opt, "username="))) {
+                       strncpy(ocicfg.username, p + 9, sizeof(ocicfg.username) 
- 1);
+                       ocicfg.auth_mode = OCI_AUTH_BASIC;
+               } else if ((p = strstr(opt, "password="))) {
+                       strncpy(ocicfg.password, p + 9, sizeof(ocicfg.password) 
- 1);
+                       ocicfg.auth_mode = OCI_AUTH_BASIC;
+               } else {
+                       erofs_err("invalid --oci option %s", opt);
+                       return -EINVAL;
+               }
+
+               opt = q ? q + 1 : NULL;
+       }
+
+       return 0;
+
+err_free:
+       free(ref_str);
+       return -EINVAL;
+}
+#endif
+
 static int mkfs_parse_one_compress_alg(char *alg,
                                       struct erofs_compr_opts *copts)
 {
@@ -822,6 +945,13 @@ static int mkfs_parse_sources(int argc, char *argv[], int 
optind)
                if (!cfg.c_src_path)
                        return -ENOMEM;
                break;
+#endif
+#ifdef OCIEROFS_ENABLED
+       case EROFS_MKFS_SOURCE_OCI:
+               cfg.c_src_path = strdup(argv[optind++]);
+               if (!cfg.c_src_path)
+                       return -ENOMEM;
+               break;
 #endif
        default:
                erofs_err("unexpected source_mode: %d", source_mode);
@@ -1219,6 +1349,13 @@ static int mkfs_parse_options_cfg(int argc, char *argv[])
                        if (err)
                                return err;
                        break;
+#endif
+#ifdef OCIEROFS_ENABLED
+               case 534:
+                       err = mkfs_parse_oci_cfg(optarg);
+                       if (err)
+                               return err;
+                       break;
 #endif
                case 'V':
                        version();
@@ -1638,7 +1775,8 @@ int main(int argc, char **argv)
                erofs_uuid_generate(g_sbi.uuid);
 
        if ((source_mode == EROFS_MKFS_SOURCE_TAR && !erofstar.index_mode) ||
-           (source_mode == EROFS_MKFS_SOURCE_S3)) {
+           (source_mode == EROFS_MKFS_SOURCE_S3) ||
+           (source_mode == EROFS_MKFS_SOURCE_OCI)) {
                err = erofs_diskbuf_init(1);
                if (err) {
                        erofs_err("failed to initialize diskbuf: %s",
@@ -1756,12 +1894,27 @@ int main(int argc, char **argv)
                                        dataimport_mode == 
EROFS_MKFS_DATA_IMPORT_ZEROFILL);
                        if (err)
                                goto exit;
+#endif
+#ifdef OCIEROFS_ENABLED
+               } else if (source_mode == EROFS_MKFS_SOURCE_OCI) {
+                       if (incremental_mode ||
+                           dataimport_mode == EROFS_MKFS_DATA_IMPORT_RVSP)
+                               err = -EOPNOTSUPP;
+                       else
+                               err = ocierofs_build_trees(root, &ocicfg,
+                                                          cfg.c_src_path,
+                                       dataimport_mode == 
EROFS_MKFS_DATA_IMPORT_ZEROFILL);
+                       if (err)
+                               goto exit;
+                       erofs_info("OCI build_trees completed, starting 
filesystem construction");
 #endif
                }
 
+               erofs_info("Starting erofs_rebuild_dump_tree...");
                err = erofs_rebuild_dump_tree(root, incremental_mode);
                if (err)
                        goto exit;
+               erofs_info("erofs_rebuild_dump_tree completed");
        }
 
        if (tar_index_512b) {
-- 
2.47.1


Reply via email to