diff --git a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaFiles.java b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaFiles.java
index f3e471e8615..90abd21453b 100644
--- a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaFiles.java
+++ b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaFiles.java
@@ -23,6 +23,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 @State(Scope.Benchmark)
@@ -38,10 +39,13 @@ public void setup() throws URISyntaxException, IOException {
             throw new RuntimeException("Unable to create directory");
         }
 
-        sourceFiles = new ArrayList<>(1_000);
-        for (int i = 0; i < 1_000; i++) {
-            Files.writeString(test.resolve("Test" + i + ".java"),
-                    "package test; class Test" + i + " {}");
+        sourceFiles = new ArrayList<>(100);
+        // to add some "meat to the bones"
+        String whitespace = String.join("", Collections.nCopies(1_000, " "));
+        for (int i = 0; i < 100; i++) {
+            Path path = test.resolve("Test" + i + ".java");
+            Files.writeString(path, "package test; class Test%d {%s}".formatted(i, whitespace));
+            sourceFiles.add(path);
         }
     }
 
diff --git a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/ParserInputBenchmark.java b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/ParserInputBenchmark.java
index d74728c6923..ab16e62bf78 100644
--- a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/ParserInputBenchmark.java
+++ b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/ParserInputBenchmark.java
@@ -16,6 +16,7 @@
 package org.openrewrite.benchmarks.java;
 
 import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.infra.Blackhole;
 import org.openjdk.jmh.runner.Runner;
 import org.openjdk.jmh.runner.RunnerException;
 import org.openjdk.jmh.runner.options.Options;
@@ -23,11 +24,9 @@
 import org.openrewrite.InMemoryExecutionContext;
 import org.openrewrite.Parser;
 import org.openrewrite.java.JavaParser;
+import org.openrewrite.tree.ParsingExecutionContextView;
 
-import java.io.BufferedInputStream;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Files;
+import java.nio.charset.StandardCharsets;
 import java.util.concurrent.TimeUnit;
 
 import static java.util.stream.Collectors.toList;
@@ -35,45 +34,35 @@
 @Fork(1)
 @Measurement(iterations = 2)
 @Warmup(iterations = 2)
