diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs
index 1157bc586313..0dcd623c93d4 100644
--- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs
+++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs
@@ -258,7 +258,8 @@ internal readonly record struct SourceFile(string Path, SourceText Text)
public static SourceFile Load(string filePath)
{
using var stream = File.OpenRead(filePath);
- return new SourceFile(filePath, SourceText.From(stream, Encoding.UTF8));
+ // Let SourceText.From auto-detect the encoding (including BOM detection)
+ return new SourceFile(filePath, SourceText.From(stream, encoding: null));
}
public SourceFile WithText(SourceText newText)
@@ -269,7 +270,9 @@ public SourceFile WithText(SourceText newText)
public void Save()
{
using var stream = File.Open(Path, FileMode.Create, FileAccess.Write);
- using var writer = new StreamWriter(stream, Encoding.UTF8);
+ // Use the encoding from SourceText, which preserves the original BOM state
+ var encoding = Text.Encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+ using var writer = new StreamWriter(stream, encoding);
Text.Write(writer);
}
diff --git a/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs
index f6b181615b72..b34ce92dfd96 100644
--- a/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs
+++ b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs
@@ -517,6 +517,94 @@ public void RemoveMultiple()
"""));
}
+ ///
+ /// Verifies that files without UTF-8 BOM don't get one added when saved.
+ /// This is critical for shebang (#!) scripts on Unix-like systems.
+ ///
+ ///
+ [Fact]
+ public void PreservesNoBomEncoding()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var tempFile = Path.Join(testInstance.Path, "test.cs");
+
+ // Create a file without BOM
+ var content = "#!/usr/bin/env dotnet run\nConsole.WriteLine();";
+ File.WriteAllText(tempFile, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
+
+ // Load, modify, and save
+ var sourceFile = SourceFile.Load(tempFile);
+ var editor = FileBasedAppSourceEditor.Load(sourceFile);
+ editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" });
+ editor.SourceFile.Save();
+
+ // Verify no BOM was added
+ var bytes = File.ReadAllBytes(tempFile);
+ Assert.True(bytes is not [0xEF, 0xBB, 0xBF, ..],
+ "File should not have UTF-8 BOM");
+
+ // Verify shebang is still first
+ var savedContent = File.ReadAllText(tempFile);
+ Assert.StartsWith("#!/usr/bin/env dotnet run", savedContent);
+ }
+
+ ///
+ /// Verifies that files with UTF-8 BOM preserve it when saved.
+ ///
+ ///
+ [Fact]
+ public void PreservesBomEncoding()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var tempFile = Path.Join(testInstance.Path, "test.cs");
+
+ // Create a file with BOM
+ var content = "Console.WriteLine();";
+ File.WriteAllText(tempFile, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
+
+ // Load, modify, and save
+ var sourceFile = SourceFile.Load(tempFile);
+ var editor = FileBasedAppSourceEditor.Load(sourceFile);
+ editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" });
+ editor.SourceFile.Save();
+
+ // Verify BOM is still present
+ var bytes = File.ReadAllBytes(tempFile);
+ Assert.True(bytes is [0xEF, 0xBB, 0xBF, ..],
+ "File should have UTF-8 BOM");
+ }
+
+ ///
+ /// Verifies that files with non-UTF-8 encodings (like UTF-16) preserve their encoding when saved.
+ ///
+ ///
+ [Fact]
+ public void PreservesNonUtf8Encoding()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var tempFile = Path.Join(testInstance.Path, "test.cs");
+
+ // Create a file with UTF-16 encoding (includes BOM by default)
+ var content = "Console.WriteLine(\"UTF-16 test\");";
+ File.WriteAllText(tempFile, content, Encoding.Unicode);
+
+ // Load, modify, and save
+ var sourceFile = SourceFile.Load(tempFile);
+ var editor = FileBasedAppSourceEditor.Load(sourceFile);
+ editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" });
+ editor.SourceFile.Save();
+
+ // Verify UTF-16 BOM is still present (0xFF 0xFE for UTF-16 LE)
+ var bytes = File.ReadAllBytes(tempFile);
+ Assert.True(bytes is [0xFF, 0xFE, ..],
+ "File should have UTF-16 LE BOM");
+
+ // Verify content is still readable as UTF-16
+ var savedContent = File.ReadAllText(tempFile, Encoding.Unicode);
+ Assert.Contains("#:package MyPackage@1.0.0", savedContent);
+ Assert.Contains("Console.WriteLine", savedContent);
+ }
+
private void Verify(
string input,
params ReadOnlySpan<(Action action, string expectedOutput)> verify)