1 module served.lsp.uri; 2 3 import served.lsp.protocol; 4 import std.algorithm; 5 import std.conv; 6 import std.path; 7 import std.string; 8 9 DocumentUri uriFromFile(scope const(char)[] file) 10 { 11 import std.uri : encodeComponent; 12 13 if (!isAbsolute(file)) 14 throw new Exception(text("Tried to pass relative path '", file, "' to uriFromFile")); 15 file = file.buildNormalizedPath.replace("\\", "/"); 16 if (file.length == 0) 17 return ""; 18 if (file[0] != '/') 19 file = '/' ~ file; // always triple slash at start but never quad slash 20 if (file.length >= 2 && file[0 .. 2] == "//") // Shares (\\share\bob) are different somehow 21 file = file[2 .. $]; 22 return text("file://", file.encodeComponent.replace("%2F", "/")); 23 } 24 25 string uriToFile(DocumentUri uri) 26 { 27 import std.uri : decodeComponent; 28 import std.string : startsWith; 29 30 if (uri.startsWith("file://")) 31 { 32 string ret = uri["file://".length .. $].decodeComponent; 33 if (ret.length >= 3 && ret[0] == '/' && ret[2] == ':') 34 return ret[1 .. $].replace("/", "\\"); 35 else if (ret.length >= 1 && ret[0] != '/') 36 return "\\\\" ~ ret.replace("/", "\\"); 37 return ret; 38 } 39 else 40 return null; 41 } 42 43 @system unittest 44 { 45 void testUri(string a, string b) 46 { 47 void assertEqual(A, B)(A a, B b) 48 { 49 import std.conv : to; 50 51 assert(a == b, a.to!string ~ " is not equal to " ~ b.to!string); 52 } 53 54 assertEqual(a.uriFromFile, b); 55 assertEqual(a, b.uriToFile); 56 assertEqual(a.uriFromFile.uriToFile, a); 57 } 58 59 version (Windows) 60 { 61 // taken from vscode-uri 62 testUri(`c:\test with %\path`, `file:///c%3A/test%20with%20%25/path`); 63 testUri(`c:\test with %25\path`, `file:///c%3A/test%20with%20%2525/path`); 64 testUri(`c:\test with %25\c#code`, `file:///c%3A/test%20with%20%2525/c%23code`); 65 testUri(`\\shäres\path\c#\plugin.json`, `file://sh%C3%A4res/path/c%23/plugin.json`); 66 testUri(`\\localhost\c$\GitDevelopment\express`, `file://localhost/c%24/GitDevelopment/express`); 67 } 68 else version (Posix) 69 { 70 testUri(`/home/pi/.bashrc`, `file:///home/pi/.bashrc`); 71 testUri(`/home/pi/Development Projects/D-code`, `file:///home/pi/Development%20Projects/D-code`); 72 } 73 } 74 75 DocumentUri uri(string scheme, string authority, string path, string query, string fragment) 76 { 77 return scheme ~ "://" 78 ~ (authority.length ? authority : "") 79 ~ (path.length ? path : "/") 80 ~ (query.length ? "?" ~ query : "") 81 ~ (fragment.length ? "#" ~ fragment : ""); 82 } 83 84 DocumentUri uriDirName(DocumentUri uri) 85 { 86 auto slash = uri.lastIndexOf('/'); 87 if (slash <= 0) 88 return uri; 89 if (uri[slash - 1] == '/') 90 { 91 if (slash == uri.length - 1) 92 return uri; 93 else 94 return uri[0 .. slash + 1]; 95 } 96 return uri[0 .. slash]; 97 } 98 99 /// 100 unittest 101 { 102 assert("file:///foo/bar/".uriDirName == "file:///foo/bar"); 103 assert("file:///foo/bar".uriDirName == "file:///foo"); 104 assert("file:///foo/bar".uriDirName.uriDirName == "file:///"); 105 } 106 107 /// Appends the path to the uri, potentially replacing the whole thing if the 108 /// path is absolute. Cleans `./` and `../` sequences using `uriNormalize`. 109 DocumentUri uriBuildNormalized(DocumentUri uri, scope const(char)[] path) 110 { 111 if (isAbsolute(path)) 112 return uriFromFile(path); 113 114 path = path.replace("\\", "/"); 115 116 if (path.startsWith("/")) 117 { 118 auto scheme = uri.indexOf("://"); 119 if (scheme == -1) 120 return path.idup; 121 else 122 return text(uri[0 .. scheme + 3], path[1 .. $]); 123 } 124 125 while (path.startsWith("../")) 126 { 127 uri = uri.uriDirName; 128 path = path[3 .. $]; 129 } 130 131 if (path.startsWith("/")) 132 path = path[1 .. $]; 133 134 return text(uri, "/", path).uriNormalize; 135 } 136 137 /// 138 unittest 139 { 140 assert(uriBuildNormalized("file:///foo/bar", "baz") == "file:///foo/bar/baz"); 141 assert(uriBuildNormalized("file:///foo/bar", "../baz") == "file:///foo/baz"); 142 version (Windows) 143 assert(uriBuildNormalized("file:///foo/bar", `c:\home\baz`) == "file:///c%3A/home/baz"); 144 else 145 assert(uriBuildNormalized("file:///foo/bar", "/home/baz") == "file:///home/baz"); 146 } 147 148 /// Cleans `./` and `../` from the URI 149 DocumentUri uriNormalize(DocumentUri uri) 150 { 151 while (true) 152 { 153 auto sameDir = uri.indexOf("./"); 154 if (sameDir == -1) 155 break; 156 if (sameDir == 0) 157 uri = uri[2 .. $]; 158 else if (sameDir >= 2 && uri[sameDir - 1] == '.' && uri[sameDir - 2] == '/') // /../ 159 uri = uri[0 .. sameDir - 1].uriDirName ~ uri[sameDir + 2 .. $]; 160 else if (uri[sameDir - 1] == '/') // /./ 161 uri = uri[0 .. sameDir] ~ uri[sameDir + 2 .. $]; 162 else 163 break; // might break on `a./b` here, but better than infinite loop for malformed url 164 } 165 166 return uri; 167 }