commit 9e2fecffbe3b95f7663524536aa41b3f5e7bc179 Author: Dmitriy Pleshevskiy Date: Sat Apr 13 17:28:26 2024 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4941cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# editors +.idea/ +.vscode/ +*.swp +# direnv +.direnv +.envrc +# nix +/result \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1cc9c8e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for picsg + +## 0.1.0.0 -- 2024-04-13 + +- First version. Released on an unsuspecting world. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..342c588 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2022 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..255141b --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# picsg + +A tool for steganographing information in a picture encoded using the Vernam +cipher. + +## Synopsis + +```sh +picsg [subcommand] +``` + +where subcommand is one of the following: + +- `encode [--img ]` +- `encode file [--img ]` +- `decode [--img] ` + +## Examples + +- Encode text using the Vernam cipher to the file `hello.enc` and create a file + `hello.enc.key` with a key to decode the `hello.enc` file. + + `picsg encode "ABC" hello.enc` + +- Decode the `hello.enc` file using key and print to the standard output + + `picsg decode hello.enc hello.enc.key -` diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/app/DecodeSteg.hs b/app/DecodeSteg.hs new file mode 100644 index 0000000..a745963 --- /dev/null +++ b/app/DecodeSteg.hs @@ -0,0 +1,138 @@ +{-| Module for decoding the information hidden in the image using LSB steganography method +-} +module DecodeSteg ( + -- * Functions required for decoding information hidden in image + getWidth, getHeight, getRed, getGreen, getBlue + , getTotalNumPixels, getTotalBits, getPixelsforMessage + , getOptimumBits, readPixelBit, readBitOfImage, readByte + , readFileName, readAllBytes, decodeImg ) where + +import Codec.Picture +import Data.Bits +import Data.Char +import Data.Word +import qualified Data.ByteString.Lazy as B +import qualified Data.ByteString.Internal as BI + +-- | Obtain width of image +getWidth :: Image a -> Int +getWidth (Image w _ _) = w + +-- | Obtain height of image +getHeight :: Image a -> Int +getHeight (Image _ h _) = h + +-- | Obtain 8 bit red channel values from RGB8pixel of image +getRed :: PixelRGB8 -> Pixel8 +getRed (PixelRGB8 r _ _) = r + +-- | Obtain 8 bit green channel values from RGB8pixel of image +getGreen :: PixelRGB8 -> Pixel8 +getGreen (PixelRGB8 _ g _) = g + +-- | Obtain 8 bit blue channel values from RGB8pixel of image +getBlue :: PixelRGB8 -> Pixel8 +getBlue (PixelRGB8 _ _ b) = b + +-- | Get total number of pixels available to hide information in image +getTotalNumPixels :: Image PixelRGB8 -> Int +getTotalNumPixels img = getWidth img * getHeight img - 64 - 1 + +-- | Get total number of bits in which information can be hidden +getTotalBits :: Image PixelRGB8 -> Int -> Int +getTotalBits img bitsPerPixel = (getWidth img * getHeight img - 64 - 1) + * bitsPerPixel * 3 + +-- | Get number of pixels in which information is hidden +getPixelsforMessage :: Int -> Int -> Int +getPixelsforMessage len bitsPerPixel = div (len * 8) (bitsPerPixel * 3) + +-- | Get optimum number of last significant bits in pixel to hide the information in image +getOptimumBits :: Image PixelRGB8 -> Int -> Int +getOptimumBits img bytes = max + (ceiling ((toRational (bytes * 8)) + / (toRational ((getTotalNumPixels img) * 3)))) 1 + +-- | A delimiter used to separate file name and its data in encoded image +nullWord8 = fromIntegral 0 + +-- | Test whether the pixel at given index is 0 or 1 +readPixelBit :: Pixel8 -> Int -> Bool +readPixelBit px idx = testBit px idx + +-- | Identifies which color channel of pixel is to be read and returns the bit at that color channel +readBitOfImage :: Image PixelRGB8 -> Int -> Int -> Int -> Int -> Double -> Bool +readBitOfImage img bitsPerPixel byteIdx bitIdx offset period + | color == 0 = readPixelBit (getRed (pixelAt img px py)) lsbIdx + | color == 1 = readPixelBit (getGreen (pixelAt img px py)) lsbIdx + | color == 2 = readPixelBit (getBlue (pixelAt img px py)) lsbIdx + where + pos = floor (fromIntegral (byteIdx * 8 + bitIdx) * period) + + pixIdx = div pos (bitsPerPixel * 3) + offset + + px = mod pixIdx (getWidth img) + + py = div pixIdx (getWidth img) + + color = div (mod pos (bitsPerPixel * 3)) bitsPerPixel + + lsbIdx = mod (mod pos (bitsPerPixel * 3)) bitsPerPixel + +{-| Converts a sequence of bits to Word8 +-} +boolToWord8 :: [ Bool ] -> Word8 +boolToWord8 = foldl (\byte bit -> byte * 2 + if bit then 1 else 0) 0 + +-- | Reads all the 8 bits of a byte (a character at given index of hidden information) +readByte :: Image PixelRGB8 -> Int -> Int -> Int -> Double -> Word8 +readByte img idx lsb offset period = boolToWord8 + [ readBitOfImage img lsb idx i offset period | i <- reverse [ 0 .. 7 ] ] + +-- | Reads the lsb bits of pixel from given starting index in which filename is stored till nullWord8 is encountered (delimiter) +readFileName :: Image PixelRGB8 -> Int -> Int -> Int -> Double -> [ Word8 ] +readFileName img start lsb offset period + | byte == nullWord8 = [] + | otherwise = [ byte ] ++ readFileName img (start + 1) lsb offset period + where + byte = readByte img start lsb offset period + +-- | Read all the bytes of image in which information is hidden +readAllBytes + :: Image PixelRGB8 -> Int -> Int -> Int -> Int -> Double -> [ Word8 ] +readAllBytes img start lsb len offset period + | len <= 0 = [] + | otherwise = (readByte img start lsb offset period) + : (readAllBytes img (start + 1) lsb (len - 1) offset period) + +{- | Convert Word8 to Int32 + -} +word8ToInt32 :: [ Word8 ] -> Int +word8ToInt32 [] = 0 +word8ToInt32 [ x ] = fromIntegral x +word8ToInt32 (x : xs) = (fromIntegral x) + shiftL (word8ToInt32 xs) 8 + +{- | Convert a sequence of Word8 to string. + -} +word8ListToString :: [ Word8 ] -> String +word8ListToString list = map BI.w2c list + +-- | Decrypt (read) bytes from file in which filename, filepath, information is hidden and writes it in a file +decodeImg :: Image PixelRGB8 -> IO [ Word8 ] +decodeImg img = return file + where + len = fromIntegral + (word8ToInt32 (readAllBytes img 0 1 4 0 + (63 / (fromIntegral (getPixelsforMessage 4 1))))) + + bitsPerPixel = getOptimumBits img len + + name = readFileName img 0 bitsPerPixel 64 + (fromIntegral (getTotalBits img bitsPerPixel) / fromIntegral (len * 8)) + + filePath = word8ListToString name + + file = readAllBytes img (length name + 1) bitsPerPixel + (len - (length name + 1)) 64 + ((fromIntegral (getTotalBits img bitsPerPixel)) + / (fromIntegral (len * 8))) diff --git a/app/EncodeSteg.hs b/app/EncodeSteg.hs new file mode 100644 index 0000000..4d4119f --- /dev/null +++ b/app/EncodeSteg.hs @@ -0,0 +1,137 @@ +{-| Module for encoding (hiding) text file or image into another image using LSB Steganography. +-} +module EncodeSteg ( getWidth, getHeight, getRed, getGreen, getBlue + , getTotalNumPixels, getTotalBits, getPixelsforMessage + , getMinBits, getChangedPixel, encodeImage + , writeBitToImage ) where + +import Codec.Picture +import Control.Monad.ST +import Data.Bits +import qualified Codec.Picture.Types as M +import Data.Word + +{-| Returns width of image. +-} +getWidth :: Image a -> Int +getWidth (Image w _ _) = w + +{-| Returns height of image. +-} +getHeight :: Image a -> Int +getHeight (Image _ h _) = h + +{-| Returns red channel values from pixel +-} +getRed :: PixelRGB8 -> Pixel8 +getRed (PixelRGB8 r _ _) = r + +{-| Returns green channel values from pixel +-} +getGreen :: PixelRGB8 -> Pixel8 +getGreen (PixelRGB8 _ g _) = g + +{-| Returns blue channel values from pixel +-} +getBlue :: PixelRGB8 -> Pixel8 +getBlue (PixelRGB8 _ _ b) = b + +{-| Returns total number of pixels +-} +getTotalNumPixels :: Image PixelRGB8 -> Int +getTotalNumPixels img = (getWidth img) * (getHeight img) - 64 - 1 + +{-| Returns total number of bits +-} +getTotalBits :: Image PixelRGB8 -> Int -> Int +getTotalBits img bitsPerPixel = ((getWidth img) * (getHeight img) - 64 - 1) + * bitsPerPixel * 3 + +{-| Returns total number of pixels to store message +-} +getPixelsforMessage :: Int -> Int -> Int +getPixelsforMessage len bitsPerPixel = div (len * 8) (bitsPerPixel * 3) + +{-| Returns minimum bits to modify required per pixel color channel +-} +getMinBits :: Image PixelRGB8 -> Int -> Int +getMinBits img bytes = max + (ceiling ((toRational (bytes * 8)) + / (toRational ((getTotalNumPixels img) * 3)))) 1 + +{-| Modifies pixel 'pix' based on value of 'b'. + bit i is a value with the ith bit set and all other bits clear. +-} +getChangedPixel :: Pixel8 -> Int -> Bool -> Pixel8 +getChangedPixel pix x b + | b = pix .|. bit x + | not b = pix .&. complement (bit x) + +{- | Converts an integer to sequence of bits. + testBit returns True if the nth bit of the argument is 1. +-} +intToBits :: Bits a => a -> Int -> [ Bool ] +intToBits x idx = map (testBit x) [ 0 .. idx - 1 ] + +{- | Convert Int32 to Word8 + -} +int32ToWord8 :: Int -> [ Word8 ] +int32ToWord8 x = map fromIntegral + [ (shiftR x (pos * 8)) .&. 255 | pos <- [ 0, 1, 2, 3 ] ] + +{-| Hides data 'message' in image. + Returns new image similar to old image but with data hidden inside it. +-} +encodeImage :: Image PixelRGB8 -> Int -> [ Word8 ] -> Either String + (Image PixelRGB8) +encodeImage img bitsPerPixel message + | bitsPerPixel <= 8 = Right + (runST $ do + newimg <- M.unsafeThawImage img + let modifyBits _i [] _start _b _period = M.freezeImage newimg + modifyBits i (x : xs) start b period = do + writeBitToImage img newimg b i 0 (bits !! 0) start period + writeBitToImage img newimg b i 1 (bits !! 1) start period + writeBitToImage img newimg b i 2 (bits !! 2) start period + writeBitToImage img newimg b i 3 (bits !! 3) start period + writeBitToImage img newimg b i 4 (bits !! 4) start period + writeBitToImage img newimg b i 5 (bits !! 5) start period + writeBitToImage img newimg b i 6 (bits !! 6) start period + writeBitToImage img newimg b i 7 (bits !! 7) start period + modifyBits (i + 1) xs start b period + where + bits = intToBits (x) 8 + modifyBits 0 (int32ToWord8 len) 0 1 + (63 / (fromIntegral (getPixelsforMessage 4 1))) + modifyBits 0 message 64 bitsPerPixel + ((toRational (getTotalBits img bitsPerPixel)) + / (toRational (len * 8)))) + | otherwise = Left "Too long information to be encoded!!!" + where + len = length message + +{-| Writes data to image based on bits of message. +-} +writeBitToImage orig img bitsPerPixel byteIdx bitIdx bitVal offset period + | color == 0 = M.writePixel img px py + (PixelRGB8 (getChangedPixel (getRed (pixelAt orig px py)) lsbIdx bitVal) + (getGreen (pixelAt orig px py)) (getBlue (pixelAt orig px py))) + | color == 1 = M.writePixel img px py + (PixelRGB8 (getRed (pixelAt orig px py)) + (getChangedPixel (getGreen (pixelAt orig px py)) lsbIdx bitVal) + (getBlue (pixelAt orig px py))) + | color == 2 = M.writePixel img px py + (PixelRGB8 (getRed (pixelAt orig px py)) (getGreen (pixelAt orig px py)) + (getChangedPixel (getBlue (pixelAt orig px py)) lsbIdx bitVal)) + where + pos = floor (fromIntegral (byteIdx * 8 + bitIdx) * period) + + p = (div pos (bitsPerPixel * 3)) + offset + + px = mod p (getWidth orig) + + py = div p (getWidth orig) + + color = (div (mod pos (bitsPerPixel * 3)) bitsPerPixel) + + lsbIdx = (mod (mod pos (bitsPerPixel * 3)) bitsPerPixel) diff --git a/app/Main.hs b/app/Main.hs new file mode 100644 index 0000000..697b01c --- /dev/null +++ b/app/Main.hs @@ -0,0 +1,114 @@ +{-# LANGUAGE LambdaCase #-} + +module Main where + +import Codec.Picture +import Data.Bits (xor) +import qualified Data.ByteString.Lazy as BL +import qualified Data.ByteString.Lazy.UTF8 as BLU +import Data.Char (chr, ord) +import Data.Word (Word8) +import DecodeSteg +import EncodeSteg +import System.Environment (getArgs) +import System.Random (randomRIO) + +generateKey :: Int -> IO [ Int ] +generateKey n = sequence $ replicate n $ randomRIO ( 0, 65535 ) + +encodeText :: String -> [ Word8 ] -> IO [ Word8 ] +encodeText keypath content = do + key <- generateKey . length $ content + BL.writeFile keypath $ BL.pack (map fromIntegral key) + return $ map (\( a, b ) -> a `xor` b) $ zip content (map fromIntegral key) + +decodeText :: [ Word8 ] -> [ Word8 ] -> [ Word8 ] +decodeText encodedText key = map (\( a, b ) -> a `xor` b) $ zip encodedText key + +main :: IO () +main = getArgs >>= \case + [ "encode", "file", filepath, encpath, "--img", imgpath ] -> do + let keypath = encpath ++ ".key" + filecontent <- BL.unpack <$> BL.readFile filepath + encoded <- encodeText keypath filecontent + readImage imgpath >>= \case + Left err -> putStrLn err + Right image -> do + let conv = (convertRGB8 image) + len = (length filepath) + 1 + (length encoded) + bitsPerPixel = getMinBits conv len + message = (BL.unpack . BLU.fromString $ filepath) + ++ [ fromIntegral 0 ] ++ encoded + finalimg = (encodeImage conv bitsPerPixel message) + case finalimg of + Left errorStr -> putStrLn errorStr + Right encrypted -> do + savePngImage encpath $ ImageRGB8 encrypted + putStrLn ("Done") + [ "encode", "file", filepath, encpath ] -> do + let keypath = encpath ++ ".key" + filecontent <- BL.unpack <$> BL.readFile filepath + encoded <- encodeText keypath filecontent + BL.writeFile encpath $ BL.pack $ encoded + [ "encode", text, encpath, "--img", imgpath ] -> do + let keypath = encpath ++ ".key" + encoded <- encodeText keypath $ BL.unpack . BLU.fromString $ text + readImage imgpath >>= \case + Left err -> putStrLn err + Right image -> do + let conv = (convertRGB8 image) + len = 1 + (length encoded) + bitsPerPixel = getMinBits conv len + message = [ fromIntegral 0 ] ++ encoded + finalimg = (encodeImage conv bitsPerPixel message) + case finalimg of + Left errorStr -> putStrLn errorStr + Right encrypted -> do + savePngImage encpath $ ImageRGB8 encrypted + putStrLn ("Done") + [ "encode", text, encpath ] -> do + let keypath = encpath ++ ".key" + encoded <- encodeText keypath $ BL.unpack . BLU.fromString $ text + BL.writeFile encpath $ BL.pack $ encoded + [ "decode", "--img", imgpath, keypath, decodepath ] -> do + readImage imgpath >>= \case + Left err -> putStrLn err + Right image -> do + encodedText <- decodeImg . convertRGB8 $ image + key <- BL.unpack <$> BL.readFile keypath + printOrWriteFile decodepath $ BL.pack + $ decodeText encodedText key + [ "decode", encpath, keypath, decodepath ] -> do + encodedText <- BL.unpack <$> BL.readFile encpath + key <- BL.unpack <$> BL.readFile keypath + printOrWriteFile decodepath $ BL.pack $ decodeText encodedText key + _ -> help + where + printOrWriteFile filepath + = if filepath == "-" then print else BL.writeFile filepath + +help :: IO () +help = do + putStrLn "Name" + putStrLn "" + putStrLn " picsg - a tool for steganographing information in a picture encoded using the Vernam cipher." + putStrLn "" + putStrLn "Synopsis" + putStrLn "" + putStrLn " picsg [subcommand]" + putStrLn "" + putStrLn " where subcommand is one of the following:" + putStrLn "" + putStrLn " - encode [--img ]" + putStrLn " - encode file [--img ]" + putStrLn " - decode [--img] " + putStrLn "" + putStrLn "Examples" + putStrLn "" + putStrLn " - Encode text using the Vernam cipher to the file hello.enc and create a file hello.enc.key with a key to decode the hello.enc file." + putStrLn "" + putStrLn " | picsg encode \"ABC\" hello.enc" + putStrLn "" + putStrLn " - Decode the hello.enc file using key and print to the standart output" + putStrLn "" + putStrLn " | picsg decode hello.enc hello.enc.key -" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9e0cdb6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1712849433, + "narHash": "sha256-flQtf/ZPJgkLY/So3Fd+dGilw2DKIsiwgMEn7BbBHL0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f173d0881eff3b21ebb29a2ef8bedbc106c86ea5", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..590e021 --- /dev/null +++ b/flake.nix @@ -0,0 +1,47 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + hPkgs = pkgs.haskellPackages; + + myDevTools = with hPkgs; [ + ghc + ghcid + ormolu + hlint + hoogle + haskell-language-server + implicit-hie + retrie + ]; + + picsg = pkgs.haskellPackages.developPackage { + root = ./.; + }; + in + { + + packages = { + default = picsg; + picsg = picsg; + docker = pkgs.dockerTools.buildImage { + name = "picgs"; + config = { + Cmd = [ "${picsg}/bin/picsg" ]; + }; + }; + }; + + devShells.default = pkgs.mkShell { + buildInputs = myDevTools ++ [ hPkgs.cabal-install ]; + + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath myDevTools; + }; + }); +} diff --git a/picsg.cabal b/picsg.cabal new file mode 100644 index 0000000..a478321 --- /dev/null +++ b/picsg.cabal @@ -0,0 +1,80 @@ +cabal-version: 3.0 +-- The cabal-version field refers to the version of the .cabal specification, +-- and can be different from the cabal-install (the tool) version and the +-- Cabal (the library) version you are using. As such, the Cabal (the library) +-- version used must be equal or greater than the version stated in this field. +-- Starting from the specification version 2.2, the cabal-version field must be +-- the first thing in the cabal file. + +-- Initial package description 'picsg' generated by +-- 'cabal init'. For further documentation, see: +-- http://haskell.org/cabal/users-guide/ +-- +-- The name of the package. +name: picsg + +-- The package version. +-- See the Haskell package versioning policy (PVP) for standards +-- guiding when and how versions should be incremented. +-- https://pvp.haskell.org +-- PVP summary: +-+------- breaking API changes +-- | | +----- non-breaking API additions +-- | | | +--- code changes with no API change +version: 0.1.0.0 + +-- A short (one-line) description of the package. +-- synopsis: + +-- A longer description of the package. +-- description: + +-- The license under which the package is released. +license: GPL-3.0-or-later + +-- The file containing the license text. +license-file: LICENSE + +-- The package author(s). +author: Dmitriy Pleshevskiy + +-- An email address to which users can send suggestions, bug reports, and patches. +maintainer: dmitriy@pleshevski.ru + +-- A copyright notice. +-- copyright: +build-type: Simple + +-- Extra doc files to be distributed with the package, such as a CHANGELOG or a README. +extra-doc-files: CHANGELOG.md + +-- Extra source files to be distributed with the package, such as examples, or a tutorial module. +-- extra-source-files: + +common warnings + ghc-options: -Wall + +executable picsg + -- Import common warning flags. + import: warnings + + -- .hs or .lhs file containing the Main module. + main-is: Main.hs + + -- Modules included in this executable, other than Main. + -- other-modules: + + -- LANGUAGE extensions used by modules in this package. + -- other-extensions: + + -- Other library packages from which modules are imported. + build-depends: base ^>=4.18.2.0, + utf8-string == 1.0.2, + random == 1.2.1.2, + bytestring == 0.11.5.3, + JuicyPixels == 3.3.8 + + -- Directories containing source files. + hs-source-dirs: app + + -- Base language which the package is written in. + default-language: Haskell2010