//
// mono-shlib-cop.cs: Check unmanaged dependencies
//
// Compile as:
//    mcs mono-shlib-cop.cs ../../class/Mono.Options/Mono.Options/Options.cs -r:Mono.Posix
//
// Authors:
//  Jonathan Pryor (jonpryor@vt.edu)
//  Jonathan Pryor (jpryor@novell.com)
//
// (C) 2005 Jonathan Pryor
// (C) 2008 Novell, Inc.
//

//
// 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.
//

//
// About:
//    mono-shlib-cop is designed to inspect an assembly and report about
//    potentially erroneous practices.  In particular, this includes:
//      - DllImporting a .so which may be a symlink (which typically requires the
//        -devel packages on Linux distros, thus bloating installation and
//        angering users)
//      - DllImporting a symbol which doesn't exist in the target library
//      - etc.
//
// Implementation:
//    - Each assembly needs to be loaded into an AppDomain so that we can
//      adjust the ApplicationBase path (which will allow us to more reliably
//      load assemblies which depend upon assemblies in the same directory).
//      We can share AppDomains (1/directory), but we (alas) can't use a
//      single AppDomain for the entire app.
//    - Thus, algorithm:
//      - Create AppDomain with ApplicationBase path set to directory assembly
//        resides in
//      - Create an AssemblyChecker instance within the AppDomain
//      - Check an assembly with AssemblyChecker; store results in AssemblyCheckInfo.
//      - Print results.
//
// TODO:
//    - AppDomain use
//    - Make -r work correctly (-r:Mono.Posix should read Mono.Posix from the
//      GAC and inspect it.)
//
#define TRACE

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Xml;

using Mono.Options;
using Mono.Unix;

[assembly: AssemblyTitle ("mono-shlib-cop")]
[assembly: AssemblyCopyright ("(C) 2005 Jonathan Pryor")]
[assembly: AssemblyDescription ("Looks up shared library dependencies of managed code")]

namespace Mono.Unmanaged.Check {
	[Serializable]
	sealed class MessageInfo {
		public string Type;
		public string Member;
		public string Message;

		public MessageInfo (string type, string member, string message)
		{
			Type = type;
			Member = member;
			Message = message;
		}

		public override bool Equals (object value)
		{
			MessageInfo other = value as MessageInfo;
			if (other == null)
				return false;

			return Type == other.Type && Member == other.Member && 
				Message == other.Message;
		}

		public override int GetHashCode ()
		{
			return Type.GetHashCode () ^ Member.GetHashCode () ^ 
				Message.GetHashCode ();
		}
	}

	sealed class MessageCollection : MarshalByRefObject {
		private ArrayList InnerList = new ArrayList ();

		public MessageCollection ()
		{
		}

		public int Add (MessageInfo value)
		{
			if (!InnerList.Contains (value))
				return InnerList.Add (value);
			return InnerList.IndexOf (value);
		}

		public void AddRange (MessageInfo[] value)
		{
			foreach (MessageInfo v in value)
				Add (v);
		}

		public void AddRange (MessageCollection value)
		{
			foreach (MessageInfo v in value)
				Add (v);
		}

		public bool Contains (MessageInfo value)
		{
			return InnerList.Contains (value);
		}

		public void CopyTo (MessageInfo[] array, int index)
		{
			InnerList.CopyTo (array, index);
		}

		public int IndexOf (MessageInfo value)
		{
			return InnerList.IndexOf (value);
		}

		public void Insert (int index, MessageInfo value)
		{
			InnerList.Insert (index, value);
		}

		public void Remove (MessageInfo value)
		{
			InnerList.Remove (value);
		}

		public IEnumerator GetEnumerator ()
		{
			return InnerList.GetEnumerator ();
		}
	}

	sealed class AssemblyCheckInfo : MarshalByRefObject {
		private MessageCollection errors   = new MessageCollection ();
		private MessageCollection warnings = new MessageCollection ();

