Skip to content

Commit c381613

Browse files
committed
Decode as UTF-8 in ExitCodeException's Show instance
Partial fix for #86. This isn't perfect (for retrocomputing you may want another character encoding) but improves the behavior in many circumstances.
1 parent 685a67a commit c381613

File tree

3 files changed

+118
-6
lines changed

3 files changed

+118
-6
lines changed

package.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies:
2121
- stm
2222
- transformers
2323
- unliftio-core
24+
- text
2425

2526
library:
2627
source-dirs: src

src/System/Process/Typed/Internal.hs

+13-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import qualified Control.Exception as E
1717
import Control.Exception hiding (bracket, finally, handle)
1818
import Control.Monad (void)
1919
import qualified System.Process as P
20+
import qualified Data.Text as T
21+
import Data.Text.Encoding.Error (lenientDecode)
22+
import qualified Data.Text.Lazy as TL (toStrict)
23+
import qualified Data.Text.Lazy.Encoding as TLE
2024
import Data.Typeable (Typeable)
2125
import System.IO (Handle, hClose, IOMode(ReadWriteMode), withBinaryFile)
2226
import Control.Concurrent.Async (async)
@@ -616,17 +620,20 @@ data ExitCodeException = ExitCodeException
616620
instance Exception ExitCodeException
617621
instance Show ExitCodeException where
618622
show ece =
619-
let stdout = L8.unpack $ eceStdout ece
620-
stderr = L8.unpack $ eceStderr ece
621-
stdout' = if L.null (eceStdout ece)
623+
let decode = TL.toStrict . TLE.decodeUtf8With lenientDecode
624+
625+
stdout = decode $ eceStdout ece
626+
stderr = decode $ eceStderr ece
627+
628+
stdout' = if T.null stdout
622629
then []
623630
else [ "\n\nStandard output:\n"
624-
, stdout
631+
, T.unpack stdout
625632
]
626-
stderr' = if L.null (eceStderr ece)
633+
stderr' = if T.null stderr
627634
then []
628635
else [ "\nStandard error:\n"
629-
, stderr
636+
, T.unpack stderr
630637
]
631638
in concat $
632639
[ "Received "

test/System/Process/TypedSpec.hs

+104
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import qualified Data.ByteString.Lazy as L
1515
import Data.String (IsString(..))
1616
import Data.Monoid ((<>))
1717
import qualified Data.ByteString.Base64 as B64
18+
import GHC.Exts (IsList(fromList))
1819

1920
#if !MIN_VERSION_base(4, 8, 0)
2021
import Control.Applicative ((*>))
@@ -341,3 +342,106 @@ spec = do
341342
++ "puppy\n"
342343
++ "Standard error:\n"
343344
++ "doggy"
345+
346+
it "decodes UTF-8" $ do
347+
let exitCodeException =
348+
ExitCodeException
349+
{ eceExitCode = ExitFailure 1
350+
, eceProcessConfig = proc "puppy" []
351+
, eceStdout = fromList [0x61, 0xc2, 0xa9, 0xe2, 0x82, 0xac, 0xf0, 0x9f, 0x92, 0xa9, 0x0a]
352+
, eceStderr = fromList [0x61, 0xc2, 0xa9, 0xe2, 0x82, 0xac, 0xf0, 0x9f, 0x92, 0xa9, 0x0a]
353+
}
354+
show exitCodeException `shouldBe`
355+
"Received ExitFailure 1 when running\n"
356+
++ "Raw command: puppy\n"
357+
++ "\n"
358+
++ "Standard output:\n"
359+
++ "a©€💩\n"
360+
++ "\n"
361+
++ "Standard error:\n"
362+
++ "a©€💩\n"
363+
364+
it "decodes UTF-8 leniently (overlong)" $ do
365+
let exitCodeException =
366+
ExitCodeException
367+
{ eceExitCode = ExitFailure 1
368+
, eceProcessConfig = proc "puppy" []
369+
, -- Overlong sequence, U+20AC € encoded as 4 bytes.
370+
-- We get four U+FFFD � replacement characters out, one
371+
-- for each byte in the sequence.
372+
eceStdout = fromList [ 0xf0, 0x82, 0x82, 0xac, 0x0a ]
373+
, eceStderr = L.empty
374+
}
375+
show exitCodeException `shouldBe`
376+
"Received ExitFailure 1 when running\n"
377+
++ "Raw command: puppy\n"
378+
++ "\n"
379+
++ "Standard output:\n"
380+
++ "����\n"
381+
382+
it "decodes UTF-8 leniently (lone surrogate)" $ do
383+
let exitCodeException =
384+
ExitCodeException
385+
{ eceExitCode = ExitFailure 1
386+
, eceProcessConfig = proc "puppy" []
387+
, -- Half of a surrogate pair, invalid in UTF-8. (U+D800)
388+
eceStdout = fromList [ 0xed, 0xa0, 0x80, 0x0a]
389+
, eceStderr = L.empty
390+
}
391+
show exitCodeException `shouldBe`
392+
"Received ExitFailure 1 when running\n"
393+
++ "Raw command: puppy\n"
394+
++ "\n"
395+
++ "Standard output:\n"
396+
++ "���\n"
397+
398+
it "decodes UTF-8 leniently (unexpected continuation)" $ do
399+
let exitCodeException =
400+
ExitCodeException
401+
{ eceExitCode = ExitFailure 1
402+
, eceProcessConfig = proc "puppy" []
403+
, -- An unexpected continuation byte.
404+
eceStdout = fromList [ 0xa0, 0x80, 0x0a]
405+
, eceStderr = L.empty
406+
}
407+
show exitCodeException `shouldBe`
408+
"Received ExitFailure 1 when running\n"
409+
++ "Raw command: puppy\n"
410+
++ "\n"
411+
++ "Standard output:\n"
412+
++ "��\n"
413+
414+
it "decodes UTF-8 leniently (missing continuation)" $ do
415+
let exitCodeException =
416+
ExitCodeException
417+
{ eceExitCode = ExitFailure 1
418+
, eceProcessConfig = proc "puppy" []
419+
, -- Missing a continuation byte.
420+
eceStdout = fromList [ 0xf0, 0x9f, 0x90, 0x0a]
421+
, eceStderr = L.empty
422+
}
423+
show exitCodeException `shouldBe`
424+
"Received ExitFailure 1 when running\n"
425+
++ "Raw command: puppy\n"
426+
++ "\n"
427+
++ "Standard output:\n"
428+
++ "���\n"
429+
430+
it "decodes UTF-8 leniently (invalid byte)" $ do
431+
let exitCodeException =
432+
ExitCodeException
433+
{ eceExitCode = ExitFailure 1
434+
, eceProcessConfig = proc "puppy" []
435+
, -- Invalid bytes (no defined meaning in UTF-8).
436+
eceStdout = fromList [ 0xc0, 0x0a, 0xc1, 0x0a, 0xf5, 0x0a, 0xff, 0x0a]
437+
, eceStderr = L.empty
438+
}
439+
show exitCodeException `shouldBe`
440+
"Received ExitFailure 1 when running\n"
441+
++ "Raw command: puppy\n"
442+
++ "\n"
443+
++ "Standard output:\n"
444+
++ "\n"
445+
++ "\n"
446+
++ "\n"
447+
++ "\n"

0 commit comments

Comments
 (0)