//
// permview.cs: Managed Permission Viewer for .NET assemblies
//
// Author:
//	Sebastien Pouliot  <sebastien@ximian.com>
//
// Copyright (C) 2004-2007 Novell, Inc (http://www.novell.com)
//

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Security;
using SSP = System.Security.Permissions;
using System.Text;

using Mono.Cecil;

[assembly: AssemblyTitle ("Mono PermView")]
[assembly: AssemblyDescription ("Managed Permission Viewer for .NET assemblies")]

namespace Mono.Tools {

	static class SecurityDeclarationRocks {

		public static PermissionSet ToPermissionSet (this SecurityDeclaration self)
		{
			if (self == null)
				throw new ArgumentNullException ("self");

			PermissionSet set;
			if (TryProcessPermissionSetAttribute (self, out set))
				return set;

			return CreatePermissionSet (self);
		}

		static bool TryProcessPermissionSetAttribute (SecurityDeclaration declaration, out PermissionSet set)
		{
			set = null;

			if (!declaration.HasSecurityAttributes && declaration.SecurityAttributes.Count != 1)
				return false;

			var security_attribute = declaration.SecurityAttributes [0];
			var attribute_type = security_attribute.AttributeType;

			if (attribute_type.Name != "PermissionSetAttribute" || attribute_type.Namespace != "System.Security.Permissions")
				return false;

			var named_argument = security_attribute.Properties [0];
			if (named_argument.Name != "XML")
				throw new NotSupportedException ();

			var attribute = new SSP.PermissionSetAttribute ((SSP.SecurityAction) declaration.Action);
			attribute.XML = (string) named_argument.Argument.Value;

			set = attribute.CreatePermissionSet ();
			return true;
		}

		static PermissionSet CreatePermissionSet (SecurityDeclaration declaration)
		{
			var set = new PermissionSet (SSP.PermissionState.None);

			foreach (var attribute in declaration.SecurityAttributes) {
				var permission = CreatePermission (declaration, attribute);
				set.AddPermission (permission);
			}

			return set;
		}

		static IPermission CreatePermission (SecurityDeclaration declaration, SecurityAttribute attribute)
		{
			var attribute_type = Type.GetType (attribute.AttributeType.FullName);
			if (attribute_type == null)
				throw new ArgumentException ();

			var security_attribute = CreateSecurityAttribute (attribute_type, declaration);
			if (security_attribute == null)
				throw new InvalidOperationException ();

			CompleteSecurityAttribute (security_attribute, attribute);

			return security_attribute.CreatePermission ();
		}

		static void CompleteSecurityAttribute (SSP.SecurityAttribute security_attribute, SecurityAttribute attribute)
		{
			if (attribute.HasFields)
				CompleteSecurityAttributeFields (security_attribute, attribute);

			if (attribute.HasProperties)
				CompleteSecurityAttributeProperties (security_attribute, attribute);
		}

		static void CompleteSecurityAttributeFields (SSP.SecurityAttribute security_attribute, SecurityAttribute attribute)
		{
			var type = security_attribute.GetType ();

			foreach (var named_argument in attribute.Fields)
				type.GetField (named_argument.Name).SetValue (security_attribute, named_argument.Argument.Value);
		}

		static void CompleteSecurityAttributeProperties (SSP.SecurityAttribute security_attribute, SecurityAttribute attribute)
		{
			var type = security_attribute.GetType ();

			foreach (var named_argument in attribute.Properties)
				type.GetProperty (named_argument.Name).SetValue (security_attribute, named_argument.Argument.Value, null);
		}

		static SSP.SecurityAttribute CreateSecurityAttribute (Type attribute_type, SecurityDeclaration declaration)
		{
			SSP.SecurityAttribute security_attribute;
			try {
				security_attribute = (SSP.SecurityAttribute) Activator.CreateInstance (
					attribute_type, new object [] { (SSP.SecurityAction) declaration.Action });
			} catch (MissingMethodException) {
				security_attribute = (SSP.SecurityAttribute) Activator.CreateInstance (attribute_type, new object [0]);
			}

			return security_attribute;
		}
	}

	class SecurityElementComparer : IComparer {

