// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

/** Options for {@linkcode exists} and {@linkcode existsSync.} */
export interface ExistsOptions {
  /**
   * When `true`, will check if the path is readable by the user as well.
   *
   * @default {false}
   */
  isReadable?: boolean;
  /**
   * When `true`, will check if the path is a directory as well. Directory
   * symlinks are included.
   *
   * @default {false}
   */
  isDirectory?: boolean;
  /**
   * When `true`, will check if the path is a file as well. File symlinks are
   * included.
   *
   * @default {false}
   */
  isFile?: boolean;
}

/**
 * Asynchronously test whether or not the given path exists by checking with
 * the file system.
 *
 * Note: Do not use this function if performing a check before another operation
 * on that file. Doing so creates a race condition. Instead, perform the actual
 * file operation directly. This function is not recommended for this use case.
 * See the recommended method below.
 *
 * @see {@link https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use} for
 * more information on the time-of-check to time-of-use bug.
 *
 * Requires `--allow-read` and `--allow-sys` permissions.
 *
 * @see {@link https://docs.deno.com/runtime/manual/basics/permissions#file-system-access}
 * for more information on Deno's permissions system.
 *
 * @param path The path to the file or directory, as a string or URL.
 * @param options Additional options for the check.
 *
 * @returns A promise that resolves with `true` if the path exists, `false`
 * otherwise.
 *
 * @example Recommended method
 * ```ts no-eval
 * // Notice no use of exists
 * try {
 *   await Deno.remove("./foo", { recursive: true });
 * } catch (error) {
 *   if (!(error instanceof Deno.errors.NotFound)) {
 *     throw error;
 *   }
 *   // Do nothing...
 * }
 * ```
 *
 * Notice that `exists()` is not used in the above example. Doing so avoids a
 * possible race condition. See the above note for details.
 *
 * @example Basic usage
 * ```ts no-eval
 * import { exists } from "@std/fs/exists";
 *
 * await exists("./exists"); // true
 * await exists("./does_not_exist"); // false
 * ```
 *
 * @example Check if a path is readable
 * ```ts no-eval
 * import { exists } from "@std/fs/exists";
 *
 * await exists("./readable", { isReadable: true }); // true
 * await exists("./not_readable", { isReadable: true }); // false
 * ```
 *
 * @example Check if a path is a directory
 * ```ts no-eval
 * import { exists } from "@std/fs/exists";
 *
 * await exists("./directory", { isDirectory: true }); // true
 * await exists("./file", { isDirectory: true }); // false
 * ```
 *
 * @example Check if a path is a file
 * ```ts no-eval
 * import { exists } from "@std/fs/exists";
 *
 * await exists("./file", { isFile: true }); // true
 * await exists("./directory", { isFile: true }); // false
 * ```
 *
 * @example Check if a path is a readable directory
 * ```ts no-eval
 * import { exists } from "@std/fs/exists";
 *
 * await exists("./readable_directory", { isReadable: true, isDirectory: true }); // true
 * await exists("./not_readable_directory", { isReadable: true, isDirectory: true }); // false
 * ```
 *
 * @example Check if a path is a readable file
 * ```ts no-eval
 * import { exists } from "@std/fs/exists";
 *
 * await exists("./readable_file", { isReadable: true, isFile: true }); // true
 * await exists("./not_readable_file", { isReadable: true, isFile: true }); // false
 * ```
 */
export async function exists(
  path: string | URL,
  options?: ExistsOptions,
): Promise<boolean> {
  try {
    const stat = await Deno.stat(path);
    if (
      options &&
      (options.isReadable || options.isDirectory || options.isFile)
    ) {
      if (options.isDirectory && options.isFile) {
        throw new TypeError(
          "ExistsOptions.options.isDirectory and ExistsOptions.options.isFile must not be true together",
        );
      }
      if (
        (options.isDirectory && !stat.isDirectory) ||
        (options.isFile && !stat.isFile)
      ) {
        return false;
      }
      if (options.isReadable) {
        return fileIsReadable(stat);
      }
    }
    return true;
  } catch (error) {
    if (error instanceof Deno.errors.NotFound) {
      return false;
    }
    if (error instanceof Deno.errors.PermissionDenied) {
      if (
        (await Deno.permissions.query({ name: "read", path })).state ===
          "granted"
      ) {
        // --allow-read not missing
        return !options?.isReadable; // PermissionDenied was raised by file system, so the item exists, but can't be read
      }
    }
    throw error;
  }
}