		public MessageCollection Errors {
			get {return errors;}
		}

		public MessageCollection Warnings {
			get {return warnings;}
		}

		private XmlDocument[] mono_configs = new XmlDocument [0];
		private IDictionary assembly_configs = new Hashtable ();

		public void SetInstallationPrefixes (IList<string> prefixes)
		{
			mono_configs = new XmlDocument [prefixes.Count];
			for (int i = 0; i < mono_configs.Length; ++i) {
				mono_configs [i] = new XmlDocument ();
				mono_configs [i].Load (Path.Combine (prefixes [i], "etc/mono/config"));
			}
		}

		public string GetDllmapEntry (string assemblyPath, string library)
		{
			string xpath = "/configuration/dllmap[@dll=\"" + library + "\"]";

			XmlDocument d = GetAssemblyConfig (assemblyPath);
			if (d != null) {
				XmlNode map = d.SelectSingleNode (xpath);
				if (map != null)
					return map.Attributes ["target"].Value;
			}
			foreach (XmlDocument config in mono_configs) {
				XmlNode map = config.SelectSingleNode (xpath);
				if (map != null)
					return map.Attributes ["target"].Value;
			}
			return null;
		}

		private XmlDocument GetAssemblyConfig (string assemblyPath)
		{
			XmlDocument d = null;
			if (assembly_configs.Contains (assemblyPath)) {
				d = (XmlDocument) assembly_configs [assemblyPath];
			}
			else {
				string _config = assemblyPath + ".config";
				if (File.Exists (_config)) {
					d = new XmlDocument ();
					d.Load (_config);
				}
				assembly_configs.Add (assemblyPath, d);
			}
			return d;
		}
	}

	sealed class AssemblyChecker : MarshalByRefObject {

		public void CheckFile (string file, AssemblyCheckInfo report)
		{
			try {
				Check (Assembly.LoadFile (file), report);
			}
			catch (FileNotFoundException e) {
				report.Errors.Add (new MessageInfo (null, null, 
					"Could not load `" + file + "': " + e.Message));
			}
		}

		public void CheckWithPartialName (string partial, AssemblyCheckInfo report)
		{
			string p = partial;
			Assembly a;
			bool retry;

			do {
				a = Assembly.LoadWithPartialName (p);
				retry = p.EndsWith (".dll");
				if (retry) {
					p = p.Substring (0, p.Length-4);
				}
			} while (a == null && retry);

			if (a == null) {
				report.Errors.Add (new MessageInfo (null, null, 
					"Could not load assembly reference `" + partial + "'."));
				return;
			}

			Check (a, report);
		}

		private void Check (Assembly a, AssemblyCheckInfo report)
		{
			foreach (Type t in a.GetTypes ()) {
				Check (t, report);
			}
		}

		private void Check (Type type, AssemblyCheckInfo report)
		{
			BindingFlags bf = BindingFlags.Instance | BindingFlags.Static | 
				BindingFlags.Public | BindingFlags.NonPublic;

			foreach (MemberInfo mi in type.GetMembers (bf)) {
				CheckMember (type, mi, report);
			}
		}

		private void CheckMember (Type type, MemberInfo mi, AssemblyCheckInfo report)
		{
			DllImportAttribute[] attributes = null;
			MethodBase[] methods = null;
			switch (mi.MemberType) {
				case MemberTypes.Constructor: case MemberTypes.Method: {
					MethodBase mb = (MethodBase) mi;
					attributes = new DllImportAttribute[]{GetDllImportInfo (mb)};
					methods = new MethodBase[]{mb};
					break;
				}
				case MemberTypes.Event: {
					EventInfo ei = (EventInfo) mi;
					MethodBase add = ei.GetAddMethod (true);
					MethodBase remove = ei.GetRemoveMethod (true);
					attributes = new DllImportAttribute[]{
						GetDllImportInfo (add), GetDllImportInfo (remove)};
					methods = new MethodBase[]{add, remove};
					break;
				}
				case MemberTypes.Property: {
					PropertyInfo pi = (PropertyInfo) mi;
					MethodInfo[] accessors = pi.GetAccessors (true);
					if (accessors == null)
						break;
					attributes = new DllImportAttribute[accessors.Length];
					methods = new MethodBase [accessors.Length];
					for (int i = 0; i < accessors.Length; ++i) {
						attributes [i] = GetDllImportInfo (accessors [i]);
						methods [i] = accessors [i];
					}
					break;
				}
			}
			if (attributes == null || methods == null)
				return;

			for (int i = 0; i < attributes.Length; ++i) {
				if (attributes [i] == null)
					continue;
				CheckLibrary (methods [i], attributes [i], report);
			}
		}