-@BenchmarkMode(Mode.SampleTime)
-@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
 @Threads(4)
 public class ParserInputBenchmark {
 
     @Benchmark
-    public void readFromDisk(JavaFiles state) {
-        //language=java
+    public void detectCharset(JavaFiles state, Blackhole bh) {
         JavaParser.fromJavaVersion().build()
                 .parseInputs(state.getSourceFiles().stream()
-                                .map(sourceFile -> new Parser.Input(sourceFile, () -> {
-                                            try {
-                                                return Files.newInputStream(sourceFile);
-                                            } catch (IOException e) {
-                                                throw new UncheckedIOException(e);
-                                            }
-                                        })
-                                )
+                                .map(Parser.Input::fromFile)
                                 .collect(toList()),
                         null,
-                        new InMemoryExecutionContext());
+                        new InMemoryExecutionContext()
+                )
+                .forEach(bh::consume);
     }
 
     @Benchmark
-    public void readFromDiskWithBufferedInputStream(JavaFiles state) {
-        //language=java
+    public void knownCharset(JavaFiles state, Blackhole bh) {
+        ParsingExecutionContextView ctx = ParsingExecutionContextView.view(new InMemoryExecutionContext())
+                .setCharset(StandardCharsets.UTF_8);
         JavaParser.fromJavaVersion().build()
                 .parseInputs(state.getSourceFiles().stream()
-                                .map(sourceFile -> new Parser.Input(sourceFile, () -> {
-                                            try {
-                                                return new BufferedInputStream(Files.newInputStream(sourceFile));
-                                            } catch (IOException e) {
-                                                throw new UncheckedIOException(e);
-                                            }
-                                        })
-                                )
+                                .map(Parser.Input::fromFile)
                                 .collect(toList()),
                         null,
-                        new InMemoryExecutionContext());
+                        ctx
+                )
+                .forEach(bh::consume);
     }
 
     public static void main(String[] args) throws RunnerException {
diff --git a/rewrite-core/src/main/java/org/openrewrite/Parser.java b/rewrite-core/src/main/java/org/openrewrite/Parser.java
index fd308a2d86d..2b38025ff18 100644
--- a/rewrite-core/src/main/java/org/openrewrite/Parser.java
+++ b/rewrite-core/src/main/java/org/openrewrite/Parser.java
@@ -177,6 +177,16 @@ public static Input fromString(Path sourcePath, String source, Charset charset)
             return new Input(sourcePath, null, () -> new ByteArrayInputStream(source.getBytes(charset)), true);
         }
 
+        public static Input fromFile(Path sourcePath) {
+            return new Input(sourcePath, FileAttributes.fromPath(sourcePath), () -> {
+                try {
+                    return Files.newInputStream(sourcePath);
+                } catch (IOException e) {
+                    throw new UncheckedIOException(e);
+                }
+            }, false);
+        }
+
         @SuppressWarnings("unused")
         public static Input fromResource(String resource) {
             return new Input(
diff --git a/rewrite-core/src/main/java/org/openrewrite/internal/EncodingDetectingInputStream.java b/rewrite-core/src/main/java/org/openrewrite/internal/EncodingDetectingInputStream.java
index 1dae79b4763..648b5817952 100644
--- a/rewrite-core/src/main/java/org/openrewrite/internal/EncodingDetectingInputStream.java
+++ b/rewrite-core/src/main/java/org/openrewrite/internal/EncodingDetectingInputStream.java
@@ -20,17 +20,20 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.UncheckedIOException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 
 public class EncodingDetectingInputStream extends InputStream {
     private static final Charset WINDOWS_1252 = Charset.forName("Windows-1252");
+    private static final byte[] UTF8_BOM = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};
 
     private final InputStream inputStream;
 
     @Nullable
     private Charset charset;
 
+    private boolean bomChecked;
     private boolean charsetBomMarked;
 
     /**
@@ -38,15 +41,13 @@ public class EncodingDetectingInputStream extends InputStream {
      */
     private int prev;
     private int prev2;
-    private int prev3;
 
     boolean maybeTwoByteSequence = false;
     boolean maybeThreeByteSequence = false;
     boolean maybeFourByteSequence = false;
 
     public EncodingDetectingInputStream(InputStream inputStream) {
-        this.inputStream = inputStream;
-        this.charset = null;
+        this(inputStream, null);
     }
 
     public EncodingDetectingInputStream(InputStream inputStream, @Nullable Charset charset) {
@@ -64,71 +65,92 @@ public boolean isCharsetBomMarked() {
 
     @Override
     public int read() throws IOException {
-        int aByte = inputStream.read();
+        int read;
+        if (!bomChecked) {
+            if (charset == null || charset == StandardCharsets.UTF_8) {
+                read = checkAndSkipUtf8Bom();
+                if (charsetBomMarked) {
+                    read = inputStream.read();
+                }
+            } else {
+                bomChecked = true;
+                read = inputStream.read();
+            }
+        } else {
+            read = inputStream.read();
+        }
+
 
         // if we haven't yet determined a charset...
+        if (read == -1) {
+            if (charset == null) {
+                if (maybeTwoByteSequence || maybeThreeByteSequence || maybeFourByteSequence) {
+                    charset = WINDOWS_1252;
+                } else {
+                    charset = StandardCharsets.UTF_8;
+                }
+            }
+        } else if (charset == null) {
+            guessCharset(read);
+        }
+        return read;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
         if (charset == null) {
-            guessCharset(aByte);
+            // we need to read the bytes one-by-one to determine the encoding
+            return super.read(b, off, len);
+        } else if (charset == StandardCharsets.UTF_8 && !bomChecked) {
+            int read = checkAndSkipUtf8Bom();
+            if (read == -1) {
+                return -1;
+            } else if (!charsetBomMarked) {
+                b[off++] = (byte) read;
+            }
+            read = inputStream.read(b, off, len - 1);
+            return read == -1 ? charsetBomMarked ? -1 : 1 : (charsetBomMarked ? 0 : 1) + read;
+        } else {
+            return inputStream.read(b, off, len);
         }
-        return aByte;
     }
 
     private void guessCharset(int aByte) {
-        if (prev3 == 0xEF && prev2 == 0xBB && prev == 0xBF) {
-            charsetBomMarked = true;
-            charset = StandardCharsets.UTF_8;
-        } else {
-            if (aByte == -1 || !(prev2 == 0 && prev == 0xEF || prev3 == 0 && prev2 == 0xEF)) {
-                if (maybeTwoByteSequence) {
-                    if (aByte == -1 && !utf8SequenceEnd(prev) || aByte != -1 && !(utf8SequenceEnd(aByte))) {
-                        charset = WINDOWS_1252;
-                    } else {
-                        maybeTwoByteSequence = false;
-                        prev2 = -1;
-                        prev = -1;
-                    }
-                } else if (maybeThreeByteSequence) {
-                    if (aByte == -1 ||
-                            utf8SequenceEnd(prev) && !(utf8SequenceEnd(aByte)) ||
-                            !utf8SequenceEnd(aByte)) {
-                        charset = WINDOWS_1252;
-                    }
-
-                    if (utf8SequenceEnd(prev) && utf8SequenceEnd(aByte)) {
-                        maybeThreeByteSequence = false;
-                        prev2 = -1;
-                        prev = -1;
-                    }
-                } else if (maybeFourByteSequence) {
-                    if (aByte == -1 ||
-                            utf8SequenceEnd(prev2) && utf8SequenceEnd(prev) && !utf8SequenceEnd(aByte) ||
-                            utf8SequenceEnd(prev) && !utf8SequenceEnd(aByte) ||
-                            !(utf8SequenceEnd(aByte))) {
-                        charset = WINDOWS_1252;
-                    }
-
-                    if (utf8SequenceEnd(prev2) && utf8SequenceEnd(prev) && utf8SequenceEnd(aByte)) {
-                        maybeFourByteSequence = false;
-                        prev2 = -1;
-                        prev = -1;
-                    }
-                } else if (utf8TwoByteSequence(aByte)) {
-                    maybeTwoByteSequence = true;
-                } else if (utf8ThreeByteSequence(aByte)) {
-                    maybeThreeByteSequence = true;
-                } else if (utf8FourByteSequence(aByte)) {
-                    maybeFourByteSequence = true;
-                } else if (!utf8TwoByteSequence(prev) && utf8SequenceEnd(aByte)) {
-                    charset = WINDOWS_1252;
-                }
+        if (utf8TwoByteSequence(aByte)) {
+            maybeTwoByteSequence = true;
+        } else if (utf8ThreeByteSequence(aByte)) {
+            maybeThreeByteSequence = true;
+        } else if (utf8FourByteSequence(aByte)) {
+            maybeFourByteSequence = true;
+        } else if (maybeTwoByteSequence) {
+            if (!utf8SequenceEnd(aByte)) {
+                charset = WINDOWS_1252;
+            } else {
+                maybeTwoByteSequence = false;
+                prev = -1;
+            }
+        } else if (maybeThreeByteSequence) {
+            if (!utf8SequenceEnd(aByte)) {
+                charset = WINDOWS_1252;
             }
 
-            if (aByte == -1 && charset == null) {
-                charset = StandardCharsets.UTF_8;
+            if (utf8SequenceEnd(prev) && utf8SequenceEnd(aByte)) {
+                maybeThreeByteSequence = false;
+                prev = -1;
+            }
+        } else if (maybeFourByteSequence) {
+            if (utf8SequenceEnd(prev2) && utf8SequenceEnd(prev) && !utf8SequenceEnd(aByte) || utf8SequenceEnd(prev) && !utf8SequenceEnd(aByte) || !utf8SequenceEnd(aByte)) {
+                charset = WINDOWS_1252;
             }
+
+            if (utf8SequenceEnd(prev2) && utf8SequenceEnd(prev) && utf8SequenceEnd(aByte)) {
+                maybeFourByteSequence = false;
+                prev = -1;
+            }
+        } else if (utf8SequenceEnd(aByte)) {
+            charset = WINDOWS_1252;
         }
 
-        prev3 = prev2;
         prev2 = prev;
         prev = aByte;
     }
@@ -143,14 +165,36 @@ public synchronized String toString() {
             };
             byte[] buffer = new byte[4096];
             int n;
-            while ((n = is.read(buffer)) != -1) {
+            // Note that `is` is this, so the BOM will be checked in `read()`
+            while ((n = is.read(buffer, 0, buffer.length)) != -1) {
                 bos.write(buffer, 0, n);
             }
 
             return bos.toString();
         } catch (IOException e) {
-            throw new UnsupportedOperationException(e);
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    private int checkAndSkipUtf8Bom() throws IOException {
+        // `Files#newInputStream()` does not need to support mark/reset, so one at the time...
+        bomChecked = true;
+        int read = inputStream.read();
+        if ((byte) read != UTF8_BOM[0]) {
+            return read;
+        }
+        read = inputStream.read();
+        if ((byte) read != UTF8_BOM[1]) {
+            return read;
+        }
+        read = inputStream.read();
+        if ((byte) read != UTF8_BOM[2]) {
+            return read;
         }
+        charsetBomMarked = true;
+        charset = StandardCharsets.UTF_8;
+        // return anything other that -1
+        return -2;
     }
 
     // The first byte of a UTF-8 two byte sequence is between 0xC0 - 0xDF.
diff --git a/rewrite-core/src/test/java/org/openrewrite/ParserTest.java b/rewrite-core/src/test/java/org/openrewrite/ParserTest.java
index 0f1b00fdb4e..ae3246361c5 100644
--- a/rewrite-core/src/test/java/org/openrewrite/ParserTest.java
+++ b/rewrite-core/src/test/java/org/openrewrite/ParserTest.java
@@ -21,7 +21,6 @@
 import org.openrewrite.tree.ParseError;
 import org.openrewrite.tree.ParsingExecutionContextView;
 
-import java.io.ByteArrayInputStream;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
@@ -80,8 +79,8 @@ void printIdempotentDiffPrint() {
            line 2
           """;
 
-        Parser.Input input1 = new Parser.Input(path, () -> new ByteArrayInputStream(before.getBytes()));
-        Parser.Input input2 = new Parser.Input(path, () -> new ByteArrayInputStream(after.getBytes()));
+        Parser.Input input1 = Parser.Input.fromString(path, before);
+        Parser.Input input2 = Parser.Input.fromString(path, after);
         SourceFile s1 = parser.parse(input1.getSource(ctx).readFully()).toList().get(0);
         SourceFile out = parser.requirePrintEqualsInput(s1, input2, null, ctx);
         assertThat(out).isInstanceOf(ParseError.class);
diff --git a/rewrite-core/src/test/java/org/openrewrite/internal/EncodingDetectingInputStreamTest.java b/rewrite-core/src/test/java/org/openrewrite/internal/EncodingDetectingInputStreamTest.java
index d17a64254c3..68205549167 100644
--- a/rewrite-core/src/test/java/org/openrewrite/internal/EncodingDetectingInputStreamTest.java
+++ b/rewrite-core/src/test/java/org/openrewrite/internal/EncodingDetectingInputStreamTest.java
@@ -33,8 +33,45 @@ class EncodingDetectingInputStreamTest {
 
     @Test
     void detectUTF8Bom() throws IOException {
-        String bom = "\uFEFF";
-        try (EncodingDetectingInputStream is = read(bom, UTF_8)) {
+        String str = "\uFEFF";
+        try (EncodingDetectingInputStream is = new EncodingDetectingInputStream(new ByteArrayInputStream(str.getBytes(UTF_8)))) {
+            assertThat(is.readFully()).isEqualTo("");
+            assertThat(is.isCharsetBomMarked()).isTrue();
+        }
+    }
+
+    @Test
+    void emptyUtf8() throws IOException {
+        String str = "";
+        try (EncodingDetectingInputStream is = new EncodingDetectingInputStream(new ByteArrayInputStream(str.getBytes(UTF_8)))) {
+            assertThat(is.readFully()).isEqualTo(str);
+            assertThat(is.isCharsetBomMarked()).isFalse();
+        }
+    }
+
+    @Test
+    void singleCharUtf8() throws IOException {
+        String str = "1";
+        try (EncodingDetectingInputStream is = new EncodingDetectingInputStream(new ByteArrayInputStream(str.getBytes(UTF_8)))) {
+            assertThat(is.readFully()).isEqualTo(str);
+            assertThat(is.isCharsetBomMarked()).isFalse();
+        }
+    }
+
+    @Test
+    void skipUTF8Bom() throws IOException {
+        String bom = "\uFEFFhello";
+        try (EncodingDetectingInputStream is = new EncodingDetectingInputStream(new ByteArrayInputStream(bom.getBytes(UTF_8)))) {
+            assertThat(is.readFully()).isEqualTo("hello");
+            assertThat(is.isCharsetBomMarked()).isTrue();
+        }
+    }
+
+    @Test
+    void skipUTF8BomKnownEncoding() throws IOException {
+        String bom = "\uFEFFhello";
+        try (EncodingDetectingInputStream is = new EncodingDetectingInputStream(new ByteArrayInputStream(bom.getBytes(UTF_8)), UTF_8)) {
+            assertThat(is.readFully()).isEqualTo("hello");
             assertThat(is.isCharsetBomMarked()).isTrue();
         }
     }
diff --git a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/CompilationUnitTest.java b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/CompilationUnitTest.java
index 23534b40563..bbdf8337036 100644
--- a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/CompilationUnitTest.java
+++ b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/CompilationUnitTest.java
@@ -58,9 +58,9 @@ void shellScript() {
           groovy(
             """
               #!/usr/bin/env groovy
-                           
+              
               def a = 'hello'
-               """
+              """
           )
         );
     }
diff --git a/rewrite-java/src/test/java/org/openrewrite/java/JavadocPrinterTest.java b/rewrite-java/src/test/java/org/openrewrite/java/JavadocPrinterTest.java
index fd85de32ba4..3bc4b046c9f 100644
--- a/rewrite-java/src/test/java/org/openrewrite/java/JavadocPrinterTest.java
+++ b/rewrite-java/src/test/java/org/openrewrite/java/JavadocPrinterTest.java
@@ -58,4 +58,4 @@ class Test {
           )
         );
     }
-}
\ No newline at end of file
+}
diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java
index e2a30d2263f..fde6936e423 100644
--- a/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java
+++ b/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java
@@ -32,7 +32,6 @@
 import org.openrewrite.test.RewriteTest;
 import org.openrewrite.tree.ParseError;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.nio.file.Paths;
 import java.util.Base64;
@@ -938,8 +937,7 @@ public MockResponse dispatch(RecordedRequest request) {
             var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext(t -> {
                 throw new RuntimeException(t);
             }));
-            var settings = MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () ->
-              new ByteArrayInputStream(
+            var settings = MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
                 //language=xml
                 """
                       <settings>
@@ -959,8 +957,8 @@ public MockResponse dispatch(RecordedRequest request) {
                               </server>
                           </servers>
                       </settings>
-                  """.formatted(mockRepo.getHostName(), mockRepo.getPort(), username, password).getBytes()
-              )), ctx);
+                  """.formatted(mockRepo.getHostName(), mockRepo.getPort(), username, password)
+              ), ctx);
             ctx.setMavenSettings(settings);
 
             var maven = MavenParser.builder().build().parse(
diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/MavenSettingsTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/MavenSettingsTest.java
index 8791dcaba93..8acbd05f729 100644
--- a/rewrite-maven/src/test/java/org/openrewrite/maven/MavenSettingsTest.java
+++ b/rewrite-maven/src/test/java/org/openrewrite/maven/MavenSettingsTest.java
@@ -34,7 +34,6 @@
 import org.openrewrite.xml.XmlParser;
 import org.openrewrite.xml.tree.Xml;
 
-import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.lang.reflect.Field;
@@ -54,7 +53,7 @@ class MavenSettingsTest {
 
     @Test
     void parse() {
-        ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
                 <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -78,8 +77,8 @@ void parse() {
                         </profile>
                     </profiles>
                 </settings>
-            """.getBytes()
-        )), ctx));
+            """
+        ), ctx));
 
         assertThat(ctx.getRepositories()).hasSize(1);
     }
@@ -87,7 +86,7 @@ void parse() {
     @Issue("https://github.com/openrewrite/rewrite/issues/131")
     @Test
     void defaultActiveWhenNoOthersAreActive() {
-        ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
                 <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -119,15 +118,15 @@ void defaultActiveWhenNoOthersAreActive() {
                         </profile>
                     </profiles>
                 </settings>
-            """.getBytes()
-        )), ctx));
+            """
+        ), ctx));
 
         assertThat(ctx.getRepositories().stream().map(MavenRepository::getUri)).containsExactly("https://activebydefault.com");
     }
 
     @Test
     void idCollisionLastRepositoryWins() {
-        ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
                 <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -158,8 +157,8 @@ void idCollisionLastRepositoryWins() {
                         </profile>
                     </profiles>
                 </settings>
-            """.getBytes()
-        )), ctx));
+            """
+        ), ctx));
 
         assertThat(ctx.getRepositories())
           .as("When multiple repositories have the same id in a maven settings file the last one wins. In a pom.xml an error would be thrown.")
@@ -169,7 +168,7 @@ void idCollisionLastRepositoryWins() {
     @Issue("https://github.com/openrewrite/rewrite/issues/131")
     @Test
     void defaultOnlyActiveIfNoOthersAreActive() {
-        ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
                 <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -206,8 +205,8 @@ void defaultOnlyActiveIfNoOthersAreActive() {
                         </profile>
                     </profiles>
                 </settings>
-            """.getBytes()
-        )), ctx));
+            """
+        ), ctx));
 
         assertThat(ctx.getActiveProfiles())
           .containsExactly("repo");
@@ -219,7 +218,7 @@ void defaultOnlyActiveIfNoOthersAreActive() {
     @Issue("https://github.com/openrewrite/rewrite/issues/130")
     @Test
     void mirrorReplacesRepository() {
-        ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
                 <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -250,8 +249,8 @@ void mirrorReplacesRepository() {
                         </mirror>
                     </mirrors>
                 </settings>
-            """.getBytes()
-        )), ctx));
+            """
+        ), ctx));
 
         assertThat(ctx.getRepositories().stream()
           .map(repo -> MavenRepositoryMirror.apply(ctx.getMirrors(), repo))
@@ -261,7 +260,7 @@ void mirrorReplacesRepository() {
 
     @Test
     void starredMirrorWithExclusion() {
-        ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
                 <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -296,8 +295,8 @@ void starredMirrorWithExclusion() {
                         </mirror>
                     </mirrors>
                 </settings>
-            """.getBytes()
-        )), ctx));
+            """
+        ), ctx));
 
         assertThat(ctx.getRepositories().stream()
           .map(repo -> MavenRepositoryMirror.apply(ctx.getMirrors(), repo)))
@@ -316,7 +315,7 @@ void starredMirrorWithExclusion() {
 
     @Test
     void serverCredentials() {
-        var settings = MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        var settings = MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
                 <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -330,8 +329,8 @@ void serverCredentials() {
                         </server>
                       </servers>
                 </settings>
-            """.getBytes()
-        )), ctx);
+            """
+        ), ctx);
 
         assertThat(settings.getServers()).isNotNull();
         assertThat(settings.getServers().getServers()).hasSize(1);
@@ -345,7 +344,7 @@ void serverCredentials() {
     void serverTimeouts() {
         // Deliberately supporting the simpler old configuration of a single timeout
         // https://maven.apache.org/guides/mini/guide-http-settings.html#connection-timeouts
-        var settings = MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        var settings = MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
                 <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -360,8 +359,8 @@ void serverTimeouts() {
                         </server>
                       </servers>
                 </settings>
-            """.getBytes()
-        )), ctx);
+            """
+        ), ctx);
 
         assertThat(settings.getServers()).isNotNull();
         assertThat(settings.getServers().getServers()).hasSize(1);
@@ -375,7 +374,7 @@ void serverTimeouts() {
     class LocalRepositoryTest {
         @Test
         void parsesLocalRepositoryPathFromSettingsXml(@TempDir Path localRepoPath) {
-            ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+            ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
               //language=xml
               """
                     <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -383,8 +382,8 @@ void parsesLocalRepositoryPathFromSettingsXml(@TempDir Path localRepoPath) {
                         xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
                           <localRepository>%s</localRepository>
                     </settings>
-                """.formatted(localRepoPath).getBytes()
-            )), ctx));
+                """.formatted(localRepoPath)
+            ), ctx));
             assertThat(ctx.getLocalRepository().getUri())
               .startsWith("file://")
               .containsSubsequence(localRepoPath.toUri().toString().split("/"));
