diff --git a/MDict.hs b/MDict.hs index 16098db..8c614fc 100644 --- a/MDict.hs +++ b/MDict.hs @@ -5,8 +5,9 @@ module MDict ( MDictType(..), withMDict, lookupWord, + locateWord, + parseDefinition, getKeys, - suggestWords, getFileType, listAllKeys ) where @@ -15,6 +16,7 @@ import Foreign import Foreign.C import Foreign.C.String import Foreign.C.Types +import Foreign.Ptr (Ptr, FunPtr, nullPtr, castFunPtr) import Foreign.ForeignPtr (ForeignPtr, newForeignPtr, withForeignPtr) import Control.Exception (bracket, throwIO) import Control.Monad (when) @@ -22,72 +24,62 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Internal as BSI import qualified Data.ByteString.Char8 as BSC import Data.List (intercalate) +import Data.Word (Word64) +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE --- C-compatible struct representation + +-- C-compatible struct data SimpleKeyItem = SimpleKeyItem - { recordStart :: CULong - , keyWordPtr :: CString + { recordStart :: Word64 + , keyWordPtr :: CString } instance Storable SimpleKeyItem where - sizeOf _ = 16 -- sizeof(simple_key_item) on 64-bit systems + sizeOf _ = 16 alignment _ = 8 - peek ptr = SimpleKeyItem - <$> peekByteOff ptr 0 -- record_start at offset 0 - <*> peekByteOff ptr 8 -- key_word at offset 8 - poke ptr (SimpleKeyItem rs kw) = do - pokeByteOff ptr 0 rs - pokeByteOff ptr 8 kw + peek ptr = SimpleKeyItem <$> peekByteOff ptr 0 <*> peekByteOff ptr 8 + poke ptr (SimpleKeyItem rs kw) = pokeByteOff ptr 0 rs >> pokeByteOff ptr 8 kw -- Dictionary handle newtype MDict = MDict (ForeignPtr ()) --- Dictionary type enumeration +-- Dictionary type data MDictType = MDX | MDD deriving (Show, Eq, Enum) --- Foreign function imports -foreign import ccall "mdict_init" - c_mdict_init :: CString -> IO (Ptr ()) +-- FFI imports +foreign import ccall "mdict_init" c_mdict_init :: CString -> IO (Ptr ()) +foreign import ccall "mdict_destory" c_mdict_destroy :: Ptr () -> IO CInt +foreign import ccall "&mdict_destory" raw_mdict_destroy_finalizer :: FunPtr (Ptr () -> IO CInt) +foreign import ccall "mdict_lookup" c_mdict_lookup :: Ptr () -> CString -> Ptr (Ptr CChar) -> IO () +foreign import ccall "mdict_locate" c_mdict_locate :: Ptr () -> CString -> Ptr (Ptr CChar) -> CInt -> IO () +foreign import ccall "mdict_parse_definition" c_mdict_parse_definition :: Ptr () -> CString -> Word64 -> Ptr (Ptr CChar) -> IO () +foreign import ccall "mdict_keylist" c_mdict_keylist :: Ptr () -> Ptr Word64 -> IO (Ptr (Ptr SimpleKeyItem)) +foreign import ccall "free_simple_key_list" c_free_simple_key_list :: Ptr (Ptr SimpleKeyItem) -> Word64 -> IO CInt +foreign import ccall "mdict_filetype" c_mdict_filetype :: Ptr () -> IO CInt +foreign import ccall "mdict_suggest" c_mdict_suggest :: Ptr () -> CString -> Ptr (Ptr (Ptr CChar)) -> CInt -> IO () +foreign import ccall "mdict_stem" c_mdict_stem :: Ptr () -> CString -> Ptr (Ptr (Ptr CChar)) -> CInt -> IO () +foreign import ccall unsafe "string.h strlen" c_strlen :: CString -> IO CSize +-- Cast finalizer to expected type for newForeignPtr +castFinalizer :: FunPtr (Ptr () -> IO CInt) -> FinalizerPtr a +castFinalizer = castFunPtr --- we need to use it somewhere, to clean memory -foreign import ccall "mdict_destory" - c_mdict_destroy :: Ptr () -> IO () - -foreign import ccall "&mdict_destory" - c_mdict_destroy_finalizer :: FunPtr (Ptr () -> IO ()) - -foreign import ccall "mdict_lookup" - c_mdict_lookup :: Ptr () -> CString -> Ptr (Ptr CChar) -> IO () - -foreign import ccall "mdict_keylist" - c_mdict_keylist :: Ptr () -> Ptr CULong -> IO (Ptr (Ptr SimpleKeyItem)) - -foreign import ccall "free_simple_key_list" - c_free_simple_key_list :: Ptr (Ptr SimpleKeyItem) -> CULong -> IO CInt - -foreign import ccall "mdict_suggest" - c_mdict_suggest :: Ptr () -> CString -> Ptr (Ptr (Ptr CChar)) -> CInt -> IO CInt - -foreign import ccall "mdict_filetype" - c_mdict_filetype :: Ptr () -> IO CInt - --- Safe initialization/cleanup +-- Safe dictionary initialization withMDict :: FilePath -> (MDict -> IO a) -> IO a -withMDict path action = - withCString path $ \cpath -> - bracket - (do rawPtr <- c_mdict_init cpath - when (rawPtr == nullPtr) $ - throwIO (userError "Failed to initialize MDict") - fp <- newForeignPtr c_mdict_destroy_finalizer rawPtr - return (MDict fp)) - (\_ -> return ()) - action +withMDict path action = withCString path $ \cpath -> + bracket + (do rawPtr <- c_mdict_init cpath + when (rawPtr == nullPtr) $ throwIO (userError "Failed to initialize MDict") + fp <- newForeignPtr (castFinalizer raw_mdict_destroy_finalizer) rawPtr + return (MDict fp)) + (\_ -> return ()) -- ForeignPtr handles cleanup + action --- Dictionary lookup with raw bytes -lookupWord :: MDict -> String -> IO (Either String BS.ByteString) -lookupWord (MDict fptr) word = +-- Lookup +-- | Lookup a word and return decoded UTF-8 Text +lookupWord :: MDict -> String -> IO (Either String T.Text) +lookupWord (MDict fptr) word = withForeignPtr fptr $ \ptr -> withCString word $ \cword -> alloca $ \resultPtr -> do @@ -97,94 +89,74 @@ lookupWord (MDict fptr) word = then return $ Left "Word not found" else do len <- c_strlen result - bs <- BSI.create (fromIntegral len) $ \p -> - BSI.memcpy p (castPtr result) (fromIntegral len) - return $ Right bs + bs <- BSI.create (fromIntegral len) $ \p -> + BSI.memcpy p (castPtr result) (fromIntegral len) + -- Decode as UTF-8 + case TE.decodeUtf8' bs of + Left err -> return $ Left ("UTF-8 decode error: " ++ show err) + Right txt -> return $ Right txt --- Key list handling -type KeyEntry = (BS.ByteString, CULong) +-- Locate +locateWord :: MDict -> String -> Int -> IO (Either String BS.ByteString) +locateWord (MDict fptr) word enc = withForeignPtr fptr $ \ptr -> + withCString word $ \cword -> + alloca $ \resultPtr -> do + c_mdict_locate ptr cword resultPtr (fromIntegral enc) + result <- peek resultPtr + if result == nullPtr then return $ Left "Word not found" + else do + len <- c_strlen result + bs <- BSI.create (fromIntegral len) $ \p -> BSI.memcpy p (castPtr result) (fromIntegral len) + return $ Right bs +-- Parse definition +parseDefinition :: MDict -> String -> Word64 -> IO (Either String BS.ByteString) +parseDefinition (MDict fptr) word start = withForeignPtr fptr $ \ptr -> + withCString word $ \cword -> + alloca $ \resultPtr -> do + c_mdict_parse_definition ptr cword start resultPtr + result <- peek resultPtr + if result == nullPtr then return $ Left "Definition not found" + else do + len <- c_strlen result + bs <- BSI.create (fromIntegral len) $ \p -> BSI.memcpy p (castPtr result) (fromIntegral len) + return $ Right bs + +-- Get keys +type KeyEntry = (BS.ByteString, Word64) getKeys :: MDict -> IO (Either String [KeyEntry]) -getKeys (MDict fptr) = - withForeignPtr fptr $ \ptr -> - alloca $ \lenPtr -> do - arrPtr <- c_mdict_keylist ptr lenPtr - len <- peek lenPtr - if arrPtr == nullPtr - then return $ Left "Failed to retrieve key list" - else do - keyPtrs <- peekArray (fromIntegral len) arrPtr - items <- mapM (handleKeyItem arrPtr len) keyPtrs - freeResult <- c_free_simple_key_list arrPtr len - if freeResult /= 0 - then return $ Left "Failed to free key list memory" - else return $ Right items +getKeys (MDict fptr) = withForeignPtr fptr $ \ptr -> + alloca $ \lenPtr -> do + arrPtr <- c_mdict_keylist ptr lenPtr + len <- peek lenPtr + if arrPtr == nullPtr then return $ Left "Failed to retrieve key list" + else do + keyPtrs <- peekArray (fromIntegral len) arrPtr + items <- mapM handleItem keyPtrs + _ <- c_free_simple_key_list arrPtr len + return $ Right items where - handleKeyItem arrPtr len ptr = do + handleItem ptr = do item <- peek ptr - key <- packKeyBytes (keyWordPtr item) - return (key, recordStart item) - - packKeyBytes :: CString -> IO BS.ByteString - packKeyBytes cstr = do + bs <- packCString (keyWordPtr item) + return (bs, recordStart item) + packCString cstr = do len <- c_strlen cstr - BSI.create (fromIntegral len) $ \p -> - BSI.memcpy p (castPtr cstr) (fromIntegral len) + BSI.create (fromIntegral len) $ \p -> BSI.memcpy p (castPtr cstr) (fromIntegral len) --- Safe suggestion handling -suggestWords :: MDict -> String -> Int -> IO (Either String [BS.ByteString]) -suggestWords (MDict fptr) word maxSuggestions = - withForeignPtr fptr $ \ptr -> - withCString word $ \cword -> - alloca $ \resultsPtr -> do - rc <- c_mdict_suggest ptr cword resultsPtr (fromIntegral maxSuggestions) - if rc /= 0 - then return $ Left "Suggestion failed" - else do - suggestionsPtr <- peek resultsPtr - if suggestionsPtr == nullPtr - then return $ Right [] - else do - suggestionPtrs <- peekArray maxSuggestions suggestionsPtr - results <- traverse readSuggestion suggestionPtrs - return $ sequence results - where - readSuggestion :: Ptr CChar -> IO (Either String BS.ByteString) - readSuggestion ptr = do - if ptr == nullPtr - then return $ Left "Null suggestion pointer" - else do - len <- c_strlen ptr - bs <- BSI.create (fromIntegral len) $ \p -> - BSI.memcpy p (castPtr ptr) (fromIntegral len) - return $ Right bs - - --- File type detection +-- File type getFileType :: MDict -> IO (Either String MDictType) -getFileType (MDict fptr) = - withForeignPtr fptr $ \ptr -> do - ft <- c_mdict_filetype ptr - return $ case ft of - 0 -> Right MDX - 1 -> Right MDD - _ -> Left "Unknown file type" +getFileType (MDict fptr) = withForeignPtr fptr $ \ptr -> do + ft <- c_mdict_filetype ptr + return $ case ft of + 0 -> Right MDX + 1 -> Right MDD + _ -> Left "Unknown file type" --- Helper for C string length -foreign import ccall unsafe "string.h strlen" - c_strlen :: CString -> IO CSize - --- Formatted list output +-- List all keys listAllKeys :: MDict -> IO String listAllKeys dict = do - result <- getKeys dict - case result of + ekeys <- getKeys dict + case ekeys of Left err -> return $ "Error: " ++ err - Right keys -> return $ formatKeys keys - where - formatKeys :: [KeyEntry] -> String - formatKeys entries = intercalate "\n" $ map formatEntry entries - - formatEntry :: KeyEntry -> String - formatEntry (bs, offset) = - BSC.unpack (BSC.take 50 bs) ++ "... (" ++ show offset ++ ")" + Right keys -> return $ unlines [BSC.unpack (BSC.take 50 bs) ++ "... (" ++ show off ++ ")" | (bs, off) <- keys] diff --git a/cli-example/Main.hs b/cli-example/Main.hs index 32fcef0..ffb7817 100644 --- a/cli-example/Main.hs +++ b/cli-example/Main.hs @@ -1,52 +1,21 @@ {-# LANGUAGE OverloadedStrings #-} -module Main where -import MDict + import System.Environment (getArgs) -import System.IO (hSetBuffering, stdout, BufferMode(..)) -import Control.Exception (try, SomeException) -import Control.Monad (unless) -import System.Exit (exitSuccess) +import qualified Data.Text.IO as TIO +import qualified MDict as M --- Highest Priority: Safe dictionary initialization with error handling -loadDictionary :: FilePath -> IO (Either String MDict) -loadDictionary path = do - result <- try $ withMDict path (return . Right) - case result of - Left (e :: SomeException) -> return $ Left $ "Error loading dictionary: " ++ show e - Right dict -> return dict --- Medium Priority: Interactive lookup loop with graceful exit -lookupLoop :: MDict -> IO () -lookupLoop dict = do - putStr "Enter word to lookup (or :quit to exit): " - hSetBuffering stdout NoBuffering - input <- getLine - unless (input == ":quit") $ do - result <- lookupWord dict input - case result of - Just definition -> putStrLn $ "\nDefinition:\n" ++ definition ++ "\n" - Nothing -> putStrLn $ "\n'" ++ input ++ "' not found in dictionary.\n" - lookupLoop dict - putStrLn "Exiting..." - exitSuccess - --- Critical: Command-line argument validation main :: IO () main = do - hSetBuffering stdout NoBuffering args <- getArgs case args of - [mdxPath] -> do - putStrLn $ "Loading dictionary: " ++ mdxPath - eitherDict <- loadDictionary mdxPath - case eitherDict of - Left err -> putStrLn err - Right dict -> do - putStrLn "Dictionary loaded successfully!\n" - lookupLoop dict - _ -> putStrLn $ unlines - [ "Invalid arguments!" - , "Usage: mdict-lookup " - , "Example: mdict-lookup /dictionaries/cantonese.mdx" - ] + [dictPath, word] -> + M.withMDict dictPath $ \dict -> do + result <- M.lookupWord dict word + case result of + Left err -> putStrLn $ "Error: " ++ err + Right txt -> do + putStrLn "Definition:" + TIO.putStrLn txt + _ -> putStrLn "Usage: my_program "