From 2bf94fc85bf9aa6dbd70c064af75bdff8a56eb87 Mon Sep 17 00:00:00 2001 From: Elizabeth Figura Date: Thu, 13 Nov 2025 15:54:44 -0600 Subject: [PATCH] Rebase against 76e7e90c3679766ac327b138e3269fb61ead6e4a. --- ...t-for-testing-for-reparse-points-wit.patch | 55 ---- ...ement-FILE_OPEN_REPARSE_POINT-option.patch | 288 ------------------ ...port-for-FileAttributeTagInformation.patch | 39 --- ...opening-files-through-nt-device-path.patch | 26 +- staging/upstream-commit | 2 +- 5 files changed, 14 insertions(+), 396 deletions(-) delete mode 100644 patches/ntdll-Junction_Points/0004-ntdll-Add-support-for-testing-for-reparse-points-wit.patch delete mode 100644 patches/ntdll-Junction_Points/0005-server-Implement-FILE_OPEN_REPARSE_POINT-option.patch delete mode 100644 patches/ntdll-Junction_Points/0018-ntdll-Add-support-for-FileAttributeTagInformation.patch diff --git a/patches/ntdll-Junction_Points/0004-ntdll-Add-support-for-testing-for-reparse-points-wit.patch b/patches/ntdll-Junction_Points/0004-ntdll-Add-support-for-testing-for-reparse-points-wit.patch deleted file mode 100644 index 540ea059..00000000 --- a/patches/ntdll-Junction_Points/0004-ntdll-Add-support-for-testing-for-reparse-points-wit.patch +++ /dev/null @@ -1,55 +0,0 @@ -From a093cbbe7bd8ea6b1e5d48a552f2712dc2915f2c Mon Sep 17 00:00:00 2001 -From: "Erich E. Hoover" -Date: Thu, 16 Jan 2014 21:01:25 -0700 -Subject: ntdll: Add support for testing for reparse points with - GetFileAttributes. - -Signed-off-by: Erich E. Hoover ---- - dlls/ntdll/unix/file.c | 23 +++++++++++++++++++---- - 2 files changed, 24 insertions(+), 4 deletions(-) - -diff --git a/dlls/ntdll/unix/file.c b/dlls/ntdll/unix/file.c -index 4c221f53bfd..948074b35ec 100644 ---- a/dlls/ntdll/unix/file.c -+++ b/dlls/ntdll/unix/file.c -@@ -1755,6 +1755,16 @@ static int fd_get_file_info( int fd, unsigned int options, struct stat *st, ULON - *attr = 0; - ret = fstat( fd, st ); - if (ret == -1) return ret; -+ if (S_ISLNK( st->st_mode )) -+ { -+ BOOL is_dir; -+ -+ /* symbolic links (either junction points or NT symlinks) are "reparse points" */ -+ *attr |= FILE_ATTRIBUTE_REPARSE_POINT; -+ /* whether a reparse point is a file or a directory is stored inside the link target */ -+ if (is_reparse_dir( fd, "", &is_dir ) == 0) -+ st->st_mode = (st->st_mode & ~S_IFMT) | (is_dir ? S_IFDIR : S_IFREG); -+ } - *attr |= get_file_attributes( st ); - /* consider mount points to be reparse points (IO_REPARSE_TAG_MOUNT_POINT) */ - if ((options & FILE_OPEN_REPARSE_POINT) && fd_is_mount_point( fd, st )) -@@ -1828,10 +1838,15 @@ static int get_file_info( const char *path, struct stat *st, ULONG *attr ) - if (ret == -1) return ret; - if (S_ISLNK( st->st_mode )) - { -- ret = stat( path, st ); -- if (ret == -1) return ret; -- /* is a symbolic link and a directory, consider these "reparse points" */ -- if (S_ISDIR( st->st_mode )) *attr |= FILE_ATTRIBUTE_REPARSE_POINT; -+ BOOL is_dir; -+ -+ /* return information about the destination (unless this is a dangling symlink) */ -+ stat( path, st ); -+ /* symbolic links (either junction points or NT symlinks) are "reparse points" */ -+ *attr |= FILE_ATTRIBUTE_REPARSE_POINT; -+ /* whether a reparse point is a file or a directory is stored inside the link target */ -+ if (is_reparse_dir( AT_FDCWD, path, &is_dir ) == 0) -+ st->st_mode = (st->st_mode & ~S_IFMT) | (is_dir ? S_IFDIR : S_IFREG); - } - else if (S_ISDIR( st->st_mode ) && (parent_path = malloc( strlen(path) + 4 ))) - { --- -2.17.1 - diff --git a/patches/ntdll-Junction_Points/0005-server-Implement-FILE_OPEN_REPARSE_POINT-option.patch b/patches/ntdll-Junction_Points/0005-server-Implement-FILE_OPEN_REPARSE_POINT-option.patch deleted file mode 100644 index 7eb38e0b..00000000 --- a/patches/ntdll-Junction_Points/0005-server-Implement-FILE_OPEN_REPARSE_POINT-option.patch +++ /dev/null @@ -1,288 +0,0 @@ -From 742f9b17c321b671e6b7bcd3bc7772e3ef66729d Mon Sep 17 00:00:00 2001 -From: "Erich E. Hoover" -Date: Thu, 16 Jan 2014 21:02:11 -0700 -Subject: [PATCH] server: Implement FILE_OPEN_REPARSE_POINT option. - -Signed-off-by: Erich E. Hoover ---- - dlls/kernelbase/file.c | 2 + - server/fd.c | 147 +++++++++++++++++++++++++++++++++++++++-- - 2 files changed, 142 insertions(+), 7 deletions(-) - -diff --git a/dlls/kernelbase/file.c b/dlls/kernelbase/file.c -index 249f476eb7e..2ea3d81a264 100644 ---- a/dlls/kernelbase/file.c -+++ b/dlls/kernelbase/file.c -@@ -769,6 +769,8 @@ static UINT get_nt_file_options( DWORD attributes ) - options |= FILE_SEQUENTIAL_ONLY; - if (attributes & FILE_FLAG_WRITE_THROUGH) - options |= FILE_WRITE_THROUGH; -+ if (attributes & FILE_FLAG_OPEN_REPARSE_POINT) -+ options |= FILE_OPEN_REPARSE_POINT; - return options; - } - -diff --git a/server/fd.c b/server/fd.c -index c82ed49034e..cd4b35915d4 100644 ---- a/server/fd.c -+++ b/server/fd.c -@@ -32,6 +32,7 @@ - #include - #include - #include -+#include - #include - #ifdef HAVE_LINUX_MAJOR_H - #include -@@ -99,6 +100,10 @@ - #include "winioctl.h" - #include "ddk/wdm.h" - -+#if !defined(O_SYMLINK) && defined(O_PATH) -+# define O_SYMLINK (O_NOFOLLOW | O_PATH) -+#endif -+ - #if defined(HAVE_SYS_EPOLL_H) && defined(HAVE_EPOLL_CREATE) - # include - # define USE_EPOLL -@@ -1064,6 +1069,9 @@ static void device_destroy( struct object *obj ) - list_remove( &device->entry ); /* remove it from the hash table */ - } - -+static int is_reparse_dir( const char *path, int *is_dir ); -+static int rmdir_recursive( int dir_fd, const char *pathname ); -+ - /****************************************************************/ - /* inode functions */ - -@@ -1071,10 +1079,29 @@ static void unlink_closed_fd( struct inode *inode, struct closed_fd *fd ) - { - /* make sure it is still the same file */ - struct stat st; -- if (!stat( fd->unix_name, &st ) && st.st_dev == inode->device->dev && st.st_ino == inode->ino) -+ if (!lstat( fd->unix_name, &st ) && st.st_dev == inode->device->dev && st.st_ino == inode->ino) - { -+ int is_reparse_point = (is_reparse_dir( fd->unix_name, NULL ) == 0); - if (S_ISDIR(st.st_mode)) rmdir( fd->unix_name ); - else unlink( fd->unix_name ); -+ /* remove reparse point metadata (if applicable) */ -+ if (is_reparse_point) -+ { -+ char tmp[PATH_MAX], metadata_path[PATH_MAX], *p; -+ -+ strcpy( tmp, fd->unix_name ); -+ p = dirname( tmp ); -+ if (p != tmp ) strcpy( tmp, p ); -+ strcpy( metadata_path, tmp ); -+ strcat( metadata_path, "/.REPARSE_POINT/" ); -+ strcpy( tmp, fd->unix_name ); -+ p = basename( tmp ); -+ if (p != tmp) strcpy( tmp, p ); -+ strcat( metadata_path, tmp ); -+ -+ rmdir_recursive( AT_FDCWD, metadata_path ); -+ rmdir( dirname( metadata_path ) ); -+ } - } - } - -@@ -1113,6 +1140,59 @@ static void inode_dump( struct object *obj, int verbose ) - fprintf( stderr, "\n" ); - } - -+/* recursively delete everything in a directory */ -+static int rmdir_recursive( int dir_fd, const char *pathname ) -+{ -+ int ret = 0, tmp_fd; -+ struct dirent *p; -+ struct stat st; -+ DIR *d; -+ -+ tmp_fd = openat( dir_fd, pathname, O_DIRECTORY|O_RDONLY|O_NONBLOCK|O_CLOEXEC ); -+ d = fdopendir( tmp_fd ); -+ if (!d) -+ { -+ close( tmp_fd ); -+ return -1; -+ } -+ -+ while (!ret && (p = readdir( d ))) -+ { -+ if (!strcmp( p->d_name, "." ) || !strcmp( p->d_name, ".." )) -+ continue; -+ if (!fstatat( dirfd(d), p->d_name, &st, AT_SYMLINK_NOFOLLOW )) -+ { -+ if (S_ISDIR( st.st_mode )) -+ ret = rmdir_recursive( dirfd(d), p->d_name ); -+ else -+ ret = unlinkat( dirfd(d), p->d_name, 0 ); -+ } -+ } -+ closedir( d ); -+ return unlinkat( dir_fd, pathname, AT_REMOVEDIR ); -+} -+ -+/* determine whether a reparse point is meant to be a directory or a file */ -+static int is_reparse_dir( const char *path, int *is_dir ) -+{ -+ char link_path[PATH_MAX], *p; -+ int ret; -+ -+ if ((ret = readlink( path, link_path, sizeof(link_path) )) < 0) -+ return ret; -+ /* confirm that this file is a reparse point */ -+ if (strncmp( link_path, ".REPARSE_POINT/", 15) != 0) -+ return -1; -+ /* skip past the reparse point indicator and the filename */ -+ p = &link_path[15]; -+ if ((p = strchr( p, '/' )) == NULL) -+ return -1; -+ p++; -+ /* read the flag indicating whether this reparse point is a directory */ -+ if (is_dir) *is_dir = (*p == '.'); -+ return 0; -+} -+ - static void inode_destroy( struct object *obj ) - { - struct inode *inode = (struct inode *)obj; -@@ -1861,6 +1941,38 @@ void get_nt_name( struct fd *fd, struct unicode_str *name ) - name->len = fd->nt_namelen; - } - -+/* check whether a file is a symlink */ -+int check_symlink( char *name ) -+{ -+ struct stat st; -+ -+ lstat( name, &st ); -+ return S_ISLNK( st.st_mode ); -+} -+ -+/* if flags does not contain O_SYMLINK then just use realpath */ -+/* otherwise return the real path of the parent and append the filename of the symlink */ -+char *normalize_path( const char *path, int flags ) -+{ -+ char tmp[PATH_MAX], resolved_path[PATH_MAX], *p; -+ -+#if defined(O_SYMLINK) -+ if ((flags & O_SYMLINK) != O_SYMLINK) -+ return realpath( path, NULL ); -+#endif -+ -+ strcpy( tmp, path ); -+ p = dirname( tmp ); -+ if (p != tmp ) strcpy( tmp, p ); -+ realpath( tmp, resolved_path ); -+ strcat( resolved_path, "/" ); -+ strcpy( tmp, path ); -+ p = basename( tmp ); -+ if (p != tmp) strcpy( tmp, p ); -+ strcat( resolved_path, tmp ); -+ return strdup( resolved_path ); -+} -+ - /* open() wrapper that returns a struct fd with no fd user set */ - struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_name, - int flags, mode_t *mode, unsigned int access, -@@ -1921,6 +2033,15 @@ struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_nam - } - else rw_mode = O_RDONLY; - -+ if ((path = dup_fd_name( root, name ))) -+ { -+#if defined(O_SYMLINK) -+ if (check_symlink( path ) && (options & FILE_OPEN_REPARSE_POINT) && !(flags & O_CREAT)) -+ flags |= O_SYMLINK; -+#endif -+ free( path ); -+ } -+ - if ((fd->unix_fd = open( name, rw_mode | (flags & ~O_TRUNC), *mode )) == -1) - { - /* if we tried to open a directory for write access, retry read-only */ -@@ -1947,10 +2068,11 @@ struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_nam - *mode = st.st_mode; - - /* only bother with an inode for normal files and directories */ -- if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode)) -+ if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)) - { - unsigned int err; - struct inode *inode = get_inode( st.st_dev, st.st_ino, fd->unix_fd ); -+ int is_link = S_ISLNK(st.st_mode), is_dir; - - if (!inode) - { -@@ -1962,7 +2084,7 @@ struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_nam - - if ((path = dup_fd_name( root, name ))) - { -- fd->unix_name = realpath( path, NULL ); -+ fd->unix_name = normalize_path( path, flags ); - free( path ); - } - -@@ -1975,13 +2097,17 @@ struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_nam - list_add_head( &inode->open, &fd->inode_entry ); - closed_fd = NULL; - -+ is_dir = S_ISDIR(st.st_mode); -+ if (is_link) -+ is_reparse_dir(fd->unix_name, &is_dir); -+ - /* check directory options */ -- if ((options & FILE_DIRECTORY_FILE) && !S_ISDIR(st.st_mode)) -+ if ((options & FILE_DIRECTORY_FILE) && !is_dir) - { - set_error( STATUS_NOT_A_DIRECTORY ); - goto error; - } -- if ((options & FILE_NON_DIRECTORY_FILE) && S_ISDIR(st.st_mode)) -+ if ((options & FILE_NON_DIRECTORY_FILE) && is_dir) - { - set_error( STATUS_FILE_IS_A_DIRECTORY ); - goto error; -@@ -2428,6 +2554,7 @@ static struct fd *get_handle_fd_obj( struct process *process, obj_handle_t handl - - static int is_dir_empty( int fd ) - { -+ int dir_fd; - DIR *dir; - int empty; - struct dirent *de; -@@ -2435,8 +2562,13 @@ static int is_dir_empty( int fd ) - if ((fd = dup( fd )) == -1) - return -1; - -- if (!(dir = fdopendir( fd ))) -+ /* use openat() so that if 'fd' was opened with O_SYMLINK we can still check the contents */ -+ dir_fd = openat( fd, ".", O_RDONLY | O_DIRECTORY | O_NONBLOCK ); -+ if (dir_fd == -1) -+ return -1; -+ if (!(dir = fdopendir( dir_fd ))) - { -+ close( dir_fd ); - close( fd ); - return -1; - } -@@ -2448,6 +2580,7 @@ static int is_dir_empty( int fd ) - empty = 0; - } - closedir( dir ); -+ close( dir_fd ); - return empty; - } - -@@ -2486,7 +2619,7 @@ static void set_fd_disposition( struct fd *fd, unsigned int flags ) - file_set_error(); - return; - } -- if (S_ISREG( st.st_mode )) /* can't unlink files we don't have permission to write */ -+ if (S_ISREG( st.st_mode ) || S_ISLNK( st.st_mode )) /* can't unlink files we don't have permission to write */ - { - if (!(flags & FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE) && - !(st.st_mode & (S_IWUSR | S_IWGRP | S_IWOTH))) --- -2.45.2 - diff --git a/patches/ntdll-Junction_Points/0018-ntdll-Add-support-for-FileAttributeTagInformation.patch b/patches/ntdll-Junction_Points/0018-ntdll-Add-support-for-FileAttributeTagInformation.patch deleted file mode 100644 index 76f4940e..00000000 --- a/patches/ntdll-Junction_Points/0018-ntdll-Add-support-for-FileAttributeTagInformation.patch +++ /dev/null @@ -1,39 +0,0 @@ -From 76c4816f9015329c24968849f20063c07016a2da Mon Sep 17 00:00:00 2001 -From: "Erich E. Hoover" -Date: Wed, 25 Nov 2020 09:19:42 -0700 -Subject: ntdll: Add support for FileAttributeTagInformation. - -Signed-off-by: Erich E. Hoover ---- - dlls/ntdll/unix/file.c | 15 ++++++++++++++- - 2 files changed, 20 insertions(+), 1 deletion(-) - -diff --git a/dlls/ntdll/unix/file.c b/dlls/ntdll/unix/file.c -index bf80708b41e..51d6aed85f0 100644 ---- a/dlls/ntdll/unix/file.c -+++ b/dlls/ntdll/unix/file.c -@@ -5421,7 +5421,20 @@ NTSTATUS WINAPI NtQueryInformationFile( HANDLE handle, IO_STATUS_BLOCK *io, - { - FILE_ATTRIBUTE_TAG_INFORMATION *info = ptr; - info->FileAttributes = attr; -- info->ReparseTag = 0; /* FIXME */ -+ info->ReparseTag = 0; -+ if (attr & FILE_ATTRIBUTE_REPARSE_POINT) -+ { -+ REPARSE_DATA_BUFFER *buffer = NULL; -+ ULONG buffer_len = 0; -+ -+ if (get_reparse_point( handle, NULL, &buffer_len ) == STATUS_BUFFER_TOO_SMALL) -+ { -+ buffer = malloc( buffer_len ); -+ if (get_reparse_point( handle, buffer, &buffer_len ) == STATUS_SUCCESS) -+ info->ReparseTag = buffer->ReparseTag; -+ free( buffer ); -+ } -+ } - if ((options & FILE_OPEN_REPARSE_POINT) && fd_is_mount_point( fd, &st )) - info->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; - } --- -2.17.1 - diff --git a/patches/ntdll-NtDevicePath/0001-ntdll-Implement-opening-files-through-nt-device-path.patch b/patches/ntdll-NtDevicePath/0001-ntdll-Implement-opening-files-through-nt-device-path.patch index 55285dd9..5de33a99 100644 --- a/patches/ntdll-NtDevicePath/0001-ntdll-Implement-opening-files-through-nt-device-path.patch +++ b/patches/ntdll-NtDevicePath/0001-ntdll-Implement-opening-files-through-nt-device-path.patch @@ -1,4 +1,4 @@ -From 6bef707eb81bcdaf7a4f159f07852cbf092a3a2a Mon Sep 17 00:00:00 2001 +From a13f85bdd673a85b34dc6cb04c3f477a6935c278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20M=C3=BCller?= Date: Tue, 30 Nov 2021 16:32:34 +0300 Subject: [PATCH] ntdll: Implement opening files through nt device paths. @@ -9,7 +9,7 @@ Subject: [PATCH] ntdll: Implement opening files through nt device paths. 2 files changed, 154 insertions(+), 4 deletions(-) diff --git a/dlls/ntdll/tests/file.c b/dlls/ntdll/tests/file.c -index 7016ca166f9..362c9fcc640 100644 +index 7f8cfdadcd5..c0bb08eab39 100644 --- a/dlls/ntdll/tests/file.c +++ b/dlls/ntdll/tests/file.c @@ -139,18 +139,22 @@ static void WINAPI apc( void *arg, IO_STATUS_BLOCK *iosb, ULONG reserved ) @@ -63,10 +63,10 @@ index 7016ca166f9..362c9fcc640 100644 static void open_file_test(void) diff --git a/dlls/ntdll/unix/file.c b/dlls/ntdll/unix/file.c -index a6873bb2f79..91afff6e49c 100644 +index 3c59f2c63e9..534c92df826 100644 --- a/dlls/ntdll/unix/file.c +++ b/dlls/ntdll/unix/file.c -@@ -3721,7 +3721,7 @@ static NTSTATUS nt_to_unix_file_name_no_root( OBJECT_ATTRIBUTES *attr, UNICODE_S +@@ -3753,7 +3753,7 @@ static NTSTATUS nt_to_unix_file_name_no_root( OBJECT_ATTRIBUTES *attr, UNICODE_S /****************************************************************************** @@ -75,18 +75,18 @@ index a6873bb2f79..91afff6e49c 100644 * * Convert a file name from NT namespace to Unix namespace. * -@@ -3729,8 +3729,8 @@ static NTSTATUS nt_to_unix_file_name_no_root( OBJECT_ATTRIBUTES *attr, UNICODE_S +@@ -3761,8 +3761,8 @@ static NTSTATUS nt_to_unix_file_name_no_root( OBJECT_ATTRIBUTES *attr, UNICODE_S * element doesn't have to exist; in that case STATUS_NO_SUCH_FILE is * returned, but the unix name is still filled in properly. */ -static NTSTATUS nt_to_unix_file_name( OBJECT_ATTRIBUTES *attr, UNICODE_STRING *nt_name, -- char **name_ret, UINT disposition ) -+NTSTATUS nt_to_unix_file_name_internal( OBJECT_ATTRIBUTES *attr, UNICODE_STRING *nt_name, -+ char **name_ret, UINT disposition ) +- char **name_ret, UINT disposition, BOOL open_reparse ) ++static NTSTATUS nt_to_unix_file_name_internal( OBJECT_ATTRIBUTES *attr, UNICODE_STRING *nt_name, ++ char **name_ret, UINT disposition, BOOL open_reparse ) { enum server_fd_type type; int root_fd, needs_close; -@@ -3781,6 +3781,133 @@ static NTSTATUS nt_to_unix_file_name( OBJECT_ATTRIBUTES *attr, UNICODE_STRING *n +@@ -3813,6 +3813,133 @@ static NTSTATUS nt_to_unix_file_name( OBJECT_ATTRIBUTES *attr, UNICODE_STRING *n } @@ -165,7 +165,7 @@ index a6873bb2f79..91afff6e49c 100644 + * returned, but the unix name is still filled in properly. + */ +NTSTATUS nt_to_unix_file_name( OBJECT_ATTRIBUTES *attr, UNICODE_STRING *nt_name, -+ char **name_ret, UINT disposition ) ++ char **name_ret, UINT disposition, BOOL open_reparse ) +{ + static const WCHAR systemrootW[] = {'\\','S','y','s','t','e','m','R','o','o','t','\\',0}; + static const WCHAR dosprefixW[] = {'\\','?','?','\\'}; @@ -175,7 +175,7 @@ index a6873bb2f79..91afff6e49c 100644 + size_t offset, name_len; + NTSTATUS status; + -+ if (attr->RootDirectory) return nt_to_unix_file_name_internal( attr, nt_name, name_ret, disposition ); ++ if (attr->RootDirectory) return nt_to_unix_file_name_internal( attr, nt_name, name_ret, disposition, open_reparse ); + + nameW = attr->ObjectName; + @@ -194,7 +194,7 @@ index a6873bb2f79..91afff6e49c 100644 + prefix = user_shared_data->NtSystemRoot; + } + else -+ return nt_to_unix_file_name_internal( attr, nt_name, name_ret, disposition ); ++ return nt_to_unix_file_name_internal( attr, nt_name, name_ret, disposition, open_reparse ); + + name_len = sizeof(dosprefixW) + wcslen(prefix) * sizeof(WCHAR) + + sizeof(WCHAR) /* '\\' */ + nameW->Length - offset * sizeof(WCHAR) + sizeof(WCHAR); @@ -213,7 +213,7 @@ index a6873bb2f79..91afff6e49c 100644 + nt_name->Buffer = name; + nt_name->Length = wcslen( name ) * sizeof(WCHAR); + attr->ObjectName = nt_name; -+ return nt_to_unix_file_name_internal( attr, nt_name, name_ret, disposition ); ++ return nt_to_unix_file_name_internal( attr, nt_name, name_ret, disposition, open_reparse ); +} + + diff --git a/staging/upstream-commit b/staging/upstream-commit index 0d167cf2..28753d88 100644 --- a/staging/upstream-commit +++ b/staging/upstream-commit @@ -1 +1 @@ -9250ecc5a6a64c73aada0ea751815412f7f00410 +76e7e90c3679766ac327b138e3269fb61ead6e4a