You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2023/06/21 07:54:16 UTC

[incubator-opendal] branch main updated: feat(bindings/haskell): support stat (#2504)

This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new b50d7f070 feat(bindings/haskell): support stat (#2504)
b50d7f070 is described below

commit b50d7f070d0c4a8c6729cde6dd68df972dc442f3
Author: silver-ymz <yi...@gmail.com>
AuthorDate: Wed Jun 21 15:54:11 2023 +0800

    feat(bindings/haskell): support stat (#2504)
    
    * feat(bindings/haskell): support stat
    
    Signed-off-by: silver-ymz <yi...@gmail.com>
    
    * enhance tests for stat
    
    Signed-off-by: silver-ymz <yi...@gmail.com>
    
    * remove content range
    
    Signed-off-by: silver-ymz <yi...@gmail.com>
    
    * support nanoseconds for metadata
    
    Signed-off-by: silver-ymz <yi...@gmail.com>
    
    ---------
    
    Signed-off-by: silver-ymz <yi...@gmail.com>
---
 Cargo.lock                                  |   1 +
 bindings/haskell/Cargo.toml                 |   1 +
 bindings/haskell/haskell-src/OpenDAL.hs     |  90 ++++++++++++++++--
 bindings/haskell/haskell-src/OpenDAL/FFI.hs |  77 ++++++++++++---
 bindings/haskell/opendal-hs.cabal           |   3 +-
 bindings/haskell/src/lib.rs                 |  80 ++++++++--------
 bindings/haskell/src/types.rs               | 141 ++++++++++++++++++++++++++++
 bindings/haskell/test/BasicTest.hs          |  29 +++++-
 8 files changed, 364 insertions(+), 58 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index cfc298619..0d22a7b72 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2785,6 +2785,7 @@ dependencies = [
 name = "opendal-hs"
 version = "0.1.0"
 dependencies = [
+ "chrono",
  "opendal",
 ]
 
diff --git a/bindings/haskell/Cargo.toml b/bindings/haskell/Cargo.toml
index 12f320a12..d204eafb2 100644
--- a/bindings/haskell/Cargo.toml
+++ b/bindings/haskell/Cargo.toml
@@ -32,3 +32,4 @@ doc = false
 
 [dependencies]
 opendal.workspace = true
+chrono = "0.4"
\ No newline at end of file
diff --git a/bindings/haskell/haskell-src/OpenDAL.hs b/bindings/haskell/haskell-src/OpenDAL.hs
index 293215a36..121c2f1bb 100644
--- a/bindings/haskell/haskell-src/OpenDAL.hs
+++ b/bindings/haskell/haskell-src/OpenDAL.hs
@@ -20,6 +20,8 @@ module OpenDAL (
   Operator,
   OpenDALError (..),
   ErrorCode (..),
+  EntryMode (..),
+  Metadata (..),
   OpMonad,
   MonadOperation (..),
   runOp,
@@ -31,6 +33,7 @@ module OpenDAL (
   copyOpRaw,
   renameOpRaw,
   deleteOpRaw,
+  statOpRaw,
 ) where
 
 import Control.Monad.Except (ExceptT, MonadError, runExceptT, throwError)
@@ -39,9 +42,10 @@ import Data.ByteString (ByteString)
 import qualified Data.ByteString as BS
 import Data.HashMap.Strict (HashMap)
 import qualified Data.HashMap.Strict as HashMap
+import Data.Time (UTCTime, parseTimeM, zonedTimeToUTC)
+import Data.Time.Format (defaultTimeLocale)
 import Foreign
 import Foreign.C.String
-import Foreign.C.Types (CChar)
 import OpenDAL.FFI
 
 newtype Operator = Operator (ForeignPtr RawOperator)
@@ -63,6 +67,20 @@ data ErrorCode
 data OpenDALError = OpenDALError {errorCode :: ErrorCode, message :: String}
   deriving (Eq, Show)
 
+data EntryMode = File | Dir | Unknown deriving (Eq, Show)
+
+data Metadata = Metadata
+  { mMode :: EntryMode
+  , mCacheControl :: Maybe String
+  , mContentDisposition :: Maybe String
+  , mContentLength :: Integer
+  , mContentMD5 :: Maybe String
+  , mContentType :: Maybe String
+  , mETag :: Maybe String
+  , mLastModified :: Maybe UTCTime
+  }
+  deriving (Eq, Show)
+
 newtype OpMonad a = OpMonad (ReaderT Operator (ExceptT OpenDALError IO) a)
   deriving
     ( Functor
@@ -81,6 +99,7 @@ class (Monad m) => MonadOperation m where
   copyOp :: String -> String -> m ()
   renameOp :: String -> String -> m ()
   deleteOp :: String -> m ()
+  statOp :: String -> m Metadata
 
 instance MonadOperation OpMonad where
   readOp path = do
@@ -111,6 +130,12 @@ instance MonadOperation OpMonad where
     op <- ask
     result <- liftIO $ deleteOpRaw op path
     either throwError return result
+  statOp path = do
+    op <- ask
+    result <- liftIO $ statOpRaw op path
+    either throwError return result
+
+-- helper functions
 
 byteSliceToByteString :: ByteSlice -> IO ByteString
 byteSliceToByteString (ByteSlice bsDataPtr len) = BS.packCStringLen (bsDataPtr, fromIntegral len)
@@ -129,10 +154,48 @@ parseErrorCode 10 = RateLimited
 parseErrorCode 11 = IsSameFile
 parseErrorCode _ = FFIError
 
+parseEntryMode :: Int -> EntryMode
+parseEntryMode 0 = File
+parseEntryMode 1 = Dir
+parseEntryMode _ = Unknown
+
+parseCString :: CString -> IO (Maybe String)
+parseCString value | value == nullPtr = return Nothing
+parseCString value = do
+  value' <- peekCString value
+  free value
+  return $ Just value'
+
+parseTime :: String -> Maybe UTCTime
+parseTime time = zonedTimeToUTC <$> parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%Q%z" time
+
+parseFFIMetadata :: FFIMetadata -> IO Metadata
+parseFFIMetadata (FFIMetadata mode cacheControl contentDisposition contentLength contentMD5 contentType eTag lastModified) = do
+  let mode' = parseEntryMode $ fromIntegral mode
+  cacheControl' <- parseCString cacheControl
+  contentDisposition' <- parseCString contentDisposition
+  let contentLength' = toInteger contentLength
+  contentMD5' <- parseCString contentMD5
+  contentType' <- parseCString contentType
+  eTag' <- parseCString eTag
+  lastModified' <- (>>= parseTime) <$> parseCString lastModified
+  return $
+    Metadata
+      { mMode = mode'
+      , mCacheControl = cacheControl'
+      , mContentDisposition = contentDisposition'
+      , mContentLength = contentLength'
+      , mContentMD5 = contentMD5'
+      , mContentType = contentType'
+      , mETag = eTag'
+      , mLastModified = lastModified'
+      }
+
+-- Exported functions
+
 runOp :: Operator -> OpMonad a -> IO (Either OpenDALError a)
 runOp operator (OpMonad op) = runExceptT $ runReaderT op operator
 
--- | Create a new Operator.
 newOp :: String -> HashMap String String -> IO (Either OpenDALError Operator)
 newOp scheme hashMap = do
   let keysAndValues = HashMap.toList hashMap
@@ -148,7 +211,7 @@ newOp scheme hashMap = do
               ffiResult <- peek ffiResultPtr
               if ffiCode ffiResult == 0
                 then do
-                  op <- Operator <$> (newForeignPtr c_free_operator $ castPtr $ dataPtr ffiResult)
+                  op <- Operator <$> (newForeignPtr c_free_operator $ dataPtr ffiResult)
                   return $ Right op
                 else do
                   let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
@@ -163,7 +226,7 @@ readOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
       ffiResult <- peek ffiResultPtr
       if ffiCode ffiResult == 0
         then do
-          byteslice <- peek (castPtr $ dataPtr ffiResult)
+          byteslice <- peek $ dataPtr ffiResult
           byte <- byteSliceToByteString byteslice
           c_free_byteslice (bsData byteslice) (bsLen byteslice)
           return $ Right byte
@@ -194,8 +257,7 @@ isExistOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
       ffiResult <- peek ffiResultPtr
       if ffiCode ffiResult == 0
         then do
-          -- For Bool type, the memory layout is different between C and Haskell.
-          val <- peek ((castPtr $ dataPtr ffiResult) :: Ptr CChar)
+          val <- peek $ dataPtr ffiResult
           let isExist = val /= 0
           return $ Right isExist
         else do
@@ -252,6 +314,22 @@ deleteOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
       ffiResult <- peek ffiResultPtr
       if ffiCode ffiResult == 0
         then return $ Right ()
+        else do
+          let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
+          errMsg <- peekCString (errorMessage ffiResult)
+          return $ Left $ OpenDALError code errMsg
+
+statOpRaw :: Operator -> String -> IO (Either OpenDALError Metadata)
+statOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
+  withCString path $ \cPath ->
+    alloca $ \ffiResultPtr -> do
+      c_blocking_stat opptr cPath ffiResultPtr
+      ffiResult <- peek ffiResultPtr
+      if ffiCode ffiResult == 0
+        then do
+          ffimatadata <- peek $ dataPtr ffiResult
+          metadata <- parseFFIMetadata ffimatadata
+          return $ Right metadata
         else do
           let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
           errMsg <- peekCString (errorMessage ffiResult)
diff --git a/bindings/haskell/haskell-src/OpenDAL/FFI.hs b/bindings/haskell/haskell-src/OpenDAL/FFI.hs
index ac4d8df55..cc682e979 100644
--- a/bindings/haskell/haskell-src/OpenDAL/FFI.hs
+++ b/bindings/haskell/haskell-src/OpenDAL/FFI.hs
@@ -15,6 +15,7 @@
 -- specific language governing permissions and limitations
 -- under the License.
 {-# LANGUAGE ForeignFunctionInterface #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 
 module OpenDAL.FFI where
 
@@ -24,14 +25,14 @@ import Foreign.C.Types
 
 data RawOperator
 
-data FFIResult = FFIResult
+data FFIResult a = FFIResult
   { ffiCode :: CUInt
-  , dataPtr :: Ptr ()
+  , dataPtr :: Ptr a
   , errorMessage :: CString
   }
   deriving (Show)
 
-instance Storable FFIResult where
+instance Storable (FFIResult a) where
   sizeOf _ = sizeOf (undefined :: CSize) + sizeOf (undefined :: Ptr ()) + sizeOf (undefined :: CString)
   alignment _ = alignment (undefined :: CSize)
   peek ptr = do
@@ -74,15 +75,69 @@ instance Storable ByteSlice where
     dataOffset = 0
     lenOffset = sizeOf (undefined :: Ptr ())
 
+data FFIMetadata = FFIMetadata
+  { ffiMode :: CUInt
+  , ffiCacheControl :: CString
+  , ffiContentDisposition :: CString
+  , ffiContentLength :: CULong
+  , ffiContentMD5 :: CString
+  , ffiContentType :: CString
+  , ffiETag :: CString
+  , ffiLastModified :: CString
+  }
+  deriving (Show)
+
+instance Storable FFIMetadata where
+  sizeOf _ = sizeOf (undefined :: CSize) + sizeOf (undefined :: CString) * 6 + sizeOf (undefined :: CULong)
+  alignment _ = alignment (undefined :: CSize)
+  peek ptr = do
+    mode <- peekByteOff ptr modeOffset
+    cacheControl <- peekByteOff ptr cacheControlOffset
+    contentDisposition <- peekByteOff ptr contentDispositionOffset
+    contentLength <- peekByteOff ptr contentLengthOffset
+    contentMD5 <- peekByteOff ptr contentMD5Offset
+    contentType <- peekByteOff ptr contentTypeOffset
+    eTag <- peekByteOff ptr eTagOffset
+    lastModified <- peekByteOff ptr lastModifiedOffset
+    return $ FFIMetadata mode cacheControl contentDisposition contentLength contentMD5 contentType eTag lastModified
+   where
+    modeOffset = 0
+    cacheControlOffset = modeOffset + sizeOf (undefined :: CSize)
+    contentDispositionOffset = cacheControlOffset + sizeOf (undefined :: CString)
+    contentLengthOffset = contentDispositionOffset + sizeOf (undefined :: CString)
+    contentMD5Offset = contentLengthOffset + sizeOf (undefined :: CULong)
+    contentTypeOffset = contentMD5Offset + sizeOf (undefined :: CString)
+    eTagOffset = contentTypeOffset + sizeOf (undefined :: CString)
+    lastModifiedOffset = eTagOffset + sizeOf (undefined :: CString)
+  poke ptr (FFIMetadata mode cacheControl contentDisposition contentLength contentMD5 contentType eTag lastModified) = do
+    pokeByteOff ptr modeOffset mode
+    pokeByteOff ptr cacheControlOffset cacheControl
+    pokeByteOff ptr contentDispositionOffset contentDisposition
+    pokeByteOff ptr contentLengthOffset contentLength
+    pokeByteOff ptr contentMD5Offset contentMD5
+    pokeByteOff ptr contentTypeOffset contentType
+    pokeByteOff ptr eTagOffset eTag
+    pokeByteOff ptr lastModifiedOffset lastModified
+   where
+    modeOffset = 0
+    cacheControlOffset = modeOffset + sizeOf (undefined :: CSize)
+    contentDispositionOffset = cacheControlOffset + sizeOf (undefined :: CString)
+    contentLengthOffset = contentDispositionOffset + sizeOf (undefined :: CString)
+    contentMD5Offset = contentLengthOffset + sizeOf (undefined :: CULong)
+    contentTypeOffset = contentMD5Offset + sizeOf (undefined :: CString)
+    eTagOffset = contentTypeOffset + sizeOf (undefined :: CString)
+    lastModifiedOffset = eTagOffset + sizeOf (undefined :: CString)
+
 foreign import ccall "via_map_ffi"
   c_via_map_ffi ::
-    CString -> Ptr CString -> Ptr CString -> CSize -> Ptr FFIResult -> IO ()
+    CString -> Ptr CString -> Ptr CString -> CSize -> Ptr (FFIResult RawOperator) -> IO ()
 foreign import ccall "&free_operator" c_free_operator :: FunPtr (Ptr RawOperator -> IO ())
 foreign import ccall "free_byteslice" c_free_byteslice :: Ptr CChar -> CSize -> IO ()
-foreign import ccall "blocking_read" c_blocking_read :: Ptr RawOperator -> CString -> Ptr FFIResult -> IO ()
-foreign import ccall "blocking_write" c_blocking_write :: Ptr RawOperator -> CString -> Ptr CChar -> CSize -> Ptr FFIResult -> IO ()
-foreign import ccall "blocking_is_exist" c_blocking_is_exist :: Ptr RawOperator -> CString -> Ptr FFIResult -> IO ()
-foreign import ccall "blocking_create_dir" c_blocking_create_dir :: Ptr RawOperator -> CString -> Ptr FFIResult -> IO ()
-foreign import ccall "blocking_copy" c_blocking_copy :: Ptr RawOperator -> CString -> CString -> Ptr FFIResult -> IO ()
-foreign import ccall "blocking_rename" c_blocking_rename :: Ptr RawOperator -> CString -> CString -> Ptr FFIResult -> IO ()
-foreign import ccall "blocking_delete" c_blocking_delete :: Ptr RawOperator -> CString -> Ptr FFIResult -> IO ()
\ No newline at end of file
+foreign import ccall "blocking_read" c_blocking_read :: Ptr RawOperator -> CString -> Ptr (FFIResult ByteSlice) -> IO ()
+foreign import ccall "blocking_write" c_blocking_write :: Ptr RawOperator -> CString -> Ptr CChar -> CSize -> Ptr (FFIResult ()) -> IO ()
+foreign import ccall "blocking_is_exist" c_blocking_is_exist :: Ptr RawOperator -> CString -> Ptr (FFIResult CBool) -> IO ()
+foreign import ccall "blocking_create_dir" c_blocking_create_dir :: Ptr RawOperator -> CString -> Ptr (FFIResult ()) -> IO ()
+foreign import ccall "blocking_copy" c_blocking_copy :: Ptr RawOperator -> CString -> CString -> Ptr (FFIResult ()) -> IO ()
+foreign import ccall "blocking_rename" c_blocking_rename :: Ptr RawOperator -> CString -> CString -> Ptr (FFIResult ()) -> IO ()
+foreign import ccall "blocking_delete" c_blocking_delete :: Ptr RawOperator -> CString -> Ptr (FFIResult ()) -> IO ()
+foreign import ccall "blocking_stat" c_blocking_stat :: Ptr RawOperator -> CString -> Ptr (FFIResult FFIMetadata) -> IO ()
\ No newline at end of file
diff --git a/bindings/haskell/opendal-hs.cabal b/bindings/haskell/opendal-hs.cabal
index 25351697b..02049b4b2 100644
--- a/bindings/haskell/opendal-hs.cabal
+++ b/bindings/haskell/opendal-hs.cabal
@@ -44,7 +44,8 @@ library
         base >=4.10.0.0 && <5,
         unordered-containers >=0.2.0.0, 
         bytestring >=0.11.0.0,
-        mtl >=2.2.0.0
+        mtl >=2.2.0.0,
+        time >=1.10.0.0
 
 test-suite opendal-hs-test
     type:             exitcode-stdio-1.0
diff --git a/bindings/haskell/src/lib.rs b/bindings/haskell/src/lib.rs
index 2d73e01ea..30c5c2df8 100644
--- a/bindings/haskell/src/lib.rs
+++ b/bindings/haskell/src/lib.rs
@@ -16,6 +16,7 @@
 // under the License.
 
 mod result;
+mod types;
 
 use ::opendal as od;
 use result::FFIResult;
@@ -24,6 +25,7 @@ use std::ffi::CStr;
 use std::mem;
 use std::os::raw::c_char;
 use std::str::FromStr;
+use types::{ByteSlice, Metadata};
 
 /// # Safety
 ///
@@ -97,45 +99,6 @@ pub unsafe extern "C" fn free_operator(operator: *mut od::BlockingOperator) {
     }
 }
 
-#[repr(C)]
-#[derive(Debug)]
-pub struct ByteSlice {
-    data: *mut c_char,
-    len: usize,
-}
-
-impl ByteSlice {
-    fn from_vec(vec: Vec<u8>) -> Self {
-        let data = vec.as_ptr() as *mut c_char;
-        let len = vec.len();
-
-        // Leak the memory to pass the ownership to Haskell
-        std::mem::forget(vec);
-
-        ByteSlice { data, len }
-    }
-
-    /// # Safety
-    ///
-    /// * `ptr` is a valid pointer to a `ByteSlice`.
-    ///
-    /// # Panics
-    ///
-    /// * If `ptr` is not a valid pointer.
-    #[no_mangle]
-    pub unsafe extern "C" fn free_byteslice(ptr: *mut c_char, len: usize) {
-        if !ptr.is_null() {
-            drop(Vec::from_raw_parts(ptr, len, len));
-        }
-    }
-}
-
-impl From<&mut ByteSlice> for Vec<u8> {
-    fn from(val: &mut ByteSlice) -> Self {
-        unsafe { Vec::from_raw_parts(val.data as *mut u8, val.len, val.len) }
-    }
-}
-
 /// # Safety
 ///
 /// * `op` is a valid pointer to a `BlockingOperator`.
@@ -438,3 +401,42 @@ pub unsafe extern "C" fn blocking_delete(
 
     *result = res;
 }
+
+/// # Safety
+///
+/// * `op` is a valid pointer to a `BlockingOperator`.
+/// * `path` is a valid pointer to a nul terminated string.
+/// * `result` is a valid pointer, and has available memory to write to
+///
+/// # Panics
+///
+/// * If `op` is not a valid pointer.
+/// * If `result` is not a valid pointer, or does not have available memory to write to.
+#[no_mangle]
+pub unsafe extern "C" fn blocking_stat(
+    op: *mut od::BlockingOperator,
+    path: *const c_char,
+    result: *mut FFIResult<Metadata>,
+) {
+    let op = if op.is_null() {
+        *result = FFIResult::err("Operator is null");
+        return;
+    } else {
+        &mut *op
+    };
+
+    let path_str = match CStr::from_ptr(path).to_str() {
+        Ok(s) => s,
+        Err(_) => {
+            *result = FFIResult::err("Failed to convert scheme to string");
+            return;
+        }
+    };
+
+    let res = match op.stat(path_str) {
+        Ok(meta) => FFIResult::ok(meta.into()),
+        Err(e) => FFIResult::err_with_source("Failed to delete", e),
+    };
+
+    *result = res;
+}
diff --git a/bindings/haskell/src/types.rs b/bindings/haskell/src/types.rs
new file mode 100644
index 000000000..877abbd47
--- /dev/null
+++ b/bindings/haskell/src/types.rs
@@ -0,0 +1,141 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use ::opendal as od;
+use chrono::SecondsFormat;
+use std::ffi::c_char;
+
+#[repr(C)]
+#[derive(Debug)]
+pub struct ByteSlice {
+    data: *mut c_char,
+    len: usize,
+}
+
+impl ByteSlice {
+    pub fn from_vec(vec: Vec<u8>) -> Self {
+        let data = vec.as_ptr() as *mut c_char;
+        let len = vec.len();
+
+        // Leak the memory to pass the ownership to Haskell
+        std::mem::forget(vec);
+
+        ByteSlice { data, len }
+    }
+
+    /// # Safety
+    ///
+    /// * `ptr` is a valid pointer to a `ByteSlice`.
+    ///
+    /// # Panics
+    ///
+    /// * If `ptr` is not a valid pointer.
+    #[no_mangle]
+    pub unsafe extern "C" fn free_byteslice(ptr: *mut c_char, len: usize) {
+        if !ptr.is_null() {
+            drop(Vec::from_raw_parts(ptr, len, len));
+        }
+    }
+}
+
+impl From<&mut ByteSlice> for Vec<u8> {
+    fn from(val: &mut ByteSlice) -> Self {
+        unsafe { Vec::from_raw_parts(val.data as *mut u8, val.len, val.len) }
+    }
+}
+
+#[repr(C)]
+#[derive(Debug)]
+pub enum EntryMode {
+    File,
+    Dir,
+    Unknown,
+}
+
+#[repr(C)]
+#[derive(Debug)]
+pub struct Metadata {
+    mode: EntryMode,
+    cache_control: *const c_char,
+    content_disposition: *const c_char,
+    content_length: u64,
+    content_md5: *const c_char,
+    content_type: *const c_char,
+    etag: *const c_char,
+    last_modified: *const c_char,
+}
+
+impl From<od::Metadata> for Metadata {
+    fn from(val: od::Metadata) -> Self {
+        let mode = match val.mode() {
+            od::EntryMode::FILE => EntryMode::File,
+            od::EntryMode::DIR => EntryMode::Dir,
+            od::EntryMode::Unknown => EntryMode::Unknown,
+        };
+
+        let cache_control = match val.cache_control() {
+            Some(s) => unsafe { leak_str(s.to_string()) },
+            None => std::ptr::null(),
+        };
+
+        let content_disposition = match val.content_disposition() {
+            Some(s) => unsafe { leak_str(s.to_string()) },
+            None => std::ptr::null(),
+        };
+
+        let content_length = val.content_length();
+
+        let content_md5 = match val.content_md5() {
+            Some(s) => unsafe { leak_str(s.to_string()) },
+            None => std::ptr::null(),
+        };
+
+        let content_type = match val.content_type() {
+            Some(s) => unsafe { leak_str(s.to_string()) },
+            None => std::ptr::null(),
+        };
+
+        let etag = match val.etag() {
+            Some(s) => unsafe { leak_str(s.to_string()) },
+            None => std::ptr::null(),
+        };
+
+        let last_modified = match val.last_modified() {
+            Some(s) => unsafe { leak_str(s.to_rfc3339_opts(SecondsFormat::Nanos, false)) },
+            None => std::ptr::null(),
+        };
+
+        Metadata {
+            mode,
+            cache_control,
+            content_disposition,
+            content_length,
+            content_md5,
+            content_type,
+            etag,
+            last_modified,
+        }
+    }
+}
+
+// Leak the memory to pass the ownership to Haskell
+// Please note that haskell should free the memory after using it
+unsafe fn leak_str(s: String) -> *const c_char {
+    let ptr = s.as_ptr() as *const c_char;
+    std::mem::forget(s);
+    ptr
+}
diff --git a/bindings/haskell/test/BasicTest.hs b/bindings/haskell/test/BasicTest.hs
index 8c1197ff4..e48117076 100644
--- a/bindings/haskell/test/BasicTest.hs
+++ b/bindings/haskell/test/BasicTest.hs
@@ -44,8 +44,23 @@ testRawOperation = do
   isExistOpRaw op "key2" >>= (@?= Right True)
   createDirOpRaw op "dir1/" >>= (@?= Right ())
   isExistOpRaw op "dir1/" >>= (@?= Right True)
+  statOpRaw op "key1" >>= \v -> case v of
+    Right meta -> meta @?= except_meta
+    Left _ -> assertFailure "should not reach here"
   deleteOpRaw op "key1" >>= (@?= Right ())
   isExistOpRaw op "key1" >>= (@?= Right False)
+ where
+  except_meta =
+    Metadata
+      { mMode = File
+      , mCacheControl = Nothing
+      , mContentDisposition = Nothing
+      , mContentLength = 6
+      , mContentMD5 = Nothing
+      , mContentType = Nothing
+      , mETag = Nothing
+      , mLastModified = Nothing
+      }
 
 testMonad :: Assertion
 testMonad = do
@@ -61,8 +76,20 @@ testMonad = do
     isExistOp "key2" >>= liftIO . (@?= True)
     createDirOp "dir1/"
     isExistOp "dir1/" >>= liftIO . (@?= True)
+    statOp "key1" >>= liftIO . (@?= except_meta)
     deleteOp "key1"
     isExistOp "key1" >>= liftIO . (@?= False)
+  except_meta =
+    Metadata
+      { mMode = File
+      , mCacheControl = Nothing
+      , mContentDisposition = Nothing
+      , mContentLength = 6
+      , mContentMD5 = Nothing
+      , mContentType = Nothing
+      , mETag = Nothing
+      , mLastModified = Nothing
+      }
 
 testError :: Assertion
 testError = do
@@ -71,4 +98,4 @@ testError = do
     Left err -> errorCode err @?= NotFound
     Right _ -> assertFailure "should not reach here"
  where
-  operation = readOp "non-exist-path"
+  operation = readOp "non-exist-path"
\ No newline at end of file