/*
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)}");
// }
// }
}