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)