@@ -392,7 +391,7 @@ void parsesLocalRepositoryPathFromSettingsXml(@TempDir Path localRepoPath) {
 
         @Test
         void parsesLocalRepositoryUriFromSettingsXml(@TempDir Path localRepoPath) {
-            ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+            ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
               //language=xml
               """
                     <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -400,8 +399,8 @@ void parsesLocalRepositoryUriFromSettingsXml(@TempDir Path localRepoPath) {
                         xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
                           <localRepository>%s</localRepository>
                     </settings>
-                """.formatted(localRepoPath).getBytes()
-            )), ctx));
+                """.formatted(localRepoPath)
+            ), ctx));
 
             assertThat(ctx.getLocalRepository().getUri())
               .startsWith("file://")
@@ -410,15 +409,15 @@ void parsesLocalRepositoryUriFromSettingsXml(@TempDir Path localRepoPath) {
 
         @Test
         void defaultsToTheMavenDefault() {
-            ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+            ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
               //language=xml
               """
                         <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
                             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                             xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
                         </settings>
-                """.getBytes()
-            )), ctx));
+                """
+            ), ctx));
 
             assertThat(ctx.getLocalRepository().getUri()).isEqualTo(MavenRepository.MAVEN_LOCAL_DEFAULT.getUri());
         }
@@ -430,7 +429,7 @@ class InterpolationTest {
         @Test
         void properties() {
             System.setProperty("rewrite.test.custom.location", "/tmp");
-            var settings = MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+            var settings = MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
               //language=xml
               """
                     <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -462,15 +461,15 @@ void properties() {
                             </profile>
                         </profiles>
                     </settings>
-                """.getBytes()
-            )), ctx);
+                """
+            ), ctx);
 
             assertThat(settings.getLocalRepository()).isEqualTo("/tmp/maven/local/repository/");
         }
 
         @Test
         void unresolvedPlaceholdersRemainUnchanged() {
-            var settings = MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+            var settings = MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
               //language=xml
               """
                     <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -502,8 +501,8 @@ void unresolvedPlaceholdersRemainUnchanged() {
                             </profile>
                         </profiles>
                     </settings>
-                """.getBytes()
-            )), ctx);
+                """
+            ), ctx);
 
             assertThat(settings.getLocalRepository())
               .isEqualTo("${custom.location.zz}/maven/local/repository/");
