/* This file is part of Utatane. Utatane is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Utatane is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Utatane. If not, see . */ #pragma warning disable BL0006 using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using FluentResults; using Functional.DiscriminatedUnion; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Mvc.Rendering; using NUglify; using NUglify.Html; namespace Utatane; public static class Utils { public static String Root = Environment.GetEnvironmentVariable("nhnd_Utatane_ROOT") ?? throw new NullReferenceException("env nhnd_Utatane_ROOT not set!"); public static List DoNotServe = ["*cookies.txt"]; private static readonly HtmlSettings HtmlSettings = new HtmlSettings() { RemoveComments = false, RemoveOptionalTags = false, RemoveInvalidClosingTags = false, RemoveEmptyAttributes = false, RemoveScriptStyleTypeAttribute = false, ShortBooleanAttribute = false, IsFragmentOnly = true, MinifyJs = false, MinifyJsAttributes = false, MinifyCss = false, MinifyCssAttributes = false, }; public static String OptimizeHtml(String html) { return Uglify.Html(html, HtmlSettings).Code ?? String.Empty; } public static Result> VerifyPath(String path) { if (DoNotServe.Contains(path)) return Result.Fail($"Do Not Serve: {path}"); String fullPath = Path.GetFullPath(Path.Join(Root, path)); if (!fullPath.StartsWith(Root)) return Result.Fail($"Outside root: {path} -> {fullPath}"); var maybeDir = new DirectoryInfo(fullPath); if (maybeDir.Exists) return Result.Ok>(maybeDir); var maybeFile = new FileInfo(fullPath); if (maybeFile.Exists) return Result.Ok>(maybeFile); return Result.Fail($"Does not exist: {path} -> {fullPath}"); } public static String FormatFileSize(Int64 size) { const Int64 terabyte = 1099511627776; // 2**40 const Int64 gigabyte = 1073741824; // 2**30 const Int64 megabyte = 1048576; // 2**20 const Int64 kilobyte = 1024; // 2**10 double sizeD = size; return size switch { > terabyte => $"{(sizeD / terabyte):F2} TiB ", > gigabyte => $"{(sizeD / gigabyte):F2} GiB", > megabyte => $"{(sizeD / megabyte):F2} MiB", > kilobyte => $"{(sizeD / kilobyte):F2} KiB", _ => $"{size} B" }; } public static MarkupString AbbrIcon(String text, String symbol) { return (MarkupString)AbbrIconPlain(text, symbol); } public static String AbbrIconPlain(String text, String symbol) { RenderTreeBuilder renderer = new RenderTreeBuilder(); var abbr = new TagBuilder("abbr"); abbr.Attributes.Add("title", text); abbr.InnerHtml.Append(symbol); using var writer = new System.IO.StringWriter(); abbr.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default); return writer.ToString(); } private static readonly Dictionary ExtToAbbr = new Dictionary { #region Extension <--> Icon mappings { ".avc", AbbrIcon("Video file", "🎞️") }, { ".flv", AbbrIcon("Video file", "🎞️") }, { ".mts", AbbrIcon("Video file", "🎞️") }, { ".m2ts", AbbrIcon("Video file", "🎞️") }, { ".m4v", AbbrIcon("Video file", "🎞️") }, { ".mkv", AbbrIcon("Video file", "🎞️") }, { ".mov", AbbrIcon("Video file", "🎞️") }, { ".mp4", AbbrIcon("Video file", "🎞️") }, { ".ts", AbbrIcon("Video file", "🎞️") }, { ".webm", AbbrIcon("Video file", "🎞️") }, { ".wmv", AbbrIcon("Video file", "🎞️") }, { ".aac", AbbrIcon("Audio file", "🔊") }, { ".alac", AbbrIcon("Audio file", "🔊") }, { ".flac", AbbrIcon("Audio file", "🔊") }, { ".m4a", AbbrIcon("Audio file", "🔊") }, { ".mp3", AbbrIcon("Audio file", "🔊") }, { ".opus", AbbrIcon("Audio file", "🔊") }, { ".wav", AbbrIcon("Audio file", "🔊") }, { ".ogg", AbbrIcon("Audio file", "🔊") }, { ".mus", AbbrIcon("Audio file", "🔊") }, { ".avif", AbbrIcon("Image file", "🖼️") }, { ".bmp", AbbrIcon("Image file", "🖼️") }, { ".gif", AbbrIcon("Image file", "🖼️") }, { ".ico", AbbrIcon("Image file", "🖼️") }, { ".heic", AbbrIcon("Image file", "🖼️") }, { ".heif", AbbrIcon("Image file", "🖼️") }, { ".jpe?g", AbbrIcon("Image file", "🖼️") }, { ".jfif", AbbrIcon("Image file", "🖼️") }, { ".jxl", AbbrIcon("Image file", "🖼️") }, { ".j2c", AbbrIcon("Image file", "🖼️") }, { ".jp2", AbbrIcon("Image file", "🖼️") }, { ".a?png", AbbrIcon("Image file", "🖼️") }, { ".svg", AbbrIcon("Image file", "🖼️") }, { ".tiff?", AbbrIcon("Image file", "🖼️") }, { ".webp", AbbrIcon("Image file", "🖼️") }, { ".pdn", AbbrIcon("Image file", "🖼️") }, { ".psd", AbbrIcon("Image file", "🖼️") }, { ".xcf", AbbrIcon("Image file", "🖼️") }, { ".ass", AbbrIcon("Subtitle file", "💬") }, { ".lrc", AbbrIcon("Subtitle file", "💬") }, { ".srt", AbbrIcon("Subtitle file", "💬") }, { ".srv3", AbbrIcon("Subtitle file", "💬") }, { ".ssa", AbbrIcon("Subtitle file", "💬") }, { ".vtt", AbbrIcon("Subtitle file", "💬") }, { ".bat", AbbrIcon("Windows script file", "📜") }, { ".cmd", AbbrIcon("Windows script file", "📜") }, { ".htm", AbbrIcon("HTML file", "📜") }, { ".html", AbbrIcon("HTML file", "📜") }, { ".xhtml", AbbrIcon("XHTML file", "📜") }, { ".bash", AbbrIcon("Shell script", "📜") }, { ".zsh", AbbrIcon("Shell script", "📜") }, { ".sh", AbbrIcon("Shell script", "📜") }, { ".cpp", AbbrIcon("C++ source file", "📜") }, { ".cxx", AbbrIcon("C++ source file", "📜") }, { ".cc", AbbrIcon("C++ source file", "📜") }, { ".hpp", AbbrIcon("C++ header file", "📜") }, { ".hxx", AbbrIcon("C++ header file", "📜") }, { ".hh", AbbrIcon("C++ header file", "📜") }, { ".py", AbbrIcon("Python script", "📜") }, { ".pyc", AbbrIcon("Compiled Python bytecode", "📜") }, { ".pyo", AbbrIcon("Compiled Python bytecode", "📜") }, { ".psm1", AbbrIcon("PowerShell module file", "📜") }, { ".psd1", AbbrIcon("PowerShell data file", "📜") }, { ".ps1", AbbrIcon("PowerShell script", "📜") }, { ".js", AbbrIcon("JavaScript source code", "📜") }, { ".css", AbbrIcon("CSS style sheet", "📜") }, { ".cs", AbbrIcon("C# source file", "📜") }, { ".c", AbbrIcon("C source file", "📜") }, { ".h", AbbrIcon("C header file", "📜") }, { ".java", AbbrIcon("Java source file", "📜") }, { ".json", AbbrIcon("Data/config file", "📜") }, { ".json5", AbbrIcon("Data/config file", "📜") }, { ".xml", AbbrIcon("Data/config file", "📜") }, { ".yaml", AbbrIcon("Data/config file", "📜") }, { ".yml", AbbrIcon("Data/config file", "📜") }, { ".ini", AbbrIcon("Data/config file", "📜") }, { ".toml", AbbrIcon("Data/config file", "📜") }, { ".cfg", AbbrIcon("Data/config file", "📜") }, { ".conf", AbbrIcon("Data/config file", "📜") }, { ".plist", AbbrIcon("Data/config file", "📜") }, { ".csv", AbbrIcon("Data/config file", "📜") }, { ".tar", AbbrIcon("File archive", "📦") }, { ".ar", AbbrIcon("File archive", "📦") }, { ".7z", AbbrIcon("File archive", "📦") }, { ".arc", AbbrIcon("File archive", "📦") }, { ".cab", AbbrIcon("File archive", "📦") }, { ".rar", AbbrIcon("File archive", "📦") }, { ".zip", AbbrIcon("File archive", "📦") }, { ".bz2", AbbrIcon("File archive", "📦") }, { ".gz", AbbrIcon("File archive", "📦") }, { ".lz", AbbrIcon("File archive", "📦") }, { ".lzma", AbbrIcon("File archive", "📦") }, { ".lzo", AbbrIcon("File archive", "📦") }, { ".xz", AbbrIcon("File archive", "📦") }, { ".Z", AbbrIcon("File archive", "📦") }, { ".zst", AbbrIcon("File archive", "📦") }, { ".apk", AbbrIcon("Android package", "📦") }, { ".deb", AbbrIcon("Debian package", "📦") }, { ".rpm", AbbrIcon("RPM package", "📦") }, { ".ipa", AbbrIcon("iOS/iPadOS package", "📦") }, { ".AppImage", AbbrIcon("AppImage bundle", "📦") }, { ".jar", AbbrIcon("Java archive", "☕") }, { ".dmg", AbbrIcon("Disk image", "💿") }, { ".iso", AbbrIcon("Disk image", "💿") }, { ".img", AbbrIcon("Disk image", "💿") }, { ".wim", AbbrIcon("Disk image", "💿") }, { ".esd", AbbrIcon("Disk image", "💿") }, { ".docx", AbbrIcon("Document", "📃") }, { ".doc", AbbrIcon("Document", "📃") }, { ".odt", AbbrIcon("Document", "📃") }, { ".pptx", AbbrIcon("Presentation", "📃") }, { ".ppt", AbbrIcon("Presentation", "📃") }, { ".odp", AbbrIcon("Presentation", "📃") }, { ".xslx", AbbrIcon("Spreadsheet", "📃") }, { ".xsl", AbbrIcon("Spreadsheet", "📃") }, { ".ods", AbbrIcon("Spreadsheet", "📃") }, { ".pdf", AbbrIcon("PDF", "📃") }, { ".md", AbbrIcon("Markdown document", "📃") }, { ".rst", AbbrIcon("reStructuredText document", "📃") }, { ".epub", AbbrIcon("EPUB e-book file", "📃") }, { ".log", AbbrIcon("Log file", "📃") }, { ".txt", AbbrIcon("Text file", "📃") }, { ".tff", AbbrIcon("Font file", "🗛") }, { ".otf", AbbrIcon("Font file", "🗛") }, { ".woff", AbbrIcon("Font file", "🗛") }, { ".woff2", AbbrIcon("Font file", "🗛") }, { ".mpls", AbbrIcon("Playlist file", "🎶") }, { ".m3u", AbbrIcon("Playlist file", "🎶") }, { ".m3u8", AbbrIcon("Playlist file", "🎶") }, { ".exe", AbbrIcon("Generic executable", "🔳") }, { ".elf", AbbrIcon("Generic executable", "🔳") }, { ".msi", AbbrIcon("Generic executable", "🔳") }, { ".msix", AbbrIcon("Generic executable", "🔳") }, { ".msixbundle", AbbrIcon("Generic executable", "🔳") }, { ".appx", AbbrIcon("Generic executable", "🔳") }, { ".appxbundle", AbbrIcon("Generic executable", "🔳") }, { ".dll", AbbrIcon("Dynamic library", "⚙️") }, { ".so", AbbrIcon("Dynamic library", "⚙️") }, { ".dylib", AbbrIcon("Dynamic library", "⚙️") } #endregion }; public static MarkupString GetIconForFileType(FileSystemInfo fsi) { if (fsi is DirectoryInfo dir) { return AbbrIcon("Directory", "📁"); } if (ExtToAbbr.TryGetValue(fsi.Name, out var abbr)) return ExtToAbbr[fsi.Extension]; return fsi.EuidAccess_R_X() ? AbbrIcon("Generic executable (+x)", "🔳") : AbbrIcon("File", "📄"); } extension(IEnumerable enumerable) { public T RandomElement() { var list = enumerable.ToList(); int index = (new Random()).Next(0, list.Count); return list.ElementAt(index); } } #pragma warning disable CA2101 // DO NOT mark these as `CharSet = CharSet.Unicode`, this will break things!! [DllImport("libc.so.6")] private static extern int euidaccess(String pathname, int mode); [DllImport("libc.so.6", SetLastError=true)] private static extern String? realpath(String pathname, IntPtr resolved); #pragma warning restore CA2101 private const int R_OK = 0b0100; private const int W_OK = 0b0010; private const int X_OK = 0b0001; extension(FileSystemInfo fsi) { public bool EuidAccess_R() { if (OperatingSystem.IsWindows()) { throw new NotImplementedException("No libc (euidaccess) on Windows!"); } return euidaccess(fsi.FullName, R_OK) == 0; } public bool EuidAccess_R_X() { if (OperatingSystem.IsWindows()) { throw new NotImplementedException("No libc (euidaccess) on Windows!"); } return euidaccess(fsi.FullName, R_OK | X_OK) == 0; } public bool IsReadable() { if (!fsi.Exists) { return false; } if (fsi is DirectoryInfo dir) { return dir.EuidAccess_R_X(); } return fsi.EuidAccess_R(); } // wrapper(win)/replacement(*nix) for ResolveLinkTarget // if not link, return self // see https://github.com/PowerShell/PowerShell/issues/25724 public FileSystemInfo? ReadLink() { if (!fsi.Attributes.HasFlag(FileAttributes.ReparsePoint)) { return null; } if (OperatingSystem.IsWindows()) { return fsi.ResolveLinkTarget(true); } Process rp = new Process { StartInfo = new ProcessStartInfo("realpath", ["-m", fsi.FullName]) { UseShellExecute = false, RedirectStandardOutput = true } }; rp.Start(); var real = rp.StandardOutput.ReadLine(); rp.WaitForExit(); if (Directory.Exists(real)) { return new DirectoryInfo(real); } // the file at real might not actually exist, but we want to return it anyways return new FileInfo(real); } // TODO: uhh this might loop if the target doesnt exist public FileSystemInfo UnravelLink() { FileSystemInfo floor = fsi ?? throw new ArgumentNullException(nameof(fsi)); while (true) { if (floor!.ReadLink() != null) { floor = floor!.ReadLink()!; } else { return floor; } } } } extension(RenderFragment fragment) { public String RenderString() { StringBuilder builder = new StringBuilder(); RenderTreeBuilder renderer = new RenderTreeBuilder(); fragment(renderer); renderer.GetFrames().Array.ToList() .ForEach(f => { // this seems like the only types that have actual content? // idk tho switch (f.FrameType) { case RenderTreeFrameType.Markup: builder.Append(f.MarkupContent); break; case RenderTreeFrameType.Text: builder.Append(f.TextContent); break; default: break; } }); return builder.ToString(); } } extension(String str) { public String ReplaceRegex(String regex, String replacement) { return Regex.Replace(str, regex, replacement); } public String ReplaceRegex(String regex, MatchEvaluator replacement) { return Regex.Replace(str, regex, replacement); } public bool Matches(String regex) { return Regex.IsMatch(str, regex); } } // extension(OneResult oneResult) { // public bool Is() where Tx : T1 { // return typeof(Tx) is typeof(T1) ? oneResult.IsT1 : false; // } // // public Tx As() where Tx : T1 { // return oneResult.IsT1 ? (Tx)oneResult.AsT1() : throw new InvalidOperationException($"Cannot cast value to type {nameof(Tx)}"); // } // } // // extension(OneResult oneResult) { // public bool Is() where Tx : T2 { // return typeof(Tx) is typeof(T2) ? oneResult.IsT2 : false; // } // // public Tx As() where Tx : T2 { // return oneResult.IsT2 ? (Tx)oneResult.AsT2() : throw new InvalidOperationException($"Cannot cast value to type {nameof(Tx)}"); // } // } // // extension(OneResult oneResult) { // public bool Is() where Tx : T3 { // return typeof(Tx) is typeof(T3) ? oneResult.IsT3 : false; // } // // public Tx As() where Tx : T3 { // return oneResult.IsT3 ? (Tx)oneResult.AsT3() : throw new InvalidOperationException($"Cannot cast value to type {nameof(Tx)}"); // } // } // // extension(OneResult oneResult) { // public bool Is(Type tx) where Tx : T4 { // return typeof(Tx) is typeof(T4) ? oneResult.IsT4 : false; // } // // public Tx As() where Tx : T4 { // return oneResult.IsT4 ? (Tx)oneResult.AsT4() : throw new InvalidOperationException($"Cannot cast value to type {nameof(Tx)}"); // } // } }