diff --git a/ql/src/codeql_ruby/files/FileSystem.qll b/ql/src/codeql_ruby/files/FileSystem.qll
new file mode 100644
index 00000000000..8223fd67ec7
--- /dev/null
+++ b/ql/src/codeql_ruby/files/FileSystem.qll
@@ -0,0 +1,168 @@
+/** Provides classes for working with files and folders. */
+
+/** A file or folder. */
+abstract class Container extends @container {
+ /** Gets a file or sub-folder in this container. */
+ Container getAChildContainer() { this = result.getParentContainer() }
+
+ /** Gets a file in this container. */
+ File getAFile() { result = getAChildContainer() }
+
+ /** Gets a sub-folder in this container. */
+ Folder getAFolder() { result = getAChildContainer() }
+
+ /**
+ * Gets the absolute, canonical path of this container, using forward slashes
+ * as path separator.
+ *
+ * The path starts with a _root prefix_ followed by zero or more _path
+ * segments_ separated by forward slashes.
+ *
+ * The root prefix is of one of the following forms:
+ *
+ * 1. A single forward slash `/` (Unix-style)
+ * 2. An upper-case drive letter followed by a colon and a forward slash,
+ * such as `C:/` (Windows-style)
+ * 3. Two forward slashes, a computer name, and then another forward slash,
+ * such as `//FileServer/` (UNC-style)
+ *
+ * Path segments are never empty (that is, absolute paths never contain two
+ * contiguous slashes, except as part of a UNC-style root prefix). Also, path
+ * segments never contain forward slashes, and no path segment is of the
+ * form `.` (one dot) or `..` (two dots).
+ *
+ * Note that an absolute path never ends with a forward slash, except if it is
+ * a bare root prefix, that is, the path has no path segments. A container
+ * whose absolute path has no segments is always a `Folder`, not a `File`.
+ */
+ abstract string getAbsolutePath();
+
+ /**
+ * Gets the base name of this container including extension, that is, the last
+ * segment of its absolute path, or the empty string if it has no segments.
+ *
+ * Here are some examples of absolute paths and the corresponding base names
+ * (surrounded with quotes to avoid ambiguity):
+ *
+ *
+ * | Absolute path | Base name |
+ * | "/tmp/tst.go" | "tst.go" |
+ * | "C:/Program Files (x86)" | "Program Files (x86)" |
+ * | "/" | "" |
+ * | "C:/" | "" |
+ * | "D:/" | "" |
+ * | "//FileServer/" | "" |
+ *
+ */
+ string getBaseName() {
+ result = getAbsolutePath().regexpCapture(".*/(([^/]*?)(?:\\.([^.]*))?)", 1)
+ }
+
+ /**
+ * Gets the extension of this container, that is, the suffix of its base name
+ * after the last dot character, if any.
+ *
+ * In particular,
+ *
+ * - if the name does not include a dot, there is no extension, so this
+ * predicate has no result;
+ * - if the name ends in a dot, the extension is the empty string;
+ * - if the name contains multiple dots, the extension follows the last dot.
+ *
+ * Here are some examples of absolute paths and the corresponding extensions
+ * (surrounded with quotes to avoid ambiguity):
+ *
+ *
+ * | Absolute path | Extension |
+ * | "/tmp/tst.go" | "go" |
+ * | "/tmp/.classpath" | "classpath" |
+ * | "/bin/bash" | not defined |
+ * | "/tmp/tst2." | "" |
+ * | "/tmp/x.tar.gz" | "gz" |
+ *
+ */
+ string getExtension() { result = getAbsolutePath().regexpCapture(".*/([^/]*?)(\\.([^.]*))?", 3) }
+
+ /** Gets the file in this container that has the given `baseName`, if any. */
+ File getFile(string baseName) {
+ result = getAFile() and
+ result.getBaseName() = baseName
+ }
+
+ /** Gets the sub-folder in this container that has the given `baseName`, if any. */
+ Folder getFolder(string baseName) {
+ result = getAFolder() and
+ result.getBaseName() = baseName
+ }
+
+ /** Gets the parent container of this file or folder, if any. */
+ Container getParentContainer() { containerparent(result, this) }
+
+ /**
+ * Gets the relative path of this file or folder from the root folder of the
+ * analyzed source location. The relative path of the root folder itself is
+ * the empty string.
+ *
+ * This has no result if the container is outside the source root, that is,
+ * if the root folder is not a reflexive, transitive parent of this container.
+ */
+ string getRelativePath() {
+ exists(string absPath, string pref |
+ absPath = getAbsolutePath() and sourceLocationPrefix(pref)
+ |
+ absPath = pref and result = ""
+ or
+ absPath = pref.regexpReplaceAll("/$", "") + "/" + result and
+ not result.matches("/%")
+ )
+ }
+
+ /**
+ * Gets the stem of this container, that is, the prefix of its base name up to
+ * (but not including) the last dot character if there is one, or the entire
+ * base name if there is not.
+ *
+ * Here are some examples of absolute paths and the corresponding stems
+ * (surrounded with quotes to avoid ambiguity):
+ *
+ *
+ * | Absolute path | Stem |
+ * | "/tmp/tst.go" | "tst" |
+ * | "/tmp/.classpath" | "" |
+ * | "/bin/bash" | "bash" |
+ * | "/tmp/tst2." | "tst2" |
+ * | "/tmp/x.tar.gz" | "x.tar" |
+ *
+ */
+ string getStem() { result = getAbsolutePath().regexpCapture(".*/([^/]*?)(?:\\.([^.]*))?", 1) }
+
+ /**
+ * Gets a URL representing the location of this container.
+ *
+ * For more information see https://lgtm.com/help/ql/locations#providing-urls.
+ */
+ abstract string getURL();
+
+ /**
+ * Gets a textual representation of the path of this container.
+ *
+ * This is the absolute path of the container.
+ */
+ string toString() { result = getAbsolutePath() }
+}
+
+/** A folder. */
+class Folder extends Container, @folder {
+ override string getAbsolutePath() { folders(this, result, _) }
+
+ /** Gets the URL of this folder. */
+ override string getURL() { result = "folder://" + getAbsolutePath() }
+}
+
+/** A file. */
+class File extends Container, @file {
+ override string getAbsolutePath() { files(this, result, _, _, _) }
+
+ /** Gets the URL of this file. */
+ override string getURL() { result = "file://" + this.getAbsolutePath() + ":0:0:0:0" }
+}