		public int Compare (object x, object y)
		{
			SecurityElement sx = (x as SecurityElement);
			SecurityElement sy = (y as SecurityElement);
			if (sx == null)
				return (sy == null) ? 0 : -1;
			else if (sy == null)
				return 1;

			// compare by name (type name, method name, action name)
			return String.Compare (sx.Attribute ("Name"), sy.Attribute ("Name"));
		}
	}

	class PermView {

		private const string NotSpecified = "\tNot specified.";

		static private void Help () 
		{
			Console.WriteLine ("Usage: permview [options] assembly{0}", Environment.NewLine);
			Console.WriteLine ("where options are:");
			Console.WriteLine (" -output filename  Output information into specified file.");
			Console.WriteLine (" -decl             Show declarative security attributes on classes and methods.");
			Console.WriteLine (" -xml              Output in XML format");
			Console.WriteLine (" -help             Show help informations (this text)");
			Console.WriteLine ();
		}

		static bool declarative = false;
		static bool xmloutput = false;

		static TextWriter ProcessOptions (string[] args)
		{
			TextWriter tw = Console.Out;
			for (int i=0; i < args.Length - 1; i++) {
				switch (args [i].ToUpper ()) {
				case "/DECL":
				case "-DECL":
				case "--DECL":
					declarative = true;
					break;
				case "/OUTPUT":
				case "-OUTPUT":
				case "--OUTPUT":
					tw = (TextWriter) new StreamWriter (args [++i]);
					break;
				case "/XML":
				case "-XML":
				case "--XML":
					xmloutput = true;
					break;
				case "/HELP":
				case "/H":
				case "-HELP":
				case "-H":
				case "--HELP":
				case "--H":
				case "-?":
				case "--?":
					Help ();
					return null;
				}
			}
			return tw;
		}

		static bool ProcessAssemblyOnly (TextWriter tw, AssemblyDefinition ad) 
		{
			bool result = true;
			string minimal = NotSpecified + Environment.NewLine;
			string optional = NotSpecified + Environment.NewLine;
			string refused = NotSpecified + Environment.NewLine;

			foreach (SecurityDeclaration decl in ad.SecurityDeclarations) {
				switch (decl.Action) {
				case Mono.Cecil.SecurityAction.RequestMinimum:
					minimal = decl.ToPermissionSet ().ToString ();
					break;
				case Mono.Cecil.SecurityAction.RequestOptional:
					optional = decl.ToPermissionSet ().ToString ();
					break;
				case Mono.Cecil.SecurityAction.RequestRefuse:
					refused = decl.ToPermissionSet ().ToString ();
					break;
				default:
					tw.WriteLine ("Invalid assembly level declaration {0}{1}{2}",
						decl.Action, Environment.NewLine, decl.ToPermissionSet ());
					result = false;
					break;
				}
			}

			tw.WriteLine ("Minimal Permission Set:");
			tw.WriteLine (minimal);
			tw.WriteLine ("Optional Permission Set:");
			tw.WriteLine (optional);
			tw.WriteLine ("Refused Permission Set:");
			tw.WriteLine (refused);
			return result;
		}

		static void ShowSecurity (TextWriter tw, string header, IEnumerable<SecurityDeclaration> declarations)
		{
			foreach (SecurityDeclaration declsec in declarations) {
				tw.WriteLine ("{0} {1} Permission Set:{2}{3}", header,
					declsec.Action, Environment.NewLine, declsec.ToPermissionSet ());
			}
		}

		static bool ProcessAssemblyComplete (TextWriter tw, AssemblyDefinition ad)
		{
			if (ad.SecurityDeclarations.Count > 0) {
				ShowSecurity (tw, "Assembly", ad.SecurityDeclarations);
			}

			foreach (ModuleDefinition module in ad.Modules) {

				foreach (TypeDefinition type in module.Types) {

					if (type.SecurityDeclarations.Count > 0) {
						ShowSecurity (tw, "Class " + type.ToString (), ad.SecurityDeclarations);
					}

					foreach (MethodDefinition method in type.Methods) {
						if (method.SecurityDeclarations.Count > 0) {
							ShowSecurity (tw, "Method " + method.ToString (), method.SecurityDeclarations);
						}
					}
				}
			}
			return true;
		}