		private static DllImportAttribute GetDllImportInfo (MethodBase method)
		{
			if (method == null)
				return null;

			if ((method.Attributes & MethodAttributes.PinvokeImpl) == 0)
				return null;

			// .NET 2.0 synthesizes pseudo-attributes such as DllImport
			DllImportAttribute dia = (DllImportAttribute) Attribute.GetCustomAttribute (method, 
						typeof(DllImportAttribute), false);
			if (dia != null)
				return dia;

			// We're not on .NET 2.0; assume we're on Mono and use some internal
			// methods...
			Type MonoMethod = Type.GetType ("System.Reflection.MonoMethod", false);
			if (MonoMethod == null) {
				return null;
			}
			MethodInfo GetDllImportAttribute = 
				MonoMethod.GetMethod ("GetDllImportAttribute", 
						BindingFlags.Static | BindingFlags.NonPublic);
			if (GetDllImportAttribute == null) {
				return null;
			}
			IntPtr mhandle = method.MethodHandle.Value;
			return (DllImportAttribute) GetDllImportAttribute.Invoke (null, 
					new object[]{mhandle});
		}
		
		private void CheckLibrary (MethodBase method, DllImportAttribute attribute, 
				AssemblyCheckInfo report)
		{
			string library = attribute.Value;
			string entrypoint = attribute.EntryPoint;
			string type = method.DeclaringType.FullName;
			string mname = method.Name;

			string found = null;
			string error = null;

			Trace.WriteLine ("Trying to load base library: " + library);

			foreach (string name in GetLibraryNames (method.DeclaringType, library, report)) {
				if (LoadLibrary (type, mname, name, entrypoint, report, out error)) {
					found = name;
					break;
				}
			}

			if (found == null) {
				report.Errors.Add (new MessageInfo (
							type, mname,
							"Could not load library `" + library + "': " + error));
				return;
			}

			// UnixFileInfo f = new UnixFileInfo (soname);
			if (found.EndsWith (".so")) {
				report.Warnings.Add (new MessageInfo (type, mname, 
						string.Format ("Library `{0}' might be a development library",
							found)));
			}
		}

		[DllImport ("libgmodule-2.0.so")]
		private static extern IntPtr g_module_open (string filename, int flags);
		private static int G_MODULE_BIND_LAZY = 1 << 0;
		private static int G_MODULE_BIND_LOCAL = 1 << 1;
		// private static int G_MODULE_BIND_MASK = 0x03;

		[DllImport ("libgmodule-2.0.so")]
		private static extern int g_module_close (IntPtr handle);

		[DllImport ("libgmodule-2.0.so")]
		private static extern IntPtr g_module_error ();

		[DllImport ("libgmodule-2.0.so")]
		private static extern IntPtr g_module_name (IntPtr h);

		[DllImport ("libgmodule-2.0.so")]
		private static extern IntPtr g_module_build_path (
			string directory, string module_name);

		[DllImport ("libgmodule-2.0.so")]
		private static extern int g_module_symbol (IntPtr module, 
				string symbol_name, out IntPtr symbol);

		[DllImport ("libglib-2.0.so")]
		private static extern void g_free (IntPtr mem);

