// // TextFile.cs // // Author: // Lluis Sanchez Gual // // Copyright (C) 2005 Novell, Inc (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Runtime.InteropServices; using Mono.Unix; using Mono.Unix.Native; using MonoDevelop.Core; using MonoDevelop.Projects.Utility; namespace MonoDevelop.Projects.Text { public class TextFile: ITextFile { const string LIBGLIB = "libglib-2.0-0.dll"; FilePath name; StringBuilder text; string sourceEncoding; bool modified; public TextFile () { } public TextFile (FilePath name) { Read (name); } public void Read (FilePath fileName) { Read (fileName, null); } public static TextFile ReadFile (FilePath fileName) { TextFile tf = new TextFile (); tf.Read (fileName); return tf; } public static TextFile ReadFile (FilePath fileName, string encoding) { TextFile tf = new TextFile (); tf.Read (fileName, encoding); return tf; } public static TextFile ReadFile (string path, Stream content) { TextFile tf = new TextFile (); tf.name = path; tf.Read (content, null); return tf; } public void Read (Stream stream, string encoding) { ByteOrderMark bom = null; byte[] content = null; long nread; retry: stream.Seek (0, SeekOrigin.Begin); if (encoding == null) { if (ByteOrderMark.TryParse (stream, out bom)) stream.Seek (bom.Length, SeekOrigin.Begin); else stream.Seek (0, SeekOrigin.Begin); } content = new byte [bom != null ? stream.Length - bom.Length : stream.Length]; nread = 0; int n; while ((n = stream.Read (content, (int) nread, (content.Length - (int) nread))) > 0) nread += n; if (encoding != null) { string s = ConvertFromEncoding (content, nread, encoding); if (s == null) { // The encoding provided was wrong, fall back to trying to use the BOM if it exists... encoding = null; content = null; goto retry; } text = new StringBuilder (s); sourceEncoding = encoding; return; } if (bom != null) { string s = ConvertFromEncoding (content, nread, bom.Name); if (s != null) { HadBOM = true; sourceEncoding = bom.Name; text = new StringBuilder (s); return; } } // Fall back to trying all the encodings... foreach (TextEncoding co in TextEncoding.ConversionEncodings) { string s = ConvertFromEncoding (content, nread, co.Id); if (s != null) { sourceEncoding = co.Id; text = new StringBuilder (s); return; } } throw new Exception ("Unknown text file encoding"); } public void Read (FilePath fileName, string encoding) { // Reads the file using the specified encoding. // If the encoding is null, it autodetects the // required encoding. this.name = fileName; using (FileStream stream = new FileStream (fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { Read (stream, encoding); } } public static string GetFileEncoding (FilePath fileName) { // Maybe this can be optimized later. TextFile file = TextFile.ReadFile (fileName); return file.SourceEncoding; } #region g_convert static string ConvertFromEncoding (byte[] content, long nread, string fromEncoding) { try { return Encoding.UTF8.GetString (ConvertToBytes (content, nread, "UTF-8", fromEncoding)); } catch (Exception e) { LoggingService.LogWarning ("Fail to use encoding " + fromEncoding, e); return null; } } struct GError { #pragma warning disable 649 public int Domain; public int Code; public IntPtr Msg; #pragma warning restore 649 } static unsafe int strlen (IntPtr str) { byte *s = (byte *) str; int n = 0; while (*s != 0) { s++; n++; } return n; } public static string Utf8PtrToString (IntPtr ptr) { if (ptr == IntPtr.Zero) return null; int len = strlen (ptr); byte[] bytes = new byte [len]; Marshal.Copy (ptr, bytes, 0, len); return System.Text.Encoding.UTF8.GetString (bytes); } static byte[] ConvertToBytes (byte[] content, long nread, string toEncoding, string fromEncoding) { if (nread > int.MaxValue) throw new Exception ("Content too large."); if (toEncoding == fromEncoding) { if (content.LongLength == nread) return content; byte[] result = new byte[nread]; Array.Copy (content, result, nread); return result; } IntPtr nr = IntPtr.Zero, nw = IntPtr.Zero; IntPtr clPtr = new IntPtr (nread); IntPtr errptr = IntPtr.Zero; IntPtr cc = g_convert (content, clPtr, toEncoding, fromEncoding, ref nr, ref nw, ref errptr); if (cc != IntPtr.Zero) { //FIXME: check for out-of-range conversions on uints int len = (int)(uint)nw.ToInt64 (); byte[] buf = new byte [len]; System.Runtime.InteropServices.Marshal.Copy (cc, buf, 0, buf.Length); g_free (cc); return buf; } else { GError err = (GError)Marshal.PtrToStructure (errptr, typeof(GError)); string reason = Utf8PtrToString (err.Msg); string message = string.Format ("Failed to convert content from {0} to {1}: {2}.", fromEncoding, toEncoding, reason); InvalidEncodingException ex = new InvalidEncodingException (message); g_error_free (errptr); throw ex; } } [DllImport(LIBGLIB, CallingConvention = CallingConvention.Cdecl)] //note: textLength is signed, read/written are not static extern IntPtr g_convert(byte[] text, IntPtr textLength, string toCodeset, string fromCodeset, ref IntPtr read, ref IntPtr written, ref IntPtr err); [DllImport(LIBGLIB, CallingConvention = CallingConvention.Cdecl)] static extern void g_free (IntPtr ptr); [DllImport(LIBGLIB, CallingConvention = CallingConvention.Cdecl)] static extern void g_error_free (IntPtr err); #endregion public FilePath Name { get { return name; } } public bool HadBOM { get; private set; } public bool Modified { get { return modified; } } public string SourceEncoding { get { return sourceEncoding; } set { sourceEncoding = value; } } public string Text { get { return text.ToString (); } set { text = new StringBuilder (value); modified = true; } } public int Length { get { return text.Length; } } public string GetText (int startPosition, int endPosition) { return text.ToString (startPosition, endPosition - startPosition); } public char GetCharAt (int position) { if (position < text.Length) return text [position]; else return (char)0; } public int GetPositionFromLineColumn (int line, int column) { int lin = 1; int col = 1; for (int i = 0; i < text.Length && lin <= line; i++) { if (line == lin && column == col) return i; if (text[i] == '\r') { if (i + 1 < text.Length && text[i + 1] == '\n') i++; lin++; col = 1; } else if (text[i] == '\n') { lin++; col = 1; } else col++; } return -1; } public void GetLineColumnFromPosition (int position, out int line, out int column) { int lin = 1; int col = 1; for (int i = 0; i < position; i++) { if (text[i] == '\r') { if (i + 1 < position && text[i + 1] == '\n') i++; lin++; col = 1; } else if (text[i] == '\n') { lin++; col = 1; } else col++; } line = lin; column = col; } public int GetLineLength (int line) { int pos = GetPositionFromLineColumn (line, 1); if (pos == -1) return 0; int len = 0; while (pos < text.Length && text [pos] != '\n' && text [pos] != '\r') { pos++; len++; } return len; } public int InsertText (int position, string textIn) { text.Insert (position, textIn); modified = true; return textIn != null ? textIn.Length : 0; } public void DeleteText (int position, int length) { text.Remove (position, length); modified = true; } public void Save () { WriteFile (name, text.ToString (), sourceEncoding, HadBOM); modified = false; } public static void WriteFile (FilePath fileName, byte[] content, string encoding, ByteOrderMark bom, bool onlyIfChanged) { int contentLength = content.Length + (bom != null ? bom.Length : 0); byte[] converted; if (encoding != null) converted = ConvertToBytes (content, content.LongLength, encoding, "UTF-8"); else converted = content; if (onlyIfChanged) { FileInfo finfo = new FileInfo (fileName); if (finfo.Exists && finfo.Length == contentLength) { bool changed = false; // Open the file on disk and compare them byte by byte... using (FileStream stream = finfo.Open (FileMode.Open, FileAccess.Read, FileShare.Read)) { byte[] buf = new byte [4096]; int bomOffset = 0; int offset = 0; int nread; int i; while (!changed && (nread = stream.Read (buf, 0, buf.Length)) > 0) { i = 0; if (bom != null && bomOffset < bom.Length) { while (i < nread && bomOffset < bom.Length) { if (bom.Bytes[bomOffset] != buf[i]) { changed = true; break; } bomOffset++; i++; } if (changed) break; } while (i < nread && offset < converted.Length) { if (converted[offset] != buf[i]) { changed = true; break; } offset++; i++; } if (offset == converted.Length && i < nread) changed = true; } if (offset < converted.Length) changed = true; } if (!changed) return; } // Content has changed... } string tempName = Path.GetDirectoryName (fileName) + Path.DirectorySeparatorChar + ".#" + Path.GetFileName (fileName); FileStream fs = new FileStream (tempName, FileMode.Create, FileAccess.Write); if (bom != null) fs.Write (bom.Bytes, 0, bom.Length); fs.Write (converted, 0, converted.Length); fs.Flush (); fs.Close (); FileService.SystemRename (tempName, fileName); FileService.NotifyFileChanged (fileName); } public static void WriteFile (FilePath fileName, string content, string encoding, ByteOrderMark bom, bool onlyIfChanged) { byte[] buf = Encoding.UTF8.GetBytes (content); WriteFile (fileName, buf, encoding, bom, onlyIfChanged); } public static void WriteFile (FilePath fileName, string content, ByteOrderMark bom, bool onlyIfChanged) { WriteFile (fileName, content, bom != null ? bom.Name : null, bom, onlyIfChanged); } public static void WriteFile (FilePath fileName, string content, string encoding) { WriteFile (fileName, content, encoding, false); } public static void WriteFile (FilePath fileName, string content, string encoding, bool saveBOM) { ByteOrderMark bom = saveBOM && encoding != null ? ByteOrderMark.GetByName (encoding) : null; WriteFile (fileName, content, encoding, bom, false); } } [Serializable] public class InvalidEncodingException : Exception { /// /// Initializes a new instance of the class /// public InvalidEncodingException () { } /// /// Initializes a new instance of the class /// /// A that describes the exception. public InvalidEncodingException (string message) : base (message) { } /// /// Initializes a new instance of the class /// /// A that describes the exception. /// The exception that is the cause of the current exception. public InvalidEncodingException (string message, Exception inner) : base (message, inner) { } } public static class ExtensionMethods { public static bool StartsWith (this IEnumerable t, IEnumerable s) { using (IEnumerator te = t.GetEnumerator()) { using (IEnumerator se = s.GetEnumerator()) { bool didMoveTe = false; while ((didMoveTe = te.MoveNext ()) && se.MoveNext ()) { if (!te.Current.Equals (se.Current)) return false; } return didMoveTe; } } } } }