@@ -516,7 +515,7 @@ void unresolvedPlaceholdersRemainUnchanged() {
         void env() {
             updateEnvMap("REWRITE_TEST_PRIVATE_REPO_USERNAME", "user");
             updateEnvMap("REWRITE_TEST_PRIVATE_REPO_PASSWORD", "pass");
-            var settings = MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+            var settings = MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
               //language=xml
               """
                     <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -548,8 +547,8 @@ void env() {
                             </profile>
                         </profiles>
                     </settings>
-                """.getBytes()
-            )), ctx);
+                """
+            ), ctx);
 
             assertThat(settings.getServers()).isNotNull();
             assertThat(settings.getServers().getServers()).hasSize(1);
@@ -621,9 +620,8 @@ class MergingTest {
         @Test
         void concatenatesElementsWithUniqueIds() {
             Path path = Paths.get("settings.xml");
-            var baseSettings = MavenSettings.parse(new Parser.Input(path, () -> new ByteArrayInputStream(
-              installationSettings.getBytes())), ctx);
-            var userSettings = MavenSettings.parse(new Parser.Input(path, () -> new ByteArrayInputStream(
+            var baseSettings = MavenSettings.parse(Parser.Input.fromString(path, installationSettings), ctx);
+            var userSettings = MavenSettings.parse(Parser.Input.fromString(path,
               //language=xml
               """
                     <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -667,8 +665,8 @@ void concatenatesElementsWithUniqueIds() {
                             </mirror>
                         </mirrors>
                     </settings>
-                """.getBytes()
-            )), ctx);
+                """
+            ), ctx);
 
             var mergedSettings = userSettings.merge(baseSettings);
 
