packages feed

cow (empty) → 0.1.0.0

raw patch · 5 files changed

+292/−0 lines, 5 filesdep +Win32dep +basedep +cow

Dependencies added: Win32, base, cow, directory, tasty, tasty-hunit, unix

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for cow++## 0.1.0.0 -- 2026-04-30++* Linux, Windows, and Darwin support.
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2026 AgentM++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ cow.cabal view
@@ -0,0 +1,41 @@+cabal-version:      3.0+name:               cow+version:            0.1.0.0+synopsis:           Cross-platform file copy-on-write+-- description:+homepage:           https://github.com/agentm/cow+license:            MIT+license-file:       LICENSE+author:             AgentM+maintainer:         agentm@themactionfaction.com+category:           System+build-type:         Simple+extra-doc-files:    CHANGELOG.md++common warnings+    ghc-options: -Wall++library+    import:           warnings+    exposed-modules:  System.IO.Copy+    build-depends:    base ^>=4.18.3.0+    if os(linux)+       Build-Depends: unix+    if os(windows)+      Build-Depends: Win32 >= 2.12+    hs-source-dirs:   src+    default-language: Haskell2010++test-suite cow-test+    import:           warnings+    default-language: Haskell2010+    type:             exitcode-stdio-1.0+    hs-source-dirs:   test+    main-is:          Main.hs+    build-depends:+        base ^>=4.18.3.0,+        cow,+        tasty,+        tasty-hunit,+        directory+
+ src/System/IO/Copy.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE CPP #-}+-- Cross-platform copy-on-write file semantics.+module System.IO.Copy +  (copyFile+#if linux_HOST_OS || darwin_HOST_OS+   ,copyFileOnlyIfCopyOnWrite+   ,CopyError(..)+#endif+  ) where+import Foreign.C.Error (Errno(..))+#if defined(mingw32_HOST_OS)+-- WINDOWS+import qualified System.Win32.File as Win32+# if defined(i386_HOST_ARCH)+#  define WINDOWS_CCONV stdcall+# elif defined(x86_64_HOST_ARCH)+#  define WINDOWS_CCONV ccall+# endif+#else+-- LINUX+import Foreign.Ptr (Ptr, nullPtr)+import Foreign.C.Types+import System.IO (Handle, IOMode(..), openFile)+import Control.Exception (throwIO)+import System.Posix.Types (Fd(..))+import System.Posix.IO (handleToFd, closeFd)+import Foreign.C.Error (getErrno, eBADF, eFAULT, eNOTTY, eTXTBSY, eISDIR, eXDEV, eOPNOTSUPP, ePERM, eINVAL, errnoToIOError)+#endif++#if linux_HOST_OS+-- | True if the function should fallback on non-copy-on-write semantics if copy-on-write is not possible.+type CopyFallback = Bool++-- | Copy data from one handle to the other hopefully using copy-on-write semantics, but fallback to generic copy if copy-on-write fails.+copyHandle :: CopyFallback -> Handle -> Handle -> IO (Either CopyError ())+copyHandle fallback source dest = do+  sourceFd <- handleToFd source+  destFd <- handleToFd dest+  ret <- ficlone destFd sourceFd+  errno <- getErrno+  let convertError errno'+        | errno' `elem` [eBADF, ePERM, eTXTBSY] =+          InvalidFileDescriptorError errno'+      convertError errno'+        | errno' `elem` [eXDEV] =+          CrossFSWriteError errno'+      convertError errno'+        | errno' `elem` [eINVAL, eISDIR, eOPNOTSUPP] =+          NotSupportedError errno'+      convertError errno' =+        InvalidFileDescriptorError errno'+      cleanup = do+        closeFd sourceFd+        closeFd destFd+  if ret /= 0 then do+    if fallback then do+      if errno `elem` [eBADF, eFAULT, eNOTTY, eTXTBSY, eISDIR, ePERM] then do+        cleanup+        pure (Left (convertError errno))+      else do --EINVAL or EOPNOTSUPP or EXDEV, so perform fallback+        result <- copy_file_range sourceFd destFd+        if result < 0 then do+          err <- getErrno+          cleanup+          pure (Left (FallbackCopyError err))+        else do+          cleanup+          pure (Right ())+    else do+      cleanup+      pure (Left (convertError errno))+  else do+    cleanup+    pure (Right ())+#elif darwin_HOST_OS+  res <- clonefile source dest+  if res < 0 then do+    -- on darwin, there is no fallback- we just assume APFS as the filesystem+    pure (Left (FallbackCopyError (Errno 0)))+  else+    pure (Right ())+#elif mingw32_HOST_OS+#endif++copyFile :: FilePath -> FilePath -> IO ()+copyFile sourcePath destPath = do+  -- call copyHandle on Linux and Darwin+#if linux_HOST_OS || darwin_HOST_OS+  sourceHandle <- openFile sourcePath ReadMode +  destHandle <- openFile destPath WriteMode +  res <- copyHandle True sourceHandle destHandle+  case res of+    Left err -> throwIO $ errnoToIOError "copyFile (CoW)" (errnoFromCopyError err) Nothing Nothing+    Right () -> pure ()+#else +  -- on Windows, copyFile will use CoW where available which is only on ReFS+  Win32.copyFile sourcePath destPath False+#endif+  +data CopyError =+  CrossFSWriteError Errno |+  InvalidFileDescriptorError Errno |+  NotSupportedError Errno |+  FallbackCopyError Errno+  deriving (Eq)++instance Show CopyError where+  show e =+    case e of+      CrossFSWriteError (Errno errno) -> "CrossFSWriteError " <> show errno+      InvalidFileDescriptorError (Errno errno) -> "InvalidFileDescriptorError " <> show errno+      NotSupportedError (Errno errno) -> "NotSupportedError " <> show errno+      FallbackCopyError (Errno errno) -> "FallbackCopyError " <> show errno++#if linux_HOST_OS || darwin_HOST_OS+errnoFromCopyError :: CopyError -> Errno+errnoFromCopyError e =+  case e of+    CrossFSWriteError e' -> e'+    InvalidFileDescriptorError e' -> e'+    NotSupportedError e' -> e'+    FallbackCopyError e' -> e'++copyFileOnlyIfCopyOnWrite :: FilePath -> FilePath -> IO (Either CopyError ())+copyFileOnlyIfCopyOnWrite sourcePath destPath = do+  sourceHandle <- openFile sourcePath ReadMode +  destHandle <- openFile destPath WriteMode +  copyHandle False sourceHandle destHandle+#endif++#ifdef linux_HOST_OS+-- LINUX ONLY+-- FICLONE ioctl number. Common value for x86_64 Linux (from linux/fs.h):+-- #define FICLONE        _IOW(0x94, 9, int)+-- Encoded here as 0x40049409 (unsigned long)+-- If this doesn't work on your architecture, check <linux/fs.h>.+ficlone_ioctl :: CULong+ficlone_ioctl = 0x40049409++foreign import ccall unsafe "ioctl"+  c_ioctl :: CInt -> CULong -> CInt -> IO CInt++-- wrapper: perform ioctl with fdDst, FICLONE, fdSrc+ficlone :: Fd -> Fd -> IO CInt+ficlone (Fd fdDst) (Fd fdSrc) = do+  -- third arg should be the source fd cast to unsigned long / int as expected by kernel+  let arg = fromIntegral fdSrc :: CInt+  --throwErrnoIfMinus1_ "ioctl(FICLONE)" $+  c_ioctl (fromIntegral fdDst) ficlone_ioctl arg++foreign import ccall unsafe "copy_file_range"+  c_copy_file_range :: CInt -> Ptr CULong -> CInt -> Ptr CULong -> CULong -> CUInt -> IO CLong++copy_file_range :: Fd -> Fd -> IO CLong+copy_file_range (Fd fdSource) (Fd fdDest) =+  c_copy_file_range (fromIntegral fdSource) nullPtr (fromIntegral fdDest) nullPtr maxBound 0++#elif darwin_HOST_OS+foreign import ccall unsafe "clonefile"+  c_clonefile :: CString -> CString -> IO CInt++clonefile :: FilePath -> FilePath -> IO CInt+clonefile sourcePath destPath =+  withCString sourcePath $ \cSourcePath ->+    withCString destPath $ \cDestPath ->+      c_clonefile cSourcePath cDestPath++#endif
+ test/Main.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE CPP #-}+import Test.Tasty+import Test.Tasty.HUnit+import System.IO.Copy+#if linux_HOST_OS || darwin_HOST_OS+import Foreign.C.Error (Errno(..))+#endif+import System.Directory (removeFile)++main :: IO ()+main = defaultMain tests++tests :: TestTree+#if linux_HOST_OS || darwin_HOST_OS+tests = testGroup "CoW" [+  testCase "mandatory CoW" testMandatoryCoW,+  testCase "optional CoW" testOptionalCoW+  ]+#else+tests = testGroup "CoW" [+  testCase "optional CoW" testOptionalCoW+  ]+#endif++#if linux_HOST_OS || darwin_HOST_OS+testMandatoryCoW :: Assertion+testMandatoryCoW = do+  -- btrfs/xfs+  let source1 = "test/cow_source.txt"+      dest1 = "test/cow_dest.txt"+      cow_data = "COW DATA"+  writeFile source1 cow_data+  result1 <- copyFileOnlyIfCopyOnWrite source1 dest1+  assertEqual "copyFileOnlyIfCopyOnWrite" (Right ()) result1+  dest1Data <- readFile dest1+  assertEqual "mandatory cow data" cow_data dest1Data+  removeFile source1+  removeFile dest1++  -- ext4+  let source2 = "/tmp/cow_source.txt"+      dest2 = "/tmp/cow_dest.txt"+  writeFile source2 "COW DATA"+  result2 <- copyFileOnlyIfCopyOnWrite source2 dest2+  assertEqual "copyFileOnlyIfCopyOnWrite expected failure" (Left (NotSupportedError (Errno 95))) result2  +#endif++testOptionalCoW :: Assertion+testOptionalCoW = do+  let source1 = "cow_source_opt.txt"+      dest1 = "cow_dest_opt.txt"+      cow_data = "COW_DATA"+  writeFile source1 cow_data+  copyFile source1 dest1+  dest1Data <- readFile dest1+  assertEqual "optional cow data" cow_data dest1Data+  removeFile source1+  removeFile dest1