		private static string[] GetLibraryNames (Type type, string library, AssemblyCheckInfo report)
		{
			// TODO: keep in sync with
			// mono/metadata/loader.c:mono_lookup_pinvoke_call
			ArrayList names = new ArrayList ();

			string dll_map = report.GetDllmapEntry (type.Assembly.Location, library);
			if (dll_map != null) 
				names.Add (dll_map);

			names.Add (library);
			int _dll_index = library.LastIndexOf (".dll");
			if (_dll_index >= 0)
				names.Add (library.Substring (0, _dll_index));

			if (!library.StartsWith ("lib"))
				names.Add ("lib" + library);

			IntPtr s = g_module_build_path (null, library);
			if (s != IntPtr.Zero) {
				try {
					names.Add (Marshal.PtrToStringAnsi (s));
				}
				finally {
					g_free (s);
				}
			}

			s = g_module_build_path (".", library);
			if (s != IntPtr.Zero) {
				try {
					names.Add (Marshal.PtrToStringAnsi (s));
				}
				finally {
					g_free (s);
				}
			}

			return (string[]) names.ToArray (typeof(string));
		}

		private static bool LoadLibrary (string type, string member, 
				string library, string symbol, AssemblyCheckInfo report, out string error)
		{
			error = null;
			IntPtr h = g_module_open (library, 
					G_MODULE_BIND_LAZY | G_MODULE_BIND_LOCAL);
			try {
				Trace.WriteLine ("    Trying library name: " + library);
				if (h != IntPtr.Zero) {
					string soname = Marshal.PtrToStringAnsi (g_module_name (h));
					Trace.WriteLine ("Able to load library " + library + 
							"; soname=" + soname);
					IntPtr ignore;
					if (g_module_symbol (h, symbol, out ignore) == 0)
						report.Errors.Add (new MessageInfo (
								type, member,
								string.Format ("library `{0}' is missing symbol `{1}'",
									library, symbol)));
					return true;
				}
				error = Marshal.PtrToStringAnsi (g_module_error ());
				Trace.WriteLine ("\tError loading library `" + library + "': " + error);
			}
			finally {
				if (h != IntPtr.Zero)
					g_module_close (h);
			}
			return false;
		}
	}

	class Runner {

		public static void Main (string[] args)
		{
			var references = new List<string> ();
			var prefixes   = new List<string> ();

			List<string> files = new OptionSet {
				{ "p|prefix|prefixes=",
				  "Mono installation prefixes (for $prefix/etc/mono/config)",
				  v => prefixes.Add (v) },
				{ "r|reference|references=",
				  "Assemblies to load by partial names (e.g. from the GAC)",
					v => references.Add (v) },
			}.Parse (args);

			AssemblyChecker checker = new AssemblyChecker ();
			AssemblyCheckInfo report = new AssemblyCheckInfo ();
			if (prefixes.Count == 0) {
				// SystemConfigurationFile is $sysconfdir/mono/VERSION/machine.config
				// We want $sysconfdir
				DirectoryInfo configDir = 
					new FileInfo (RuntimeEnvironment.SystemConfigurationFile).Directory.Parent.Parent.Parent;
				prefixes.Add (configDir.ToString ());
			}
			report.SetInstallationPrefixes (prefixes);
			foreach (string assembly in files) {
				checker.CheckFile (assembly, report);
			}

			foreach (string assembly in references) {
				checker.CheckWithPartialName (assembly, report);
			}

			foreach (MessageInfo m in report.Errors) {
				PrintMessage ("error", m);
			}

			foreach (MessageInfo m in report.Warnings) {
				PrintMessage ("warning", m);
			}
		}

		private static void PrintMessage (string type, MessageInfo m)
		{
			Console.Write ("{0}: ", type);
			if (m.Type != null)
				Console.Write ("in {0}", m.Type);
			if (m.Member != null)
				Console.Write (".{0}: ", m.Member);
			Console.WriteLine (m.Message);
		}
	}
}