@@ -681,9 +679,8 @@ void concatenatesElementsWithUniqueIds() {
         @Test
         void replacesElementsWithMatchingIds() {
             Path path = Paths.get("settings.xml");
-            var baseSettings = MavenSettings.parse(new Parser.Input(path, () -> new ByteArrayInputStream(
-              installationSettings.getBytes())), ctx);
-            var userSettings = MavenSettings.parse(new Parser.Input(path, () -> new ByteArrayInputStream(
+            var baseSettings = MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"), installationSettings), ctx);
+            var userSettings = MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
               //language=xml
               """
                     <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -716,8 +713,8 @@ void replacesElementsWithMatchingIds() {
                             </mirror>
                         </mirrors>
                     </settings>
-                """.getBytes()
-            )), ctx);
+                """
+            ), ctx);
 
             var mergedSettings = userSettings.merge(baseSettings);
 
@@ -742,7 +739,7 @@ void replacesElementsWithMatchingIds() {
      */
     @Test
     void serverHttpHeaders() {
-        var settings = MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        var settings = MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
             <settings>
@@ -772,8 +769,8 @@ void serverHttpHeaders() {
                     </profile>
                 </profiles>
             </settings>
-            """.getBytes()
-        )), ctx);
+            """
+        ), ctx);
 
         MavenSettings.Server server = settings.getServers().getServers().get(0);
         assertThat(server.getConfiguration().getHttpHeaders().get(0).getName()).isEqualTo("X-JFrog-Art-Api");
diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/internal/MavenPomDownloaderTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/internal/MavenPomDownloaderTest.java
index fbd2f3c5a99..89bf27be4e8 100755
--- a/rewrite-maven/src/test/java/org/openrewrite/maven/internal/MavenPomDownloaderTest.java
+++ b/rewrite-maven/src/test/java/org/openrewrite/maven/internal/MavenPomDownloaderTest.java
@@ -35,7 +35,6 @@
 import org.openrewrite.maven.MavenSettings;
 import org.openrewrite.maven.tree.*;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.UnknownHostException;
 import java.nio.file.Files;
@@ -46,10 +45,7 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 
-import static java.util.Collections.emptyList;
-import static java.util.Collections.emptyMap;
-import static java.util.Collections.singletonList;
-import static java.util.Collections.singletonMap;
+import static java.util.Collections.*;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@@ -94,7 +90,7 @@ void ossSonatype() {
     @Test
     void centralIdOverridesDefaultRepository() {
         var ctx = MavenExecutionContextView.view(this.ctx);
-        ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
             <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -116,8 +112,8 @@ void centralIdOverridesDefaultRepository() {
                 <activeProfile>central</activeProfile>
               </activeProfiles>
             </settings>
-            """.getBytes()
-        )), ctx));
+            """
+        ), ctx));
 
         // Avoid actually trying to reach the made-up https://internalartifactrepository.yourorg.com
         for (MavenRepository repository : ctx.getRepositories()) {
@@ -196,7 +192,7 @@ public void repositoryAccessFailed(String uri, Throwable e) {
     @Test
     void mirrorsOverrideRepositoriesInPom() {
         var ctx = MavenExecutionContextView.view(this.ctx);
-        ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
             <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -211,8 +207,8 @@ void mirrorsOverrideRepositoriesInPom() {
                 </mirror>
               </mirrors>
             </settings>
-            """.getBytes()
-        )), ctx));
+            """
+        ), ctx));
 
         Path pomPath = Paths.get("pom.xml");
         Pom pom = Pom.builder()
diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/search/FindRepositoryOrderTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/search/FindRepositoryOrderTest.java
index 054038a2614..fe84bd472e8 100644
--- a/rewrite-maven/src/test/java/org/openrewrite/maven/search/FindRepositoryOrderTest.java
+++ b/rewrite-maven/src/test/java/org/openrewrite/maven/search/FindRepositoryOrderTest.java
@@ -24,7 +24,6 @@
 import org.openrewrite.maven.table.MavenRepositoryOrder;
 import org.openrewrite.test.RewriteTest;
 
-import java.io.ByteArrayInputStream;
 import java.nio.file.Paths;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -36,7 +35,7 @@ class FindRepositoryOrderTest implements RewriteTest {
     @Test
     void findRepositoryOrder() {
         var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext());
-        ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream(
+        ctx.setMavenSettings(MavenSettings.parse(Parser.Input.fromString(Paths.get("settings.xml"),
           //language=xml
           """
             <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -60,8 +59,8 @@ void findRepositoryOrder() {
                     </profile>
                 </profiles>
             </settings>
-            """.getBytes()
-        )), ctx));
+            """
+        ), ctx));
 
         rewriteRun(
           spec -> spec
diff --git a/rewrite-test/src/main/java/org/openrewrite/test/RewriteTest.java b/rewrite-test/src/main/java/org/openrewrite/test/RewriteTest.java
index a1259aecdac..0351f635d24 100644
--- a/rewrite-test/src/main/java/org/openrewrite/test/RewriteTest.java
+++ b/rewrite-test/src/main/java/org/openrewrite/test/RewriteTest.java
@@ -32,7 +32,9 @@
 import org.openrewrite.quark.Quark;
 import org.openrewrite.remote.Remote;
 import org.openrewrite.tree.ParseError;
+import org.openrewrite.tree.ParsingExecutionContextView;
 
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.*;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -628,7 +630,9 @@ default void rewriteRun(SourceSpec<?>... sources) {
     }
 
     default ExecutionContext defaultExecutionContext(SourceSpec<?>[] sourceSpecs) {
-        return new InMemoryExecutionContext(t -> fail("Failed to parse sources or run recipe", t));
+        InMemoryExecutionContext ctx = new InMemoryExecutionContext(t -> fail("Failed to parse sources or run recipe", t));
+        ParsingExecutionContextView.view(ctx).setCharset(StandardCharsets.UTF_8);
+        return ctx;
     }
 
     @Override