mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-03 02:22:32 +00:00
* WIP: Haskell impl of resizeImageToStrSize * WIP: add FFI export wrapper * WIP: switch ios resizeImageToStrSizeSync to backend * WIP: fix resizeImageToStrSizeSync * WIP: add JNI wrappers * Add non-base64 resize and cWriteImage * WIP: add chat_write_image * fix resizeToStr * fixes * fix syntax, raise jpeg quality back * add chatWriteImage * Fix cfArgs in tests * Use jpeg-turbo encoder * add nix pins * bump jpeg-turbo * add CI deps * add encoding funs to android lib * bump jpeg-turbo for 20.04 fix * jpeg-turbo fix for 8.10 * android fixes * Bump jpeg-turbo * set static flag for jpeg-turbo * Fix nix for aarch64/android * Fix android C api * Fix resize in android * Bump hs-libjpeg-turbo * target correct override * override android lib from android pkgs * add macos build tweaks * bump hs-jpeg-turbo for 8.10 fixes * more build fixes * make ios compile * Always use pkgconfig for libjpeg-turbo * WIP: debug libjpeg in ubuntu pkg-config CI * Add pkg-config to dockerfiles * Add libturbojpeg to dockerfiles * Use -dev version of libturbojpeg in dockerfiles * Install pkg-config in windows CI * Disable flag for custom libturbojpeg linking options Workarounds relocation error on x86_64-linux with gcc: /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/libturbojpeg.a(turbojpeg.c.o): relocation R_X86_64_TPOFF32 against `errStr' can not be used when making a shared object; recompile with -fPIC /usr/bin/ld: failed to set dynamic section sizes: bad value * WIP: debug libjpeg in windows pkg-config CI * WIP: debug libjpeg package in windows CI * core: image resize module and buffer FFI * ci: link libjpeg-turbo statically on all platforms * ui: resize images in the core * core: rename image resize functions to chat_resize_image and chat_resize_image_uri * ci: restore whitespace in build.yml * ci: build libjpeg-turbo 3.1.4.1 from a submodule * core: hs-jpeg-turbo from github.com/simplex-chat * nodejs: resize images * python: resize images * core: hs-jpeg-turbo from gitlab.com, the GitHub import lacks LFS objects * core: generate /img previews * ci: link libjpeg-turbo on Windows without pkg-config * ci: build libjpeg-turbo on Windows with the GHC toolchain * docs: plan for image resizing in the core * docs: update image resizing plan * ios: prevent crash when wallpaper image cannot be resized * android, desktop: raise an error when an image cannot be resized * docs: update spec references for image resizing * docker: build the CLI image from a clone without submodules * docs: apply review fixes to image resizing plan * docker: build the CLI from a git worktree with the submodule present * docs: apply second review to image resizing plan * docs: point image resizing plan at the rewritten commit * core: choose image format by transparency in chat_resize_image * nodejs: resize images to the app sizes * python: resize images to the app sizes * nodejs, python: describe the core modules as used for image resizing * docs: add license notices of the image libraries * ci: describe the avatar method without attributing it to the apps * ci, ios, nix: remove the libjpeg-turbo build outside cabal * python: name resize_image in the core module text * nodejs, python: describe resizing to the app image sizes * core: refuse images larger than the supported size * core: resize to JPEG images with a side above 65500 pixels * docs: record image resizing decisions for the bundled libjpeg-turbo * core, docs: build libjpeg-turbo and stb from the fork submodules * docs: record the fork pins in the image resizing plan * core: resize images to at most 4320 pixels per side, refuse above 16384 * docs: record the image side limits in the image resizing plan * core: pin the rebased hs-jpeg-turbo fork * core: pin the hs-jpeg-turbo fork without package.yaml * docs: plan scaled JPEG decoding and EXIF orientation of images * docs: copy the JuicyPixels-stbir licence of the pinned fork * core: pin the hs-jpeg-turbo fork with scaled JPEG decoding * core: decode JPEG at the display size, up to 65500 pixels per side * core: rotate JPEG images by their EXIF orientation * docs: refer the image plan to the JPEG decoding code * docs: record the approved image processing decisions * core: limit JPEG decoding memory to 1024 MB outside iOS and Android * docs: name JPEG decoding in the libjpeg-turbo notice * docs: plan image resizing from files and the decisions of 26 Oct * core: scale images in one Catmull-Rom pass with sharpening * docs: plan image sizes at a fixed quality * core: resize images at a fixed quality to fill the size limit * core: resize images from a file path or a file descriptor * ios, android, desktop: resize picked images from their files * core: encode resized images with mozjpeg * tests: check descriptor closing by the file it refers to * core: raise JPEG quality only for a full-size image below 97% of the limit * ci: name the mozjpeg submodule in the Windows cleanup comments * docs: refer the image plan to the current code * docs: refer the image plan to the code merged with master * core: export every image resizing function from each library * core: close the file descriptor when its handle cannot be created * docs: count the C sources of mozjpeg with the arithmetic decoder * core: use the reviewed hs-jpeg-turbo and JuicyPixels-stbir forks * ci: build the armv7a Android core library on request * core: round image heights half up in the size search * ios: describe the file image resizing functions in SimpleX.h * ci: pick, preview and send images in the Claude app tests * core: decode PNG images with libspng and refuse other formats * core: encode PNG with libspng, keeping the smaller filter choice * ci: pick, preview and send images in the scripted app smoke tests * flake: build the PNG decoder package in nix builds * ios: remove the copy of a picked image after sending it * core: refuse image files larger than the decode memory limit * core: scale a JPEG image to its largest size once per resize * core: bisect JPEG quality without a step function * core: reduce the memory used to sharpen and to decode PNG images * android, desktop: pass the image format to the core as an integer * android, desktop: fuse the preview conditions of picked media * android, desktop, ci: check that the core resized the smoke test images * python: remove the docstrings of the explicit image resizing functions * docs: add the second review fixes to the image plan * ci: tap the iOS attachment button by its accessibility label * core: keep the colour profiles of resized images * docs: add the colour profiles to the image plan * ci: wait up to 30 s for the new group screens in the iOS smoke test * ci: pick the Android smoke test images from their own photo folders * ci: tap the iOS photo picker by position, as its tree is not shown * ci: pick the test card and the logo by their iOS picker positions * core: update JuicyPixels-stbir with the stb_image_resize licence terms * core: write a fixed Display P3 profile for Display P3 images only * core: do not keep the input image in memory for its colour profile * core: read the JPEG segments once for the orientation and the profile * core: read an image file into one buffer of its size * docs: add the third review fixes to the image plan * core: resize images with Display P3 or no profile, not profile bytes * ci: check the pixel sizes of the images sent in the iOS smoke test * core: update hs-jpeg-turbo with PNG chunk limits and encoder fixes * docs: list Compact ICC Profiles, libspng and miniz in the dependencies * android, desktop: reword the picked media preview comment * docs: refer the image plan to the code merged with master * ci: render the iOS snapshots before the images case * ci: dismiss the snapshot tests' camera alert before the iOS images case * core: update hs-jpeg-turbo with tests and the JuicyPixels-spng synopsis * core: pin hs-jpeg-turbo and JuicyPixels-stbir in simplex-chat repositories * update ios core library * core, ui: accept only JPEG and PNG image data * core: update JuicyPixels-stbir * docs: plan for the iOS copy of picked images --------- Co-authored-by: IC Rainbow <aenor.realm@gmail.com> Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> Co-authored-by: Alexandre Esteves <alexfmpe@proton.me>
485 lines
20 KiB
Haskell
485 lines
20 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE TemplateHaskell #-}
|
|
|
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
|
|
module MobileTests (mobileTests) where
|
|
|
|
import ChatClient
|
|
import ChatTests.DBUtils
|
|
import ChatTests.Utils
|
|
import Codec.Picture (PixelRGB8 (..), generateImage)
|
|
import Codec.Picture.JPEGTurbo (encodeRGB)
|
|
import Control.Concurrent (forkIO)
|
|
import Control.Concurrent.STM
|
|
import Control.Monad.Except
|
|
import Data.Aeson (FromJSON)
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.Aeson.TH as JQ
|
|
import Data.ByteString (ByteString)
|
|
import qualified Data.ByteString as B
|
|
import qualified Data.ByteString.Base64 as B64
|
|
import qualified Data.ByteString.Char8 as BS
|
|
import Data.ByteString.Internal (create)
|
|
import qualified Data.ByteString.Lazy.Char8 as LB
|
|
import Data.Time.Clock (getCurrentTime)
|
|
import Data.Word (Word8, Word32)
|
|
import Foreign.C
|
|
import Foreign.Marshal.Alloc (alloca, mallocBytes)
|
|
import Foreign.Marshal.Utils (copyBytes)
|
|
import Foreign.Ptr
|
|
import Foreign.StablePtr
|
|
import Foreign.Storable (peek)
|
|
import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding)
|
|
import qualified GHC.IO.FD as FD
|
|
import GHC.IO.Handle.FD (fdToHandle)
|
|
import ImageTests (pngShortImageData, pngSideAbove16384)
|
|
import JSONFixtures
|
|
import Simplex.Chat
|
|
import Simplex.Chat.Badges (BadgeInfo (..), BadgeRequest (..), BadgeType (..), generateMasterKey, verifyCredential)
|
|
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatDatabase (..))
|
|
import Simplex.Chat.Image (maxDecodeMemoryMB)
|
|
import Simplex.Chat.Mobile hiding (error)
|
|
import Simplex.Chat.Mobile.Badges hiding (error)
|
|
import Simplex.Chat.Mobile.File
|
|
import Simplex.Chat.Mobile.Shared
|
|
import Simplex.Chat.Mobile.WebRTC
|
|
import Simplex.Chat.Options.DB
|
|
import Simplex.Chat.Store
|
|
import Simplex.Chat.Store.Profiles
|
|
import Simplex.Chat.Types (AgentUserId (..), Profile (..))
|
|
import Simplex.Messaging.Agent.Client (AgentClient (..))
|
|
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), Env (..))
|
|
import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..))
|
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Crypto.File (CryptoFile(..), CryptoFileArgs (..))
|
|
import qualified Simplex.Messaging.Crypto.File as CF
|
|
import Simplex.Messaging.Encoding.String
|
|
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
|
import System.Directory (copyFile)
|
|
import System.FilePath ((</>))
|
|
import System.IO (IOMode (..), hClose, hSetFileSize, utf8, withBinaryFile)
|
|
import System.IO.Error (tryIOError)
|
|
import System.Posix.Internals (c_open, fdStat, o_RDONLY, withFilePath)
|
|
import System.Process (createPipeFd)
|
|
import Test.Hspec hiding (it)
|
|
|
|
mobileTests :: HasCallStack => SpecWith TestParams
|
|
mobileTests = do
|
|
describe "mobile API" $ do
|
|
runIO $ do
|
|
setLocaleEncoding utf8
|
|
setFileSystemEncoding utf8
|
|
setForeignEncoding utf8
|
|
it "start new chat without user" testChatApiNoUser
|
|
it "start new chat with existing user" testChatApi
|
|
it "should set queue size via C API" testChatMigrateInitQueueCApi
|
|
it "should encrypt/decrypt WebRTC frames" testMediaApi
|
|
it "should encrypt/decrypt WebRTC frames via C API" testMediaCApi
|
|
describe "should read/write encrypted files via C API" $ do
|
|
it "latin1 name" $ testFileCApi "test"
|
|
it "utf8 name 1" $ testFileCApi "тест"
|
|
it "utf8 name 2" $ testFileCApi "👍"
|
|
it "no exception on missing file" testMissingFileCApi
|
|
describe "should encrypt/decrypt files via C API" $ do
|
|
it "latin1 name" $ testFileEncryptionCApi "test"
|
|
it "utf8 name 1" $ testFileEncryptionCApi "тест"
|
|
it "utf8 name 2" $ testFileEncryptionCApi "👍"
|
|
it "no exception on missing file" testMissingFileEncryptionCApi
|
|
describe "validate name" $ do
|
|
it "should convert invalid name to a valid name" testValidNameCApi
|
|
describe "JSON length" $ do
|
|
it "should compute length of JSON encoded string" testChatJsonLengthCApi
|
|
describe "Parsers" $ do
|
|
it "should parse server address" testChatParseServer
|
|
it "should parse and sanitize URI" testChatParseUri
|
|
describe "Badges" $ do
|
|
it "should generate key and issue badge via C API, verify credential" testBadgeKeygenIssueCApi
|
|
describe "Images" $ do
|
|
it "should resize image to data URI via C API" testResizeImageUriCApi
|
|
it "should resize image to data size via C API" testResizeImageCApi
|
|
it "should choose image format by transparency via C API" testResizeImageAutoFormatCApi
|
|
it "should resize JPEG with a side above 16384 via C API" testResizeLargeJpegCApi
|
|
it "should resize image from a file path via C API" testResizeImageFileCApi
|
|
it "should resize image from a file descriptor via C API" testResizeImageFdCApi
|
|
it "should refuse malformed PNG via C API" testResizeMalformedPngCApi
|
|
|
|
noActiveUser :: LB.ByteString
|
|
noActiveUser =
|
|
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
|
noActiveUserSwift
|
|
#else
|
|
noActiveUserTagged
|
|
#endif
|
|
|
|
activeUserExists :: LB.ByteString
|
|
activeUserExists =
|
|
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
|
activeUserExistsSwift
|
|
#else
|
|
activeUserExistsTagged
|
|
#endif
|
|
|
|
activeUser :: LB.ByteString
|
|
activeUser =
|
|
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
|
activeUserSwift
|
|
#else
|
|
activeUserTagged
|
|
#endif
|
|
|
|
chatStarted :: LB.ByteString
|
|
chatStarted =
|
|
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
|
chatStartedSwift
|
|
#else
|
|
chatStartedTagged
|
|
#endif
|
|
|
|
connectionsDiff :: LB.ByteString
|
|
connectionsDiff =
|
|
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
|
connectionsDiffSwift
|
|
#else
|
|
connectionsDiffTagged
|
|
#endif
|
|
|
|
parsedMarkdown :: LB.ByteString
|
|
parsedMarkdown =
|
|
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
|
parsedMarkdownSwift
|
|
#else
|
|
parsedMarkdownTagged
|
|
#endif
|
|
|
|
testChatApiNoUser :: TestParams -> IO ()
|
|
testChatApiNoUser ps = do
|
|
let tmp = tmpPath ps
|
|
dbPrefix = tmp </> "1"
|
|
Right cc <- chatMigrateInit dbPrefix "" "yesUp"
|
|
Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "myKey" "yesUp"
|
|
chatSendCmd cc "/u" `shouldReturn` noActiveUser
|
|
chatSendCmd cc "/_start" `shouldReturn` noActiveUser
|
|
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUser
|
|
chatSendCmd cc "/_start" `shouldReturn` chatStarted
|
|
|
|
testChatApi :: TestParams -> IO ()
|
|
testChatApi ps = do
|
|
let tmp = tmpPath ps
|
|
dbPrefix = tmp </> "1"
|
|
Right ChatDatabase {chatStore, agentStore} <- createChatDatabase (ChatDbOpts dbPrefix "myKey" DB.TQOff True) (MigrationConfig MCYesUp Nothing)
|
|
insertUser agentStore
|
|
ts <- getCurrentTime
|
|
Right _ <- withTransaction chatStore $ \db -> runExceptT $ createUserRecordAt db (AgentUserId 1) False False aliceProfile {preferences = Nothing} True ts
|
|
Right cc <- chatMigrateInit dbPrefix "myKey" "yesUp"
|
|
Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "" "yesUp"
|
|
Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "anotherKey" "yesUp"
|
|
chatSendCmd cc "/u" `shouldReturn` activeUser
|
|
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists
|
|
chatSendCmd cc "/_start" `shouldReturn` chatStarted
|
|
chatRecvMsg cc `shouldReturn` connectionsDiff
|
|
chatRecvMsgWait cc 10000 `shouldReturn` ""
|
|
chatParseMarkdown "hello" `shouldBe` "{}"
|
|
chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown
|
|
|
|
testChatMigrateInitQueueCApi :: TestParams -> IO ()
|
|
testChatMigrateInitQueueCApi ps = do
|
|
cPath <- newCString $ tmpPath ps </> "1"
|
|
cKey <- newCString ""
|
|
cConfirm <- newCString "yesUp"
|
|
alloca $ \ctrlPtr -> do
|
|
let migrateInit queueSize = peekCAString =<< cChatMigrateInitQueue cPath cKey cConfirm queueSize ctrlPtr
|
|
migrateInit 0 `shouldReturn` jsonStr DBMInvalidQueueSize
|
|
migrateInit (-1) `shouldReturn` jsonStr DBMInvalidQueueSize
|
|
migrateInit 65536 `shouldReturn` jsonStr DBMOk
|
|
ChatController {config = ChatConfig {tbqSize}, smpAgent = AgentClient {agentEnv = Env {config = AgentConfig {tbqSize = agentQSize}}}} <- deRefStablePtr =<< peek ctrlPtr
|
|
tbqSize `shouldBe` 65536
|
|
agentQSize `shouldBe` 65536
|
|
where
|
|
jsonStr = LB.unpack . J.encode
|
|
|
|
testMediaApi :: HasCallStack => TestParams -> IO ()
|
|
testMediaApi ps = do
|
|
let tmp = tmpPath ps
|
|
Right c@ChatController {random = g} <- chatMigrateInit (tmp </> "1") "" "yesUp"
|
|
cc <- newStablePtr c
|
|
key <- atomically $ C.randomBytes 32 g
|
|
frame <- atomically $ C.randomBytes 100 g
|
|
let keyStr = strEncode key
|
|
reserved = B.replicate (C.authTagSize + C.gcmIVSize) 0
|
|
frame' = frame <> reserved
|
|
Right encrypted <- runExceptT $ chatEncryptMedia cc keyStr frame'
|
|
encrypted `shouldNotBe` frame'
|
|
B.length encrypted `shouldBe` B.length frame'
|
|
runExceptT (chatDecryptMedia keyStr encrypted) `shouldReturn` Right frame'
|
|
|
|
testMediaCApi :: HasCallStack => TestParams -> IO ()
|
|
testMediaCApi ps = do
|
|
let tmp = tmpPath ps
|
|
Right c@ChatController {random = g} <- chatMigrateInit (tmp </> "1") "" "yesUp"
|
|
cc <- newStablePtr c
|
|
key <- atomically $ C.randomBytes 32 g
|
|
frame <- atomically $ C.randomBytes 100 g
|
|
let keyStr = strEncode key
|
|
reserved = B.replicate (C.authTagSize + C.gcmIVSize) 0
|
|
frame' = frame <> reserved
|
|
encrypted <- test (cChatEncryptMedia cc) keyStr frame'
|
|
encrypted `shouldNotBe` frame'
|
|
test cChatDecryptMedia keyStr encrypted `shouldReturn` frame'
|
|
where
|
|
test :: HasCallStack => (CString -> Ptr Word8 -> CInt -> IO CString) -> ByteString -> ByteString -> IO ByteString
|
|
test f keyStr frame = do
|
|
let len = B.length frame
|
|
cLen = fromIntegral len
|
|
ptr <- mallocBytes len
|
|
putByteString ptr frame
|
|
cKeyStr <- newCAString $ BS.unpack keyStr
|
|
(f cKeyStr ptr cLen >>= peekCAString) `shouldReturn` ""
|
|
getByteString ptr cLen
|
|
|
|
instance FromJSON WriteFileResult where
|
|
parseJSON = $(JQ.mkParseJSON (sumTypeJSON $ dropPrefix "WF") ''WriteFileResult)
|
|
|
|
instance FromJSON ReadFileResult where
|
|
parseJSON = $(JQ.mkParseJSON (sumTypeJSON $ dropPrefix "RF") ''ReadFileResult)
|
|
|
|
testFileCApi :: FilePath -> TestParams -> IO ()
|
|
testFileCApi fileName ps = do
|
|
let tmp = tmpPath ps
|
|
cc <- mkCCPtr tmp
|
|
src <- B.readFile "./tests/fixtures/test.pdf"
|
|
let path = tmp </> (fileName <> ".pdf")
|
|
cPath <- newCString path
|
|
let len = B.length src
|
|
cLen = fromIntegral len
|
|
ptr <- mallocBytes $ B.length src
|
|
putByteString ptr src
|
|
r <- peekCAString =<< cChatWriteFile cc cPath ptr cLen
|
|
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r
|
|
let encryptedFile = CryptoFile path $ Just cfArgs
|
|
CF.getFileContentsSize encryptedFile `shouldReturn` fromIntegral (B.length src)
|
|
cKey <- encodedCString key
|
|
cNonce <- encodedCString nonce
|
|
-- the returned pointer contains 0, buffer length as Word32, then buffer
|
|
ptr' <- cChatReadFile cPath cKey cNonce
|
|
peek ptr' `shouldReturn` (0 :: Word8)
|
|
sz :: Word32 <- peek (ptr' `plusPtr` 1)
|
|
let sz' = fromIntegral sz
|
|
contents <- create sz' $ \toPtr -> copyBytes toPtr (ptr' `plusPtr` 5) sz'
|
|
contents `shouldBe` src
|
|
sz' `shouldBe` len
|
|
|
|
testMissingFileCApi :: TestParams -> IO ()
|
|
testMissingFileCApi ps = do
|
|
let tmp = tmpPath ps
|
|
let path = tmp </> "missing_file"
|
|
cPath <- newCString path
|
|
CFArgs key nonce <- atomically . CF.randomArgs =<< C.newRandom
|
|
cKey <- encodedCString key
|
|
cNonce <- encodedCString nonce
|
|
ptr <- cChatReadFile cPath cKey cNonce
|
|
peek ptr `shouldReturn` 1
|
|
err <- peekCAString (ptr `plusPtr` 1)
|
|
err `shouldContain` "missing_file: openBinaryFile: does not exist"
|
|
|
|
testFileEncryptionCApi :: FilePath -> TestParams -> IO ()
|
|
testFileEncryptionCApi fileName ps = do
|
|
let tmp = tmpPath ps
|
|
cc <- mkCCPtr tmp
|
|
let fromPath = tmp </> (fileName <> ".source.pdf")
|
|
copyFile "./tests/fixtures/test.pdf" fromPath
|
|
src <- B.readFile fromPath
|
|
cFromPath <- newCString fromPath
|
|
let toPath = tmp </> (fileName <> ".encrypted.pdf")
|
|
cToPath <- newCString toPath
|
|
r <- peekCAString =<< cChatEncryptFile cc cFromPath cToPath
|
|
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r
|
|
CF.getFileContentsSize (CryptoFile toPath $ Just cfArgs) `shouldReturn` fromIntegral (B.length src)
|
|
cKey <- encodedCString key
|
|
cNonce <- encodedCString nonce
|
|
let toPath' = tmp </> (fileName <> ".decrypted.pdf")
|
|
cToPath' <- newCString toPath'
|
|
"" <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath'
|
|
B.readFile toPath' `shouldReturn` src
|
|
|
|
testMissingFileEncryptionCApi :: TestParams -> IO ()
|
|
testMissingFileEncryptionCApi ps = do
|
|
let tmp = tmpPath ps
|
|
cc <- mkCCPtr tmp
|
|
let fromPath = tmp </> "missing_file.source.pdf"
|
|
toPath = tmp </> "missing_file.encrypted.pdf"
|
|
cFromPath <- newCString fromPath
|
|
cToPath <- newCString toPath
|
|
r <- peekCAString =<< cChatEncryptFile cc cFromPath cToPath
|
|
Just (WFError err) <- jDecode r
|
|
err `shouldContain` fromPath
|
|
CFArgs key nonce <- atomically . CF.randomArgs =<< C.newRandom
|
|
cKey <- encodedCString key
|
|
cNonce <- encodedCString nonce
|
|
let toPath' = tmp </> "missing_file.decrypted.pdf"
|
|
cToPath' <- newCString toPath'
|
|
err' <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath'
|
|
err' `shouldContain` toPath
|
|
|
|
mkCCPtr :: FilePath -> IO (StablePtr ChatController)
|
|
mkCCPtr tmp = either (error . show) newStablePtr =<< chatMigrateInit (tmp </> "1") "" "yesUp"
|
|
|
|
testValidNameCApi :: TestParams -> IO ()
|
|
testValidNameCApi _ = do
|
|
let goodName = "Джон Доу 👍"
|
|
cName1 <- cChatValidName =<< newCString goodName
|
|
peekCString cName1 `shouldReturn` goodName
|
|
cName2 <- cChatValidName =<< newCString " @'Джон' Доу 👍 "
|
|
peekCString cName2 `shouldReturn` goodName
|
|
|
|
testChatJsonLengthCApi :: TestParams -> IO ()
|
|
testChatJsonLengthCApi _ = do
|
|
cInt1 <- cChatJsonLength =<< newCString "Hello!"
|
|
cInt1 `shouldBe` 6
|
|
cInt2 <- cChatJsonLength =<< newCString "こんにちは!"
|
|
cInt2 `shouldBe` 18
|
|
|
|
testChatParseServer :: TestParams -> IO ()
|
|
testChatParseServer _ = do
|
|
pure ()
|
|
|
|
testChatParseUri :: TestParams -> IO ()
|
|
testChatParseUri _ = do
|
|
pure ()
|
|
|
|
-- Generate a server keypair and issue a badge credential via the C FFI,
|
|
-- constructing the request from the typed records, then verify the issued
|
|
-- credential's BBS signature on the Haskell side.
|
|
testBadgeKeygenIssueCApi :: TestParams -> IO ()
|
|
testBadgeKeygenIssueCApi _ = do
|
|
g <- C.newRandom
|
|
IssuerKeyPair {publicKey, secretKey} <- ffiResult =<< (peekCString =<< cChatBadgeKeygen)
|
|
mk <- generateMasterKey g
|
|
badgeExpiry <- getCurrentTime
|
|
let req = BadgeIssueReq {badgeKeyIdx = 1, secretKey, request = BadgeRequest {masterKey = mk, badgeInfo = BadgeInfo {badgeType = BTSupporter, badgeExpiry, badgeExtra = ""}}}
|
|
cred <- ffiResult =<< (peekCString =<< cChatBadgeIssue =<< newCString (LB.unpack (J.encode req)))
|
|
verifyCredential publicKey cred `shouldReturn` True
|
|
|
|
testResizeImageUriCApi :: TestParams -> IO ()
|
|
testResizeImageUriCApi _ = do
|
|
(ptr, len) <- cBuffer =<< B.readFile "./tests/fixtures/test.jpg"
|
|
uri <- peekCAString =<< cChatResizeImageUri ptr len 14000
|
|
uri `shouldStartWith` "data:image/jpg;base64,"
|
|
length uri `shouldSatisfy` (<= 14000)
|
|
(ptr', len') <- cBuffer "not an image"
|
|
(peekCAString =<< cChatResizeImageUri ptr' len' 14000) `shouldReturn` ""
|
|
|
|
testResizeImageCApi :: TestParams -> IO ()
|
|
testResizeImageCApi _ = do
|
|
(ptr, len) <- cBuffer =<< B.readFile "./tests/fixtures/test.jpg"
|
|
let resizeToDataSize png = do
|
|
Right bytes <- B64.decode . BS.pack <$> (peekCAString =<< cChatResizeImage ptr len 100000 png)
|
|
B.length bytes `shouldSatisfy` (<= 100000)
|
|
pure bytes
|
|
B.take 2 <$> resizeToDataSize 0 `shouldReturn` "\xFF\xD8"
|
|
B.take 8 <$> resizeToDataSize 1 `shouldReturn` "\x89PNG\r\n\x1a\n"
|
|
|
|
testResizeImageAutoFormatCApi :: TestParams -> IO ()
|
|
testResizeImageAutoFormatCApi _ = do
|
|
B.take 2 <$> resizeWithAutoFormat "./tests/fixtures/test.jpg" `shouldReturn` "\xFF\xD8"
|
|
B.take 8 <$> resizeWithAutoFormat "./tests/fixtures/logo-large-rgba.png" `shouldReturn` "\x89PNG\r\n\x1a\n"
|
|
where
|
|
resizeWithAutoFormat path = do
|
|
(ptr, len) <- cBuffer =<< B.readFile path
|
|
Right bytes <- B64.decode . BS.pack <$> (peekCAString =<< cChatResizeImage ptr len 100000 (-1))
|
|
pure bytes
|
|
|
|
testResizeLargeJpegCApi :: TestParams -> IO ()
|
|
testResizeLargeJpegCApi _ = do
|
|
(ptr, len) <- cBuffer =<< encodeRGB 90 Nothing (generateImage (\_ _ -> PixelRGB8 128 128 128) 20000 16)
|
|
Right bytes <- B64.decode . BS.pack <$> (peekCAString =<< cChatResizeImage ptr len 261120 (-1))
|
|
B.take 2 bytes `shouldBe` "\xFF\xD8"
|
|
|
|
testResizeImageFileCApi :: TestParams -> IO ()
|
|
testResizeImageFileCApi ps = do
|
|
uri <- peekCAString =<< withCString "./tests/fixtures/test.jpg" (`cChatResizeImageFileUri` 14000)
|
|
uri `shouldStartWith` "data:image/jpg;base64,"
|
|
length uri `shouldSatisfy` (<= 14000)
|
|
Right bytes <- B64.decode . BS.pack <$> (peekCAString =<< withCString "./tests/fixtures/logo-large-rgba.png" (\path -> cChatResizeImageFile path 100000 (-1)))
|
|
B.take 8 bytes `shouldBe` "\x89PNG\r\n\x1a\n"
|
|
(peekCAString =<< withCString "./tests/fixtures/missing.jpg" (`cChatResizeImageFileUri` 14000)) `shouldReturn` ""
|
|
oversized <- oversizedJpegFile ps
|
|
(peekCAString =<< withCString oversized (`cChatResizeImageFileUri` 14000)) `shouldReturn` ""
|
|
|
|
testResizeImageFdCApi :: TestParams -> IO ()
|
|
testResizeImageFdCApi ps = do
|
|
jpeg <- openFd "./tests/fixtures/test.jpg"
|
|
jpegFile <- fdStat jpeg
|
|
uri <- peekCAString =<< cChatResizeImageFdUri jpeg 14000
|
|
uri `shouldStartWith` "data:image/jpg;base64,"
|
|
length uri `shouldSatisfy` (<= 14000)
|
|
isOpen jpeg jpegFile `shouldReturn` False
|
|
piped <- pipeFd =<< B.readFile "./tests/fixtures/test.jpg"
|
|
pipedFile <- fdStat piped
|
|
pipedUri <- peekCAString =<< cChatResizeImageFdUri piped 14000
|
|
pipedUri `shouldStartWith` "data:image/jpg;base64,"
|
|
isOpen piped pipedFile `shouldReturn` False
|
|
pdf <- openFd "./tests/fixtures/test.pdf"
|
|
pdfFile <- fdStat pdf
|
|
(peekCAString =<< cChatResizeImageFd pdf 100000 (-1)) `shouldReturn` ""
|
|
isOpen pdf pdfFile `shouldReturn` False
|
|
dir <- withFilePath "./tests/fixtures" $ \path -> c_open path o_RDONLY 0
|
|
dirFile <- fdStat dir
|
|
(peekCAString =<< cChatResizeImageFdUri dir 14000) `shouldReturn` ""
|
|
isOpen dir dirFile `shouldReturn` False
|
|
(peekCAString =<< cChatResizeImageFdUri 1000000 14000) `shouldReturn` ""
|
|
(peekCAString =<< cChatResizeImageFdUri (-1) 14000) `shouldReturn` ""
|
|
oversized <- openFd =<< oversizedJpegFile ps
|
|
oversizedFile <- fdStat oversized
|
|
(peekCAString =<< cChatResizeImageFdUri oversized 14000) `shouldReturn` ""
|
|
isOpen oversized oversizedFile `shouldReturn` False
|
|
where
|
|
openFd path = FD.fdFD . fst <$> FD.openFile path ReadMode False
|
|
isOpen fd file = either (const False) (== file) <$> tryIOError (fdStat fd)
|
|
pipeFd s = do
|
|
(r, w) <- createPipeFd
|
|
h <- fdToHandle w
|
|
_ <- forkIO $ B.hPut h s >> hClose h
|
|
pure r
|
|
|
|
oversizedJpegFile :: TestParams -> IO FilePath
|
|
oversizedJpegFile ps = do
|
|
let path = tmpPath ps </> "oversized.jpg"
|
|
copyFile "./tests/fixtures/test.jpg" path
|
|
withBinaryFile path ReadWriteMode (`hSetFileSize` (fromIntegral maxDecodeMemoryMB * 1048576 + 1))
|
|
pure path
|
|
|
|
testResizeMalformedPngCApi :: TestParams -> IO ()
|
|
testResizeMalformedPngCApi _ = do
|
|
refused pngShortImageData
|
|
refused pngSideAbove16384
|
|
where
|
|
refused png = do
|
|
(ptr, len) <- cBuffer png
|
|
(peekCAString =<< cChatResizeImageUri ptr len 14000) `shouldReturn` ""
|
|
(peekCAString =<< cChatResizeImage ptr len 261120 (-1)) `shouldReturn` ""
|
|
|
|
cBuffer :: ByteString -> IO (Ptr Word8, CInt)
|
|
cBuffer s = do
|
|
ptr <- mallocBytes $ B.length s
|
|
putByteString ptr s
|
|
pure (ptr, fromIntegral $ B.length s)
|
|
|
|
-- Decode an FFI `BadgeResult` envelope, returning the result or failing on error.
|
|
ffiResult :: FromJSON r => String -> IO r
|
|
ffiResult s = case J.eitherDecode (LB.pack s) of
|
|
Right (BadgeResult r) -> pure r
|
|
Right (BadgeError e) -> error $ "badge FFI error: " <> show e
|
|
Left e -> error $ "badge FFI decode failed: " <> e <> " in " <> s
|
|
|
|
jDecode :: FromJSON a => String -> IO (Maybe a)
|
|
jDecode = pure . J.decode . LB.pack
|
|
|
|
encodedCString :: StrEncoding a => a -> IO CString
|
|
encodedCString = newCAString . BS.unpack . strEncode
|