using System.Text; namespace AIStudio.Tools.Rust; /// /// Tells from the beginning of a file whether it holds text which the runtime's text reader can read. /// /// /// FileTypes asks it for READABLE_TEXT, i.e., for text whose file name or extension is not known in /// advance, such as LICENSE, .gitignore, or main.tf. That applies across the app: chat attachments, /// data sources, and the selectors of file patterns. Only the first PROBE_SIZE bytes are read, so a /// file whose text ends there passes as well. The runtime's text reader has the final say when it /// extracts the text. /// internal static class ReadableTextDetection { /// /// How many bytes at the beginning of a file are read at most. /// /// /// The same size as BINARY_PROBE_SIZE in runtime/src/file_data.rs, where the text reader looks /// for NUL bytes. Change both together, so that this check and the reader agree on what is binary. /// public const int PROBE_SIZE = 8_192; /// /// Text holds at most one control character in this many characters, e.g., an escape sequence /// in a log. Binary data holds far more. /// private const int MIN_CHARACTERS_PER_CONTROL_CHARACTER = 100; private const byte DELETE_BYTE = 0x7F; private static readonly byte[] UTF8_BOM = [0xEF, 0xBB, 0xBF]; private static readonly byte[] UTF16_BE_BOM = [0xFE, 0xFF]; private static readonly byte[] UTF16_LE_BOM = [0xFF, 0xFE]; /// /// Determines from the first PROBE_SIZE bytes whether a file uses a recognizable text encoding /// and does not resemble binary data. /// /// The path of the file. /// True, when the file looks like readable text. False, when it is empty, binary, or cannot be read. public static bool IsReadableTextFile(string filePath) { try { using var stream = new FileStream( filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, PROBE_SIZE, FileOptions.SequentialScan); var buffer = new byte[PROBE_SIZE]; var length = 0; while (length < buffer.Length) { var read = stream.Read(buffer, length, buffer.Length - length); if (read == 0) break; length += read; } if (length == 0) return false; return IsReadableText(buffer.AsSpan(0, length)); } catch (IOException) { return false; } catch (UnauthorizedAccessException) { return false; } catch (DecoderFallbackException) { return false; } catch (ArgumentException) { return false; } catch (NotSupportedException) { return false; } } private static bool IsReadableText(ReadOnlySpan bytes) { if (bytes.StartsWith(UTF8_BOM)) return IsReadableText(bytes[UTF8_BOM.Length..], new UTF8Encoding(true, true)); if (bytes.StartsWith(UTF16_BE_BOM)) return IsReadableText(bytes[UTF16_BE_BOM.Length..], new UnicodeEncoding(true, true, true)); if (bytes.StartsWith(UTF16_LE_BOM)) return IsReadableText(bytes[UTF16_LE_BOM.Length..], new UnicodeEncoding(false, true, true)); if (bytes.Contains((byte)0)) return false; try { return IsReadableText(bytes, new UTF8Encoding(false, true)); } catch (DecoderFallbackException) { // Legacy single-byte encodings can represent every byte. Control bytes are therefore // the useful distinction between an encoded text file and arbitrary binary content. return HasAcceptableControlByteRatio(bytes); } } private static bool IsReadableText(ReadOnlySpan bytes, Encoding encoding) { // // The probe may end in the middle of a character. Without flushing, the decoder keeps // such a rest to itself instead of rejecting it: // var characters = new char[encoding.GetMaxCharCount(bytes.Length)]; var decoder = encoding.GetDecoder(); decoder.Convert(bytes, characters, false, out _, out var charactersUsed, out _); return charactersUsed > 0 && HasAcceptableControlCharacterRatio(characters.AsSpan(0, charactersUsed)); } private static bool HasAcceptableControlByteRatio(ReadOnlySpan bytes) { var controls = 0; foreach (var value in bytes) { if ((value < (byte)' ' && value != (byte)'\t' && value != (byte)'\n' && value != (byte)'\r' && value != (byte)'\f') || value == DELETE_BYTE) controls++; } return controls * MIN_CHARACTERS_PER_CONTROL_CHARACTER <= bytes.Length; } private static bool HasAcceptableControlCharacterRatio(ReadOnlySpan characters) { var controls = 0; foreach (var character in characters) { if (char.IsControl(character) && character != '\t' && character != '\n' && character != '\r' && character != '\f') controls++; } return controls * MIN_CHARACTERS_PER_CONTROL_CHARACTER <= characters.Length; } }