Editor/Lifecycle/PackageLocator.cs
using System;
using System.IO;
using Editor;

namespace Sandbox.SecBox.Lifecycle;

// Locates a Package's on-disk folder so we can scan it. Returns ONLY paths
// under <projectRoot>/Libraries/<something>/ - never the project root itself,
// never anything outside the project tree. Engine packages and the project's
// own package both correctly return null, so InstallHook skips them.
internal static class PackageLocator
{
	public static string FolderFor(Package pkg)
	{
		if (pkg == null) return null;

		var ident = pkg.FullIdent ?? pkg.Ident;
		if (string.IsNullOrEmpty(ident)) return null;

		var proj = Project.Current;
		var projectRoot = proj?.RootDirectory?.FullName;
		if (string.IsNullOrEmpty(projectRoot)) return null;

		var libRoot = Path.Combine(projectRoot, "Libraries");
		if (!Directory.Exists(libRoot)) return null;

		// Try ident verbatim.
		var direct = Path.Combine(libRoot, ident);
		if (Directory.Exists(direct) && IsUnderLibRoot(direct, libRoot)) return direct;

		// Try last segment (sometimes folders are <org>.<name> sometimes just <name>).
		var lastSegment = ident.Contains('.')
			? ident.Substring(ident.LastIndexOf('.') + 1)
			: ident;
		var bySegment = Path.Combine(libRoot, lastSegment);
		if (Directory.Exists(bySegment) && IsUnderLibRoot(bySegment, libRoot)) return bySegment;

		// LocalPackage's CodePath. CodePath is usually <something>/Code - we want
		// the parent. But ONLY accept it if that parent lands under Libraries/.
		// Without that check, the deadlock_district project's own LocalPackage
		// (CodePath = <projectRoot>/Code, parent = <projectRoot>) would resolve
		// to the entire project - catastrophic over-scan.
		var codePath = ReflectionHelpers.GetProp(pkg, "CodePath") as string;
		if (!string.IsNullOrEmpty(codePath) && Directory.Exists(codePath))
		{
			var parent = Path.GetDirectoryName(codePath);
			if (!string.IsNullOrEmpty(parent) && Directory.Exists(parent) && IsUnderLibRoot(parent, libRoot))
				return parent;
		}

		return null;
	}

	// Defence-in-depth - ensures returned paths are strict descendants of
	// <projectRoot>/Libraries/. Refuses Libraries/ itself.
	static bool IsUnderLibRoot(string candidate, string libRoot)
	{
		try
		{
			var full = Path.GetFullPath(candidate).TrimEnd(Path.DirectorySeparatorChar);
			var root = Path.GetFullPath(libRoot).TrimEnd(Path.DirectorySeparatorChar);
			if (string.Equals(full, root, StringComparison.OrdinalIgnoreCase)) return false;
			return full.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
		}
		catch { return false; }
	}

	public static string CurrentProjectRoot()
	{
		try { return Project.Current?.RootDirectory?.FullName; }
		catch { return null; }
	}

	// Locate our own library's on-disk folder by scanning LibrarySystem.All
	// for the one whose root contains secbox.sbproj. Content-based -
	// survives the engine's "{org}.{ident}#local" FullIdent format and any
	// renames. Assembly.Location is unusable here because the editor
	// compiles adapter assemblies in-memory.
	public static string CurrentSecboxLibraryRoot()
	{
		try
		{
			var libs = LibrarySystem.All;
			if (libs == null) return null;

			foreach (var lib in libs)
			{
				string root = null;
				try { root = lib?.Project?.RootDirectory?.FullName; }
				catch { }

				if (!string.IsNullOrEmpty(root) && File.Exists(Path.Combine(root, "secbox.sbproj")))
					return root;
			}
		}
		catch { }

		return null;
	}
}