		static void AddAttribute (SecurityElement se, string attr, string value)
		{
			value = value.Replace ("&", "&amp;");
			se.AddAttribute (attr, value);
		}

		static SecurityElement AddSecurityXml (IEnumerable<SecurityDeclaration> declarations)
		{
			ArrayList list = new ArrayList ();
			foreach (SecurityDeclaration declsec in declarations) {
				SecurityElement child = new SecurityElement ("Action");
				AddAttribute (child, "Name", declsec.Action.ToString ());
				child.AddChild (declsec.ToPermissionSet ().ToXml ());
				list.Add (child);
			}
			// sort actions
			list.Sort (Comparer);

			SecurityElement se = new SecurityElement ("Actions");
			foreach (SecurityElement child in list) {
				se.AddChild (child);
			}
			return se;
		}

		static SecurityElementComparer comparer;
		static IComparer Comparer {
			get {
				if (comparer == null)
					comparer = new SecurityElementComparer ();
				return comparer;
			}
		}

		static bool ProcessAssemblyXml (TextWriter tw, AssemblyDefinition ad)
		{
			SecurityElement se = new SecurityElement ("Assembly");
			se.AddAttribute ("Name", ad.Name.FullName);

			if (ad.SecurityDeclarations.Count > 0) {
				se.AddChild (AddSecurityXml (ad.SecurityDeclarations));
			}

			ArrayList tlist = new ArrayList ();
			ArrayList mlist = new ArrayList ();

			foreach (ModuleDefinition module in ad.Modules) {

				foreach (TypeDefinition type in module.Types) {

					SecurityElement klass = new SecurityElement ("Class");
					SecurityElement methods = new SecurityElement ("Methods");

					SecurityElement typelem = null;
					if (type.SecurityDeclarations.Count > 0) {
						typelem = AddSecurityXml (type.SecurityDeclarations);
					}

					if (mlist.Count > 0)
						mlist.Clear ();

					foreach (MethodDefinition method in type.Methods) {
						if (method.SecurityDeclarations.Count > 0) {
							SecurityElement meth = new SecurityElement ("Method");
							AddAttribute (meth, "Name", method.ToString ());
							meth.AddChild (AddSecurityXml (method.SecurityDeclarations));
							mlist.Add (meth);
						}
					}

					// sort methods
					mlist.Sort (Comparer);
					foreach (SecurityElement method in mlist) {
						methods.AddChild (method);
					}

					if ((typelem != null) || ((methods.Children != null) && (methods.Children.Count > 0))) {
						AddAttribute (klass, "Name", type.ToString ());
						if (typelem != null)
							klass.AddChild (typelem);
						if ((methods.Children != null) && (methods.Children.Count > 0))
							klass.AddChild (methods);
						tlist.Add (klass);
					}
				}

				// sort types
				tlist.Sort (Comparer);
				foreach (SecurityElement type in tlist) {
					se.AddChild (type);
				}
			}

			tw.WriteLine (se.ToString ());
			return true;
		}

		[STAThread]
		static int Main (string[] args) 
		{
			try {
				Console.WriteLine (new AssemblyInfo ().ToString ());
				if (args.Length == 0) {
					Help ();
					return 0;
				}

				TextWriter tw = ProcessOptions (args);
				if (tw == null)
					return 0;

				string assemblyName = args [args.Length - 1];
				AssemblyDefinition ad = AssemblyDefinition.ReadAssembly (assemblyName);
				if (ad != null) {
					bool complete = false;
					
					if (declarative) {
						// full output (assembly+classes+methods)
						complete = ProcessAssemblyComplete (tw, ad);
					} else if (xmloutput) {
						// full output in XML (for easier diffs after c14n)
						complete = ProcessAssemblyXml (tw, ad);
					} else {
						// default (assembly only)
						complete = ProcessAssemblyOnly (tw, ad);
					}

					if (!complete) {
						Console.Error.WriteLine ("Couldn't reflect informations.");
						return 1;
					}
				} else {
					Console.Error.WriteLine ("Couldn't load assembly '{0}'.", assemblyName);
					return 2;
				}
				tw.Close ();
			}
			catch (Exception e) {
				Console.Error.WriteLine ("Error: " + e.ToString ());
				Help ();
				return 3;
			}
			return 0;
		}
	}
}