11package io .shiftleft .utils
22
33import java .io .Reader
4- import java .nio .charset .{CharsetDecoder , CodingErrorAction }
5- import java .nio .file .Path
6- import java .util .regex .Pattern
4+ import java .nio .charset .{CharsetDecoder , CodingErrorAction , StandardCharsets }
5+ import java .nio .file .{Files , Path }
76import scala .io .{BufferedSource , Codec , Source }
8- import scala .jdk .CollectionConverters .IteratorHasAsScala
97import scala .util .Using
108
119object IOUtils {
1210
13- private val boms : Set [Char ] = Set (
14- '\uefbb ' , // UTF-8
15- '\ufeff ' , // UTF-16 (BE)
16- '\ufffe ' // UTF-16 (LE)
17- )
18-
1911 /** Creates a new UTF-8 decoder. Sadly, instances of CharsetDecoder are not thread-safe as the doc states: 'Instances
2012 * of this class are not safe for use by multiple concurrent threads.' (copied from:
2113 * [[java.nio.charset.CharsetDecoder ]])
@@ -28,43 +20,30 @@ object IOUtils {
2820 .onMalformedInput(CodingErrorAction .REPLACE )
2921 .onUnmappableCharacter(CodingErrorAction .REPLACE )
3022
23+ /** Skips a leading byte order mark (BOM) if present. Once decoded as UTF-8, a BOM shows up as the single character
24+ * U+FEFF (e.g. the UTF-8 BOM bytes EF BB BF decode to U+FEFF).
25+ */
3126 private def skipBOMIfPresent (reader : Reader ): Unit = {
3227 reader.mark(1 )
33- val possibleBOM = new Array [Char ](1 )
34- reader.read(possibleBOM)
35- if (! boms.contains(possibleBOM(0 ))) {
28+ if (reader.read() != '\ufeff ' ) {
3629 reader.reset()
3730 }
3831 }
3932
4033 private def contentFromBufferedSource (bufferedSource : BufferedSource ): Seq [String ] = {
4134 val reader = bufferedSource.bufferedReader()
4235 skipBOMIfPresent(reader)
43- reader.lines().iterator().asScala.toSeq
44- }
45-
46- private def contentStringFromBufferedSource (bufferedSource : BufferedSource ): String = {
47- val reader = bufferedSource.bufferedReader()
48- val stringBuilder = new StringBuilder
49- val bufferSize = 1024
50- var productive = true
51-
52- skipBOMIfPresent(reader)
53- while (productive) {
54- val buffer = new Array [Char ](bufferSize)
55- val read = reader.read(buffer)
56- productive = read > 0
57- if (productive) {
58- stringBuilder.appendAll(buffer, 0 , read)
59- }
36+ val lines = List .newBuilder[String ]
37+ var line = reader.readLine()
38+ while (line != null ) {
39+ lines += line
40+ line = reader.readLine()
6041 }
61-
62- stringBuilder.toString
42+ lines.result()
6343 }
6444
6545 /** Reads a file at the given path and:
6646 * - skips BOM if present
67- * - removes unpaired surrogates
6847 * - uses UTF-8 encoding (replacing malformed and unmappable characters)
6948 *
7049 * @param path
@@ -77,15 +56,25 @@ object IOUtils {
7756
7857 /** Reads a file at the given path and:
7958 * - skips BOM if present
80- * - removes unpaired surrogates
8159 * - uses UTF-8 encoding (replacing malformed and unmappable characters)
8260 *
8361 * @param path
8462 * the file path
8563 * @return
8664 * a String with the given file's contents
8765 */
88- def readEntireFile (path : Path ): String =
89- Using .resource(Source .fromFile(path.toFile)(createDecoder()))(contentStringFromBufferedSource)
66+ def readEntireFile (path : Path ): String = {
67+ val bytes = Files .readAllBytes(path)
68+ // Strip the UTF-8 BOM (EF BB BF) if present. The String constructor replaces malformed and unmappable input with
69+ // the default replacement character, i.e. decoding is lenient just like in readLinesInFile above.
70+ val offset = if (startsWithUtf8Bom(bytes)) Utf8BomLength else 0
71+ new String (bytes, offset, bytes.length - offset, StandardCharsets .UTF_8 )
72+ }
73+
74+ private val Utf8BomLength = 3
75+
76+ private def startsWithUtf8Bom (bytes : Array [Byte ]): Boolean =
77+ bytes.length >= Utf8BomLength &&
78+ bytes(0 ) == 0xef .toByte && bytes(1 ) == 0xbb .toByte && bytes(2 ) == 0xbf .toByte
9079
9180}
0 commit comments