/**
 * Synchronously test whether or not the given path exists by checking with
 * the file system.
 *
 * Note: Do not use this function if performing a check before another operation
 * on that file. Doing so creates a race condition. Instead, perform the actual
 * file operation directly. This function is not recommended for this use case.
 * See the recommended method below.
 *
 * @see {@link https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use} for
 * more information on the time-of-check to time-of-use bug.
 *
 * Requires `--allow-read` and `--allow-sys` permissions.
 *
 * @see {@link https://docs.deno.com/runtime/manual/basics/permissions#file-system-access}
 * for more information on Deno's permissions system.
 *
 * @param path The path to the file or directory, as a string or URL.
 * @param options Additional options for the check.
 *
 * @returns `true` if the path exists, `false` otherwise.
 *
 * @example Recommended method
 * ```ts no-eval
 * // Notice no use of exists
 * try {
 *   Deno.removeSync("./foo", { recursive: true });
 * } catch (error) {
 *   if (!(error instanceof Deno.errors.NotFound)) {
 *     throw error;
 *   }
 *   // Do nothing...
 * }
 * ```
 *
 * Notice that `existsSync()` is not used in the above example. Doing so avoids
 * a possible race condition. See the above note for details.
 *
 * @example Basic usage
 * ```ts no-eval
 * import { existsSync } from "@std/fs/exists";
 *
 * existsSync("./exists"); // true
 * existsSync("./does_not_exist"); // false
 * ```
 *
 * @example Check if a path is readable
 * ```ts no-eval
 * import { existsSync } from "@std/fs/exists";
 *
 * existsSync("./readable", { isReadable: true }); // true
 * existsSync("./not_readable", { isReadable: true }); // false
 * ```
 *
 * @example Check if a path is a directory
 * ```ts no-eval
 * import { existsSync } from "@std/fs/exists";
 *
 * existsSync("./directory", { isDirectory: true }); // true
 * existsSync("./file", { isDirectory: true }); // false
 * ```
 *
 * @example Check if a path is a file
 * ```ts no-eval
 * import { existsSync } from "@std/fs/exists";
 *
 * existsSync("./file", { isFile: true }); // true
 * existsSync("./directory", { isFile: true }); // false
 * ```
 *
 * @example Check if a path is a readable directory
 * ```ts no-eval
 * import { existsSync } from "@std/fs/exists";
 *
 * existsSync("./readable_directory", { isReadable: true, isDirectory: true }); // true
 * existsSync("./not_readable_directory", { isReadable: true, isDirectory: true }); // false
 * ```
 *
 * @example Check if a path is a readable file
 * ```ts no-eval
 * import { existsSync } from "@std/fs/exists";
 *
 * existsSync("./readable_file", { isReadable: true, isFile: true }); // true
 * existsSync("./not_readable_file", { isReadable: true, isFile: true }); // false
 * ```
 */
export function existsSync(
  path: string | URL,
  options?: ExistsOptions,
): boolean {
  try {
    const stat = Deno.statSync(path);
    if (
      options &&
      (options.isReadable || options.isDirectory || options.isFile)
    ) {
      if (options.isDirectory && options.isFile) {
        throw new TypeError(
          "ExistsOptions.options.isDirectory and ExistsOptions.options.isFile must not be true together",
        );
      }
      if (
        (options.isDirectory && !stat.isDirectory) ||
        (options.isFile && !stat.isFile)
      ) {
        return false;
      }
      if (options.isReadable) {
        return fileIsReadable(stat);
      }
    }
    return true;
  } catch (error) {
    if (error instanceof Deno.errors.NotFound) {
      return false;
    }
    if (error instanceof Deno.errors.PermissionDenied) {
      if (
        Deno.permissions.querySync({ name: "read", path }).state === "granted"
      ) {
        // --allow-read not missing
        return !options?.isReadable; // PermissionDenied was raised by file system, so the item exists, but can't be read
      }
    }
    throw error;
  }
}

function fileIsReadable(stat: Deno.FileInfo) {
  if (stat.mode === null) {
    return true; // Exclusive on Non-POSIX systems
  } else if (Deno.uid() === stat.uid) {
    return (stat.mode & 0o400) === 0o400; // User is owner and can read?
  } else if (Deno.gid() === stat.gid) {
    return (stat.mode & 0o040) === 0o040; // User group is owner and can read?
  }
  return (stat.mode & 0o004) === 0o004; // Others can read?
}

// denoCacheMetadata={"headers":{"access-control-expose-headers":"*","content-security-policy":"default-src 'none'; script-src 'none'; style-src 'none'; img-src 'none'; font-src 'none'; connect-src 'none'; frame-src 'none'; object-src 'none'; frame-ancestors 'none'; sandbox; form-action 'none';","nel":"{\"report_to\":\"cf-nel\",\"success_fraction\":0.0,\"max_age\":604800}","etag":"\"5ca5c26abf92241c2d7b06273403f749\"","age":"2132943","alt-svc":"h3=\":443\"; ma=86400","date":"Tue, 28 Jul 2026 12:31:15 GMT","x-jsr-backend":"modules","cf-cache-status":"HIT","accept-ranges":"bytes","set-cookie":"__cf_bm=dBbIbeDnF67_g2IJe8tAo5kGbG7lUPbTWoEP6GafJWQ-1785241875.3333771-1.0.1.1-lhBbiW2hwIPCCnGzLusZtEasGcffotGr67uTKW_HEd3AjPe21ZgeiecTRsTdLm_5NGigdqK1z53ArqLpFUYrp_tf1tx0m5nRAKzu.MhMY.ltETfOyt2Kpq3MRHOK96mr; HttpOnly; SameSite=None; Secure; Path=/; Domain=jsr.io; Expires=Tue, 28 Jul 2026 13:01:15 GMT","report-to":"{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=dmOI89joqh44xrAJ1kFrPU1eER6cv4dPKiCOM5G3Trnigy52i8gkEiw72LKqXQkh%2BpRXBKe%2F7xCYt5pryxWR%2FXpop%2FiawKzJ8rvrLLDe1uapQTgrAwz%2FmIo%3D\"}]}","server":"cloudflare","content-length":"9044","access-control-allow-origin":"*","cross-origin-resource-policy":"cross-origin","x-robots-tag":"noindex","x-content-type-options":"nosniff","cf-ray":"a223e658cd897db6-LAX","vary":"Origin","cache-control":"public, max-age=31536000, immutable","content-type":"text/typescript"},"url":"https://jsr.io/@std/fs/1.0.3/exists.ts","time":1785241875}