Source: xxe/res/URIUtil.js

  1. const CSRI_SEPARATOR_NO_AUTHORITY = ")@no-authority/";
  2. const CSRI_SEPARATOR_OPAQUE_URI = ")@opaque-uri/";
  3. const CSRI_SEPARATORS = [
  4. CSRI_SEPARATOR_NO_AUTHORITY,
  5. CSRI_SEPARATOR_OPAQUE_URI,
  6. ")@",
  7. ")"
  8. ];
  9. /**
  10. * The file path separator on Windows , that is, <code>'\'</code>.
  11. * @type string
  12. */
  13. const WINDOWS_FILE_PATH_SEPARATOR = '\\';
  14. /**
  15. * The platform specific file path separator, that is, <code>'\'</code>
  16. * on Windows and <code>'/'</code> on the other platforms.
  17. * @type string
  18. */
  19. const FILE_PATH_SEPARATOR =
  20. PLATFORM_IS_WINDOWS? WINDOWS_FILE_PATH_SEPARATOR : '/';
  21. /**
  22. * Some helper functions (static methods) related to URIs and to file paths.
  23. */
  24. /*TEST|export|TEST*/ class URIUtil {
  25. // -----------------------------------------------------------------------
  26. // "csri:" server specific URL to normal client URI
  27. // -----------------------------------------------------------------------
  28. /**
  29. * Converts specified URI to an actual URI if it's a "csri:" URL
  30. * <small>(Client Side Resource Identifier, an URL form common to
  31. * {@link XMLEditor} and XXE server of an URI managed by
  32. * client-code)</small>; returns specified URI as is otherwise.
  33. * <p>"csri:" URL examples:
  34. * <pre>csri://(file)@no-authority/tmp/doc.xml
  35. *csri://(file)@myserver/share/docs/doc.xml
  36. *csri://(urn)@opaque-uri/ietf:rfc:2648
  37. *csri://(https)@www.xmlmind.com:80/xmleditor/index.html
  38. *csri://(ftp)foo:bar@xmlmind.com/xmleditor/index.html</pre>
  39. *
  40. * @param {string} url - an URL, possibly a "csri:" one.
  41. * @return {string} corresponding URI.
  42. */
  43. static csriURLToURI(url) {
  44. if (url === null || !url.startsWith("csri://(")) {
  45. return url;
  46. }
  47. let uri = url;
  48. for (let separ of CSRI_SEPARATORS) {
  49. let pos = uri.indexOf(separ, 8);
  50. if (pos > 8) {
  51. let scheme = uri.substring(8, pos).trim();
  52. if (scheme.length > 0) {
  53. let tail;
  54. if (CSRI_SEPARATOR_NO_AUTHORITY === separ) {
  55. tail = "///" + uri.substring(pos + separ.length);
  56. } else if (CSRI_SEPARATOR_OPAQUE_URI === separ) {
  57. tail = uri.substring(pos + separ.length);
  58. } else {
  59. tail = "//" + uri.substring(pos + separ.length);
  60. }
  61. uri = scheme + ":" + tail;
  62. }
  63. // Done.
  64. break;
  65. }
  66. }
  67. return uri;
  68. }
  69. // -----------------------------------------------------------------------
  70. // URI utilities
  71. // -----------------------------------------------------------------------
  72. /**
  73. * Returns the basename, if any, of specified URI.
  74. *
  75. * @param {string} uri - a relative or absolute, <em>valid</em> URI.
  76. * @return {string} the basename of specified URI <small>(decoded using
  77. * <code>decodeURIComponent</code>)</small> if any;
  78. * <code>null</code> otherwise.
  79. */
  80. static uriBasename(uri) {
  81. let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
  82. if (!path) {
  83. return null;
  84. }
  85. let basename = URIUtil.pathBasename(path, '/');
  86. return !basename? null : decodeURIComponent(basename);
  87. }
  88. /**
  89. * Changes the basename of specified URI.
  90. *
  91. * @param {string} uri - a relative or absolute, <em>valid</em> URI.
  92. * @param {string} basename - the new basename <small>(to be encoded using
  93. * <code>encodeURIComponent</code>)</small>.
  94. * @return {string} modified URI; <code>null</code> if specified
  95. * URI has no path.
  96. */
  97. static uriSetBasename(uri, basename) {
  98. let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
  99. if (!path) {
  100. return null;
  101. }
  102. basename = encodeURIComponent(basename);
  103. let parent = URIUtil.pathParent(path, '/', /*trailingSepar*/ true);
  104. if (!parent) {
  105. // Common case: uri consists in just a basename (e.g. "bin.dat").
  106. path = basename;
  107. } else {
  108. path = parent + basename;
  109. }
  110. return URIUtil.joinURI(scheme, authority, path, query, fragment);
  111. }
  112. /**
  113. * Splits specified URI into its components:
  114. * scheme, authority, path, query, fragment.
  115. * <p>Note that returned URI components are <em>NOT</em> decoded using
  116. * <code>decodeURIComponent</code>.
  117. *
  118. * @param {string} uri - a relative or absolute, <em>valid</em> URI.
  119. * @return {array} an array containing scheme, authority, path, query,
  120. * fragment, each of this component possibly being <code>null</code> or
  121. * just the empty string.
  122. */
  123. static splitURI(uri) {
  124. if (!uri) {
  125. return [ null, null, null, null, null ];
  126. }
  127. // Cannot simply use the URL API.
  128. // The URL API consider any protocol other than "http", "https", "ftp"
  129. // as being opaque.
  130. // For example, (new URL(uri)).pathname for "foo://bar.com/gee/wiz.dat"
  131. // would be "//bar.com/gee/wiz.dat".
  132. let scheme = null;
  133. let authority = null;
  134. let path = null;
  135. let query = null;
  136. let fragment = null;
  137. // Authority?
  138. let pos = uri.indexOf("://");
  139. if (pos < 0) {
  140. // No authority. Example: "file:/tmp/server.log".
  141. pos = uri.indexOf(":");
  142. if (pos < 0) {
  143. // No scheme. Example: "../foo/bar.txt".
  144. path = uri;
  145. } else {
  146. // Example: "urn:isbn:8088451".
  147. scheme = uri.substring(0, pos);
  148. path = uri.substring(pos+1);
  149. }
  150. } else {
  151. // Example: "ftp://john@acme.com/foo/bar.txt".
  152. scheme = uri.substring(0, uri.indexOf(":"));
  153. pos += 3; // Skip "://".
  154. let slash = uri.indexOf("/", pos);
  155. if (slash < 0) {
  156. authority = uri.substring(pos);
  157. // Example: "http://acme.com". No path, but "" and not null
  158. // for consistency with the other cases.
  159. path = "";
  160. } else {
  161. authority = uri.substring(pos, slash);
  162. // Path starting with "/".
  163. path = uri.substring(slash);
  164. }
  165. }
  166. // Fragment?
  167. if (path !== null) {
  168. pos = path.lastIndexOf("#");
  169. if (pos >= 0) {
  170. fragment = path.substring(pos+1);
  171. path = path.substring(0, pos);
  172. }
  173. }
  174. // Query?
  175. if (path !== null) {
  176. pos = path.lastIndexOf("?");
  177. if (pos >= 0) {
  178. query = path.substring(pos+1);
  179. path = path.substring(0, pos);
  180. }
  181. }
  182. return [ scheme, authority, path, query, fragment ];
  183. }
  184. /**
  185. * Returns the file extension, if any, of specified URI.
  186. *
  187. * @param {string} uri - a relative or absolute, <em>valid</em> URI.
  188. * @return {string} the file extension of specified URI if any;
  189. * <code>null</code> otherwise.
  190. */
  191. static uriExtension(uri) {
  192. let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
  193. if (!path) {
  194. return null;
  195. }
  196. let ext = URIUtil.pathExtension(path, '/');
  197. return !ext? null : decodeURIComponent(ext);
  198. }
  199. /**
  200. * Returns the parent, if any, of specified URI.
  201. *
  202. * @param {string} uri - a relative or absolute, <em>valid</em> URI.
  203. * @param {string} [trailingSepar=true] trailingSepar - if
  204. * <code>true</code>, returned URI has a path which ends with
  205. * a trailing <code>'/'</code> character.
  206. * @return {string} the parent of specified URI if any;
  207. * <code>null</code> otherwise.
  208. */
  209. static uriParent(uri, trailingSepar=true) {
  210. let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
  211. if (!path) {
  212. return null;
  213. }
  214. let parent = URIUtil.pathParent(path, '/', trailingSepar);
  215. if (!parent) {
  216. return null;
  217. }
  218. return URIUtil.joinURI(scheme, authority, parent);
  219. }
  220. /**
  221. * Join specified URI components and return resulting URI.
  222. * <p>Specified URI components are joined as is, that is,
  223. * without first encoding them using <code>encodeURIComponent</code>.
  224. */
  225. static joinURI(scheme, authority, path, query=null, fragment=null) {
  226. let uri = "";
  227. if (scheme) {
  228. uri += scheme;
  229. uri += ':';
  230. }
  231. if (authority) {
  232. uri += "//";
  233. uri += authority;
  234. }
  235. if (path) {
  236. uri += path;
  237. }
  238. if (query) {
  239. uri += '?';
  240. uri += query;
  241. }
  242. if (fragment) {
  243. uri += '#';
  244. uri += fragment;
  245. }
  246. return uri;
  247. }
  248. /**
  249. * Tests whether specified URI is an absolute hierarchical URI.
  250. *
  251. * @param {string} uri - a relative or absolute, <em>valid</em> URI.
  252. * @return {boolean} <code>true</code> if it's an absolute
  253. * hierarchical URI; <code>false</code> otherwise.
  254. */
  255. static isAbsoluteURI(uri) {
  256. let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
  257. return (scheme && path && path.startsWith('/'))? true : false;
  258. }
  259. /**
  260. * Returns specified URI after making it relative to the other URI
  261. * (when this is possible).
  262. *
  263. * @param {string} uri - an absolute, <em>valid</em> URI.
  264. * @param {string} baseURI - another absolute, <em>valid</em> URI
  265. * which is used as a base URI.
  266. * @return {string} the relative URI when possible;
  267. * specified uri as is otherwise (for example, when specified URIs
  268. * don't have the same scheme or authority).
  269. */
  270. static relativizeURI(uri, baseURI) {
  271. // uri and baseURI must be absolute URIs having the same scheme and
  272. // authority (case-insensitive).
  273. let [scheme1, authority1, path1, query1, fragment1] =
  274. URIUtil.splitURI(uri);
  275. if (scheme1) {
  276. scheme1 = scheme1.toLowerCase();
  277. }
  278. let auth1 = authority1;
  279. if (!auth1) {
  280. auth1 = null;
  281. } else {
  282. auth1 = auth1.toLowerCase();
  283. if (scheme1 === "file" && auth1 === "localhost") {
  284. auth1 = null;
  285. }
  286. }
  287. let [scheme2, authority2, path2, query2, fragment2] =
  288. URIUtil.splitURI(baseURI);
  289. if (scheme2) {
  290. scheme2 = scheme2.toLowerCase();
  291. }
  292. let auth2 = authority2;
  293. if (!auth2) {
  294. auth2 = null;
  295. } else {
  296. auth2 = auth2.toLowerCase();
  297. if (scheme2 === "file" && auth2 === "localhost") {
  298. auth2 = null;
  299. }
  300. }
  301. if (!scheme1 || !path1 || !path1.startsWith('/') ||
  302. !scheme2 || !path2 || !path2.startsWith('/') ||
  303. scheme1 !== scheme2 || auth1 !== auth2) {
  304. return uri;
  305. }
  306. // ---
  307. if (PLATFORM_IS_WINDOWS && scheme1 === "file") {
  308. // On Windows, "file:" URLs may have different volumes (e.g.
  309. // file:/C:/foo and file:/D:/bar). When this is the case, do not
  310. // attempt to compute a relative path.
  311. let drive1 = null;
  312. let match = path1.match(/^\/([a-zA-Z]:)\//);
  313. if (match !== null) {
  314. drive1 = match[1].toUpperCase(); // Not case-sensitive.
  315. // Normalize to upper-case.
  316. }
  317. let drive2 = null;
  318. match = path2.match(/^\/([a-zA-Z]:)\//);
  319. if (match !== null) {
  320. drive2 = match[1].toUpperCase();
  321. }
  322. if (drive1 !== drive2) {
  323. return uri;
  324. }
  325. }
  326. let relPath = URIUtil.relativizePath(path1, path2, '/');
  327. return URIUtil.joinURI(/*scheme*/ null, /*authority*/ null,
  328. relPath, query1, fragment1);
  329. }
  330. /**
  331. * Returns specified relative URI after resolving it against the other URI.
  332. *
  333. * @param {string} uri - a relative or absolute, <em>valid</em> URI.
  334. * @param {string} baseURI - an absolute, <em>valid</em> URI
  335. * which is used as a base URI.
  336. * @return {string} the resolved URI when possible;
  337. * specified uri as is otherwise (for example, when specified URI
  338. * is already absolute).
  339. */
  340. static resolveURI(uri, baseURI) {
  341. // baseURI must be an absolute URI.
  342. let [scheme2, authority2, path2, query2, fragment2] =
  343. URIUtil.splitURI(baseURI);
  344. if (!scheme2 || !path2 || !path2.startsWith('/')) {
  345. return uri;
  346. }
  347. let [scheme1, authority1, path1, query1, fragment1] =
  348. URIUtil.splitURI(uri);
  349. if (scheme1 && path1 && path1.startsWith('/')) {
  350. // Already absolute: nothing to do.
  351. return uri;
  352. }
  353. let absPath = !path1? "/" : URIUtil.resolvePath(path1, path2, '/');
  354. return URIUtil.joinURI(scheme2, authority2,
  355. absPath, query1, fragment1);
  356. }
  357. /**
  358. * Returns specified absolute URI after normalizing its authority
  359. * if it's a "file:" URI. Otherwise returns specified URI as is.
  360. */
  361. static normalizeFileURI(uri) {
  362. let uri2;
  363. if (uri && (uri2 = uri.toLowerCase()).startsWith("file:/")) {
  364. let matches = (new RegExp("^file://([^/]*)/")).exec(uri2);
  365. if (matches === null) {
  366. // file:/PATH.
  367. uri = "file:///" + uri.substring(6);
  368. } else {
  369. if (matches[1] === "localhost") {
  370. // file://localhost/PATH.
  371. uri = "file:///" + uri.substring(17);
  372. }
  373. // Otherwise, file:///PATH or file://AUTH/PATH. Leave it as is.
  374. }
  375. }
  376. return uri;
  377. }
  378. // -----------------------------------------------------------------------
  379. // URI to platform specific file path and the other way round
  380. // -----------------------------------------------------------------------
  381. /**
  382. * Returns the file path corresponding to specified absolute "file:" URI.
  383. *
  384. * @param {string} uri - an absolute, <em>valid</em>, "file:" URI.
  385. * @param {string} [pathSepar=FILE_PATH_SEPARATOR] pathSepar -
  386. * the character used to separate file path segments
  387. * (that is, '\' on Windows).
  388. * @return {string} corresponding absolute file path when possible,
  389. * <code>null</code> otherwise.
  390. */
  391. static uriToFilePath(uri, pathSepar=FILE_PATH_SEPARATOR) {
  392. let [scheme, authority, path, query, fragment] = URIUtil.splitURI(uri);
  393. if (!scheme || scheme !== "file") {
  394. return null;
  395. }
  396. if (!path) {
  397. path = "/";
  398. }
  399. // Ignore authority, query and fragment.
  400. let segments = path.split('/');
  401. for (let i = segments.length-1; i >= 0; --i) {
  402. segments[i] = decodeURIComponent(segments[i]);
  403. }
  404. let file = segments.join(pathSepar);
  405. if (pathSepar === WINDOWS_FILE_PATH_SEPARATOR) {
  406. if (authority && authority !== "localhost") {
  407. // Special processing of "file://server/path".
  408. file = "\\\\" + authority + file;
  409. } else {
  410. // "\C:" becomes "C:\".
  411. // "\C:\temp\foo" becomes "C:\temp\foo".
  412. if (file.match(/^\\[a-zA-Z]:/)) {
  413. if (file.length === 3) {
  414. file = file.substring(1) + "\\";
  415. } else if (file.charAt(3) === "\\") {
  416. file = file.substring(1);
  417. }
  418. }
  419. }
  420. }
  421. return URIUtil.normalizePath(file, pathSepar);
  422. }
  423. /**
  424. * Convert specified path to a "file:" URI.
  425. *
  426. * @param {string} path - relative or absolute path.
  427. * @param {string} [pathSepar=FILE_PATH_SEPARATOR] pathSepar -
  428. * the character used to separate file path segments
  429. * (that is, '\' on Windows).
  430. * @return {string} corresponding relative or absolute "file:" URI.
  431. * Note that if <code>path</code> is relative, this URI will have no
  432. * "file:" scheme (e.g. returns "tmp/my%20doc.xml" for "tmp\\my doc.xml");
  433. */
  434. static pathToFileURI(path, pathSepar=FILE_PATH_SEPARATOR) {
  435. let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
  436. let uri;
  437. if (URIUtil.testIsAbsolute(drive, checkedPath, pathSepar)) {
  438. uri = "file://";
  439. if (drive !== null) {
  440. if (drive.match(/^[a-zA-Z]:/)) {
  441. uri += "/" + drive;
  442. } else {
  443. // UNC path like "\\serv\share\doc.xml".
  444. // Discard leading "\\".
  445. uri += drive.substring(2);
  446. }
  447. // uri is something like "file:///C:" or "file://my-server".
  448. }
  449. } else {
  450. // Relative path. Resulting uri will have no "file:" scheme.
  451. uri = "";
  452. }
  453. let parts = checkedPath.split(pathSepar).map((part) => {
  454. if (part.length > 0) {
  455. part = encodeURIComponent(part);
  456. }
  457. return part;
  458. });
  459. uri += parts.join('/');
  460. return uri;
  461. }
  462. static checkPath(path, pathSepar) {
  463. let drive = null;
  464. if (pathSepar === WINDOWS_FILE_PATH_SEPARATOR) {
  465. // Example: \\ComputerName\SharedFolder\Resource
  466. let match = path.match(/^(\\\\[^\\]+)\\/);
  467. if (match !== null) {
  468. drive = match[1].toUpperCase(); // Not case-sensitive.
  469. // Normalize to upper-case.
  470. path = path.substring(drive.length);
  471. } else {
  472. // Example: C:\Folder\File
  473. // Drive relative paths like D:File not supported.
  474. let match = path.match(/^([a-zA-Z]:)\\/);
  475. if (match !== null) {
  476. drive = match[1].toUpperCase(); // Not case-sensitive.
  477. // Normalize to upper-case.
  478. path = path.substring(drive.length);
  479. }
  480. }
  481. }
  482. // Otherwise, we have an absolute or relative path.
  483. path = URIUtil.doNormalizePath(path, pathSepar);
  484. return [drive, path];
  485. }
  486. static doNormalizePath(path, pathSepar) {
  487. const originalPath = path;
  488. const prependSlash = path.startsWith(pathSepar);
  489. const appendSlash = path.endsWith(pathSepar);
  490. if (prependSlash || appendSlash) {
  491. let start = 0;
  492. let end = path.length-1;
  493. while (start <= end && path.charAt(start) === pathSepar) {
  494. ++start;
  495. }
  496. while (end >= start && path.charAt(end) === pathSepar) {
  497. --end;
  498. }
  499. if (start > end) {
  500. return pathSepar;
  501. }
  502. path = path.substring(start, end+1);
  503. // This one does not start or end with pathSepar.
  504. }
  505. // ---
  506. let twoPasses = false;
  507. let buffer = "";
  508. let parts = path.split(pathSepar);
  509. for (let part of parts) {
  510. if (part.length === 0 ||
  511. "." === part) { // e.g. "x//y", "x/./y"
  512. continue;
  513. }
  514. if (part.trim().length === 0) {
  515. // Malformed path. Give up.
  516. return originalPath;
  517. }
  518. if (buffer.length > 0) {
  519. buffer += pathSepar;
  520. }
  521. buffer += part;
  522. if (".." === part) {
  523. twoPasses = true;
  524. }
  525. }
  526. path = buffer;
  527. // ---
  528. if (twoPasses) {
  529. parts = path.split(pathSepar);
  530. buffer = "";
  531. for (let i = parts.length-1; i >= 0; --i) {
  532. let part = parts[i];
  533. if (".." === part) {
  534. // Count skipped levels: (i-j).
  535. let j = i;
  536. while (j >= 0 && ".." === parts[j]) {
  537. --j;
  538. }
  539. // Skip the ".." and the corresponding level: 2*(i-j).
  540. // (+1 because the for loop includes a --i.)
  541. i = i - 2*(i-j) + 1;
  542. if (i < 0) {
  543. // Malformed path. Give up.
  544. return originalPath;
  545. }
  546. } else {
  547. if (buffer.length > 0) {
  548. buffer = pathSepar + buffer;
  549. }
  550. buffer = part + buffer;
  551. }
  552. }
  553. }
  554. path = buffer;
  555. // ---
  556. if (prependSlash) {
  557. path = pathSepar + path;
  558. }
  559. if (appendSlash) {
  560. path = path + pathSepar;
  561. }
  562. return path;
  563. }
  564. static testIsAbsolute(drive, checkedPath, pathSepar) {
  565. if (pathSepar === WINDOWS_FILE_PATH_SEPARATOR && drive === null) {
  566. return false;
  567. }
  568. return checkedPath.startsWith(pathSepar);
  569. }
  570. // -----------------------------------------------------------------------
  571. // Low-level path utilities.
  572. // (Work for platform specific file and URI paths.
  573. // Work for relative and absolute paths.)
  574. // -----------------------------------------------------------------------
  575. /**
  576. * Tests whether specified path is absolute or relative.
  577. * <p>On Windows, a drive relative path like "\foo\bar" is considered
  578. * to be relative. Only "C:\foo\bar" or "\\my-server\foo\bar" are
  579. * considered to be absolute.
  580. *
  581. * @param {string} path - relative or absolute path.
  582. * @param {string} pathSepar - the character used to separate
  583. * path segments (that is, '/' for URIs).
  584. * @return {boolean} <code>true</code> if absolute;
  585. * <code>false</code> if relative.
  586. */
  587. static isAbsolutePath(path, pathSepar) {
  588. let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
  589. return URIUtil.testIsAbsolute(drive, checkedPath, pathSepar);
  590. }
  591. /**
  592. * Resolves specified path against specified base.
  593. * <p><strong>IMPORTANT:</strong> a directory path is expected to end
  594. * with <code><i>pathSepar</i></code>.
  595. *
  596. * @param {string} path - relative or absolute path.
  597. * @param {string} basePath - relative or absolute path.
  598. * @param {string} pathSepar - the character used to separate
  599. * path segments (that is, '/' for URIs).
  600. * @return {string} resolved path.
  601. */
  602. static resolvePath(path, basePath, pathSepar) {
  603. if (URIUtil.isAbsolutePath(path, pathSepar) ||
  604. !basePath) {
  605. return path;
  606. }
  607. let parentPath;
  608. if (basePath.endsWith(pathSepar)) {
  609. // Assume basePath points to a directory.
  610. parentPath = basePath;
  611. } else {
  612. parentPath = URIUtil.pathParent(basePath, pathSepar,
  613. /*trailingSepar*/ true);
  614. }
  615. return URIUtil.normalizePath(!parentPath? path : parentPath + path,
  616. pathSepar);
  617. }
  618. /**
  619. * Returns specified relative or absolute path after normalizing it,
  620. * that is, after removing ".", ".." and duplicate path separators from it.
  621. *
  622. * @param {string} path - relative or absolute path.
  623. * @param {string} pathSepar - the character used to separate
  624. * path segments (that is, '/' for URIs).
  625. * @return {string} normalized path or original path if it is not possible
  626. * to normalize it (examples: "../../foo/bar", "a/ /b/c").
  627. */
  628. static normalizePath(path, pathSepar) {
  629. let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
  630. if (drive !== null) {
  631. checkedPath = drive + checkedPath;
  632. }
  633. return checkedPath;
  634. }
  635. /**
  636. * Returns the extension of specified path. To make it simple, the
  637. * substring after last '.', not including last '.'.
  638. * <ul>
  639. * <li>Returns <code>null</code> for "<tt>/tmp/test</tt>".
  640. * <li>Returns the empty string for "<tt>/tmp/test.</tt>".
  641. * <li>Returns <code>null</code> for "<tt>~/.profile</tt>".
  642. * <li>Returns "<tt>gz</tt>" for "<tt>/tmp/test.tar.gz</tt>".
  643. * </ul>
  644. *
  645. * @param {string} path - relative or absolute path possibly
  646. * having an extension
  647. * @param {string} pathSepar - the character used to separate
  648. * path segments (that is, '/' for URIs)
  649. * @return {string} extension if any; <code>null</code> otherwise
  650. */
  651. static pathExtension(path, pathSepar) {
  652. let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
  653. let dot = URIUtil.indexOfDot(checkedPath, pathSepar);
  654. if (dot < 0) {
  655. return null;
  656. } else {
  657. return checkedPath.substring(dot+1);
  658. }
  659. }
  660. static indexOfDot(checkedPath, pathSepar) {
  661. let dot = checkedPath.lastIndexOf('.');
  662. if (dot >= 0) {
  663. let baseNameStart = checkedPath.lastIndexOf(pathSepar);
  664. if (baseNameStart < 0) {
  665. baseNameStart = 0;
  666. } else {
  667. ++baseNameStart;
  668. }
  669. if (dot <= baseNameStart) {
  670. dot = -1;
  671. }
  672. }
  673. return dot;
  674. }
  675. /**
  676. * Returns the base name of specified path. To make it simple, the
  677. * substring after last '/'.
  678. * <p>Examples:
  679. * <ul>
  680. * <li>Returns the empty string for "<tt>/</tt>".
  681. * <li>Returns "<tt>foo</tt>" for "<tt>foo</tt>".
  682. * <li>Returns "<tt>bar</tt>" for "<tt>/foo/bar</tt>".
  683. * <li>Returns "<tt>bar</tt>" for "<tt>/foo/bar/</tt>".
  684. * </ul>
  685. *
  686. * @param {string} path - relative or absolute path
  687. * @param {string} pathSepar - the character used to separate
  688. * path segments (that is, '/' for URIs)
  689. * @return {string} base name of specified path
  690. */
  691. static pathBasename(path, pathSepar) {
  692. let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
  693. if (checkedPath === pathSepar) {
  694. return "";
  695. }
  696. // Normalize "/foo/bar/" to "/foo/bar".
  697. if (checkedPath.endsWith(pathSepar)) {
  698. checkedPath = checkedPath.substring(0, checkedPath.length-1);
  699. }
  700. let slash = checkedPath.lastIndexOf(pathSepar);
  701. if (slash < 0) {
  702. return checkedPath;
  703. }
  704. return checkedPath.substring(slash+1);
  705. }
  706. /**
  707. * Returns the parent of specified path. To make it simple,
  708. * the substring before last '/'.
  709. * <p>Examples:
  710. * <ul>
  711. * <li>Returns <code>null</code> for "<tt>/</tt>".
  712. * <li>Returns <code>null</code> for "<tt>foo</tt>".
  713. * <li>Returns "<tt>/</tt>" for "<tt>/foo</tt>".
  714. * <li>Returns "<tt>/foo</tt>" (or "<tt>/foo/</tt>") for
  715. * "<tt>/foo/bar</tt>".
  716. * <li>Returns "<tt>/foo</tt>" (or "<tt>/foo/</tt>") for
  717. * "<tt>/foo/bar/</tt>".
  718. * </ul>
  719. *
  720. * @param {string} path - relative or absolute path
  721. * @param {string} pathSepar - the character used to separate
  722. * path segments (that is, '/' for URIs)
  723. * @param {string} trailingSepar - if <code>true</code>,
  724. * returned path ends with a trailing <code>pathSepar</code> character
  725. * @return {string} parent of specified path or
  726. * <code>null</code> for <code>pathSepar</code> or if specified path
  727. * does not contain the <code>pathSepar</code> character
  728. */
  729. static pathParent(path, pathSepar, trailingSepar) {
  730. let [drive, checkedPath] = URIUtil.checkPath(path, pathSepar);
  731. if (checkedPath === pathSepar) {
  732. return null;
  733. }
  734. // Normalize "/foo/bar/" to "/foo/bar".
  735. if (checkedPath.endsWith(pathSepar)) {
  736. checkedPath = checkedPath.substring(0, checkedPath.length-1);
  737. }
  738. let slash = checkedPath.lastIndexOf(pathSepar);
  739. if (slash < 0) {
  740. return null;
  741. }
  742. let parent;
  743. if (slash === 0) {
  744. // Example: "/foo"
  745. parent = pathSepar;
  746. } else {
  747. parent = checkedPath.substring(0, trailingSepar? slash+1 : slash);
  748. }
  749. if (drive !== null) {
  750. parent = drive + parent;
  751. }
  752. return parent;
  753. }
  754. /**
  755. * Returns specified path after making it relative to the other path
  756. * (when this is possible).
  757. * <p><strong>IMPORTANT:</strong> a directory path is expected to end
  758. * with <code><i>pathSepar</i></code>.
  759. *
  760. * @param {string} path - an absolute path.
  761. * @param {string} basePath - another absolute path which is used as a base.
  762. * @param {string} pathSepar - the character used to separate
  763. * path segments (that is, '/' for URIs)
  764. * @return {string} the relative path when possible;
  765. * specified path as is otherwise (Windows example: when specified paths
  766. * don't have the same drive.
  767. */
  768. static relativizePath(path, basePath, pathSepar) {
  769. // path and basePath must be absolute paths having the same drive (if
  770. // any).
  771. let [drive1, checkedPath1] = URIUtil.checkPath(path, pathSepar);
  772. let [drive2, checkedPath2] = URIUtil.checkPath(basePath, pathSepar);
  773. if (!URIUtil.testIsAbsolute(drive1, checkedPath1, pathSepar) ||
  774. !URIUtil.testIsAbsolute(drive2, checkedPath2, pathSepar) ||
  775. drive1 !== drive2) {
  776. return path;
  777. }
  778. path = checkedPath1;
  779. basePath = checkedPath2;
  780. if (pathSepar === path) {
  781. return pathSepar;
  782. }
  783. if (pathSepar === basePath) {
  784. return path.substring(1);
  785. }
  786. if (!basePath.endsWith(pathSepar)) {
  787. basePath = URIUtil.pathParent(basePath, pathSepar,
  788. /*trailingSepar*/ true);
  789. }
  790. // Windows filenames are case insensitive.
  791. const onWindows = (pathSepar === WINDOWS_FILE_PATH_SEPARATOR);
  792. const upSegment = ".." + pathSepar;
  793. let relPath = "";
  794. while (basePath !== null) {
  795. let start = basePath;
  796. if (path.startsWith(start) ||
  797. (onWindows &&
  798. path.toLowerCase().startsWith(start.toLowerCase()))) {
  799. relPath += path.substring(start.length);
  800. break;
  801. }
  802. relPath += upSegment;
  803. basePath = URIUtil.pathParent(basePath, pathSepar,
  804. /*trailingSepar*/ true);
  805. }
  806. return relPath;
  807. }
  808. // -----------------------------------------------------------------------
  809. static formatFileSize(byteCount, fallback=null) {
  810. if (!Number.isInteger(byteCount) || byteCount < 0) {
  811. return fallback;
  812. }
  813. let unit = 0;
  814. while (byteCount >= 1024) {
  815. byteCount /= 1024;
  816. unit++;
  817. }
  818. if (unit > 0) {
  819. byteCount = byteCount.toFixed(1);
  820. }
  821. return `${byteCount} ${" KMGTPEZY"[unit]}B`;
  822. }
  823. static formatFileDate(millis, fallback=null) {
  824. if (!Number.isInteger(millis) || millis < 0) {
  825. return fallback;
  826. }
  827. const date = new Date(millis);
  828. let text = date.getFullYear();
  829. text += "-";
  830. text += URIUtil.formatFileDateField(1 + date.getMonth());
  831. text += "-";
  832. text += URIUtil.formatFileDateField(date.getDate());
  833. text += " ";
  834. text += URIUtil.formatFileDateField(date.getHours());
  835. text += ":";
  836. text += URIUtil.formatFileDateField(date.getMinutes());
  837. return text;
  838. }
  839. static formatFileDateField(value) {
  840. let text = String(value);
  841. while (text.length < 2) {
  842. text = "0" + text;
  843. }
  844. return text;
  845. }
  846. // -----------------------------------------------------------------------
  847. /*TEST|
  848. static test_URIUtil(logger, filePathSepar) {
  849. const uriList = [
  850. "",
  851. "foo/bar.dat",
  852. "/foo/bar.dat",
  853. "file:/foo/bar.xml",
  854. "file:///foo/bar.xml",
  855. "file:/Z:/foo/bar.xml#end",
  856. "file:/C:",
  857. "file:///C:/foo/././gee//wiz//bar.xml",
  858. "file://GOOD/foo/gee/wiz/../../bar.xml",
  859. "file://BAD/foo/gee/wiz/../../../../bar.xml",
  860. "file://BAD/foo/%20%20%20%20/bar.xml",
  861. "http://www.uiuc.edu:80",
  862. "http://www.uiuc.edu:80/",
  863. "http://www.uiuc.edu:80/SDG/",
  864. "http://www.uiuc.edu:80/SDG/Software/Mosaic/Demo/url-primer",
  865. "http://www.uiuc.edu:80/SDG/Software/Mosaic/Demo/.url-primer",
  866. "http://www.uiuc.edu:80/SDG/Software/Mosaic/Demo/url-primer.html",
  867. "http://www.uiuc.edu/SDG/Software/Mosaic/Demo/url-primer.html.gz",
  868. "http://127.0.0.1/SDG/Software/Mosaic/Demo/url-primer.html#chapter1",
  869. "foo://example.com:8042/over/there?name=ferret#nose",
  870. "urn:example:animal:ferret:nose",
  871. "urn:example:animal:ferret:nose?name=ferret#foo",
  872. "ftp://hs%40x:my%20pass@www.foo.com:1024/bar/\
  873. vin%20ros%C3%A9.jar#ros%C3%A9",
  874. "csri://(file)@no-authority/",
  875. "csri://(file)@no-authority/home/hussein/",
  876. "csri://(file)@no-authority/home//hussein/./tmp/..////",
  877. "csri://(file)@no-authority/../.././doc.xml",
  878. "csri://(file)@no-authority/z:/docs/doc.xml",
  879. "csri://(file)@myserver/share/docs/doc.xml",
  880. "csri://(file)@192.168.1.205/tmp/dump.dat",
  881. "csri://(file)@[1080:0:0:0:8:800:200C:417A]:80/tmp/dump.dat",
  882. "csri://(xdocs)@MyServer/Content/sample/dita/tasks/\
  883. Very%20Important.dita",
  884. "csri://(https)@www.xmlmind.com/xmleditor/index.html#notice",
  885. "csri://(http)@www.xmlmind.com:80/xslsrv/exec?op=jobs&auth=toto",
  886. "csri://(ftp)john:doe@localhost/src/test/makefile",
  887. "csri://(urn)@opaque-uri/ietf:rfc:2648",
  888. "csri://(urn)@opaque-uri/lex:eu:council:directive:\
  889. 2010-03-09;2010-19-UE#summary",
  890. ];
  891. for (let uri of uriList) {
  892. let [scheme, authority, path, query, fragment] =
  893. URIUtil.splitURI(uri);
  894. let abs = URIUtil.isAbsoluteURI(uri);
  895. let name = URIUtil.uriBasename(uri);
  896. let ext = URIUtil.uriExtension(uri);
  897. let parent = URIUtil.uriParent(uri);
  898. // File path tests ---
  899. let uri2 = URIUtil.csriURLToURI(uri);
  900. let file = URIUtil.uriToFilePath(uri2, filePathSepar);
  901. let uri3 = null;
  902. if (file !== null) {
  903. uri3 = URIUtil.pathToFileURI(file, filePathSepar);
  904. }
  905. logger(`uri='${uri}':\n\
  906. splitURI=[scheme=${scheme}, authority=${authority}, path=${path}, \
  907. query=${query}, fragment=${fragment}],\n\
  908. abs=${abs}, basename='${name}', extension='${ext}', parent='${parent}'\n\
  909. csriURLToURI(uri)=uri2=${uri2}\n\
  910. uriToFilePath(uri2,${filePathSepar})=${file}\n\
  911. pathToFileURI(file,${filePathSepar})=${uri3}
  912. ---`);
  913. }
  914. }
  915. |TEST*/
  916. // -----------------------------------------------------------------------
  917. /*TEST|
  918. static test_relativize(logger) {
  919. const paths1 = [
  920. "", "",
  921. "/", "/",
  922. "/", "/foo",
  923. "/foo", "/",
  924. "/foo", "/bar",
  925. "foo", "/bar",
  926. "/foo", "bar",
  927. "/home/hussein", "/home",
  928. "/home/hussein", "/home/",
  929. "/foo/gee", "/foo/gee/wiz.xml",
  930. "/foo/zip/bar.xml", "/foo/gee/wiz.xml",
  931. "/foo/zip/bar.xml", "/foo/wiz.xml",
  932. "/foo/bar.xml", "/foo/gee/wiz.xml",
  933. "/foo/zip/bar.xml", "/foo/zip/bar.xml",
  934. "/usr/local/bin/html2ps", "/usr/bin/grep",
  935. "/foo/zip/bar/", "/foo/wiz",
  936. "/foo/bar", "/foo/gee/wiz/",
  937. "/foo/zip/bar/", "/foo/wiz/",
  938. "/foo/bar/", "/foo/gee/wiz/",
  939. "/foo/bar/", "/foo/bar/",
  940. "/foo/bar/", "/foo/bar/gee",
  941. "/foo/bar/gee", "/foo/bar/",
  942. "/foo/bar/", "/foo/bar/wiz/",
  943. "/foo/bar/wiz/", "/foo/bar/",
  944. ];
  945. URIUtil.testRelativizePath(logger, paths1, '/');
  946. logger("---");
  947. const paths2 = [
  948. "Z:", "Z:",
  949. "C:\\", "C:\\",
  950. "C:\\home\\hussein", "c:\\home",
  951. "C:\\home\\hussein", "c:\\home\\",
  952. "C:\\", "C:\\home\\hussein",
  953. "C:\\home\\hussein", "C:\\",
  954. "C:\\home", "c:\\home\\hussein",
  955. "c:\\home\\hussein", "C:\\HOME",
  956. "C:\\HOME\\HUSSEIN", "c:\\home",
  957. "C:\\home\\hussein\\DOT emacs", "C:\\home\\hussein\\bin\\clean.bat",
  958. "C:\\HOME\\HUSSEIN\\BIN\\CLEAN.BAT", "c:\\home\\hussein\\DOT emacs",
  959. "C:\\home\\hussein\\DOT emacs", "C:\\home\\hussein\\bin\\",
  960. "C:\\home\\hussein\\bin\\", "C:\\home\\hussein\\DOT emacs",
  961. "C:\\home\\hussein\\bin", "Z:\\docsrc",
  962. "Z:\\bin\\clean", "C:\\home\\hussein\\bin\\clean.bat",
  963. "\\\\Vboxsrv\\home_hussein\\bin\\clean",
  964. "\\\\Vboxsrv\\home_hussein\\.profile",
  965. "\\\\Vboxsrv\\home_hussein\\.profile",
  966. "\\\\VBOXSRV\\HOME_HUSSEIN\\BIN\\CLEAN",
  967. ];
  968. URIUtil.testRelativizePath(logger, paths2, '\\');
  969. logger("---");
  970. const uris = [
  971. "file:/Users/john/docs/doc.xml",
  972. "file:/Users/john/",
  973. "file:/Users/john/docs/doc.xml",
  974. "file:///Users/john/",
  975. "file:/Users/john/docs/doc.xml",
  976. "file://localhost/Users/john/",
  977. "file:/C:/Users/john/docs/doc.xml",
  978. "file:/Z:/docs/",
  979. "http://www.acme.com/docs/toc.html",
  980. "https://www.acme.com/docs/",
  981. "http://www.acme.biz/docs/toc.html",
  982. "http://www.acme.com/docs/",
  983. "http://www.acme.com/docs/toc.html",
  984. "http://www.acme.com/docs/",
  985. "http://www.acme.com/",
  986. "http://www.acme.com/docs/toc.html",
  987. "http://www.acme.com/docs/toc.html",
  988. "http://www.acme.com/",
  989. "http://www.acme.com/docs/toc.html",
  990. "http://www.acme.com/docs/toc.html",
  991. "http://www.acme.com/index.html",
  992. "http://www.acme.com/docs/toc.html",
  993. "http://www.acme.com/docs/index.html#index",
  994. "http://www.acme.com/docs/toc.html",
  995. "http://www.acme.com/docs/images/logo?format=SVG",
  996. "http://www.acme.com/docs/toc.html",
  997. "http://www.acme.com/index.html",
  998. "http://www.acme.com/docs/chapters/",
  999. "http://www.acme.com/docs/chapters/chapter1.html",
  1000. "http://www.acme.com/docs/toc.html",
  1001. ];
  1002. for (let i = 0; i < uris.length; i += 2) {
  1003. let uri = uris[i];
  1004. let baseURI = uris[i+1];
  1005. let relURI = URIUtil.relativizeURI(uri, baseURI);
  1006. let absURI = URIUtil.resolveURI(relURI, baseURI);
  1007. logger(`"${uri}" relative to "${baseURI}" = "${relURI}"\n\
  1008. relative uri resolved = "${absURI}"`);
  1009. }
  1010. }
  1011. static testRelativizePath(logger, paths, pathSepar) {
  1012. for (let i = 0; i < paths.length; i += 2) {
  1013. let path = paths[i];
  1014. let basePath = paths[i+1];
  1015. let relPath = URIUtil.relativizePath(path, basePath, pathSepar);
  1016. let absPath = URIUtil.resolvePath(relPath, basePath, pathSepar);
  1017. logger(`"${path}" relative to "${basePath}" = "${relPath}"\n\
  1018. relative path resolved = "${absPath}"`);
  1019. }
  1020. }
  1021. |TEST*/
  1022. }