Newer
Older
dub_jkp / source / dub / internal / io / mockfs.d
  1. /*******************************************************************************
  2.  
  3. An unittest implementation of `Filesystem`
  4.  
  5. *******************************************************************************/
  6.  
  7. module dub.internal.io.mockfs;
  8.  
  9. public import dub.internal.io.filesystem;
  10.  
  11. static import dub.internal.vibecompat.core.file;
  12.  
  13. import std.algorithm;
  14. import std.exception;
  15. import std.range;
  16. import std.string;
  17.  
  18. /// Ditto
  19. public final class MockFS : Filesystem {
  20. ///
  21. private FSEntry cwd;
  22.  
  23. ///
  24. public this () scope
  25. {
  26. this.cwd = new FSEntry();
  27. }
  28.  
  29. public override NativePath getcwd () const scope
  30. {
  31. return this.cwd.path();
  32. }
  33.  
  34. ///
  35. public override bool existsDirectory (in NativePath path) const scope
  36. {
  37. auto entry = this.cwd.lookup(path);
  38. return entry !is null && entry.isDirectory();
  39. }
  40.  
  41. /// Ditto
  42. public override void mkdir (in NativePath path) scope
  43. {
  44. this.cwd.mkdir(path);
  45. }
  46.  
  47. /// Ditto
  48. public override bool existsFile (in NativePath path) const scope
  49. {
  50. auto entry = this.cwd.lookup(path);
  51. return entry !is null && entry.isFile();
  52. }
  53.  
  54. /// Ditto
  55. public override void writeFile (in NativePath path, const(ubyte)[] data)
  56. scope
  57. {
  58. return this.cwd.writeFile(path, data);
  59. }
  60.  
  61. /// Reads a file, returns the content as `ubyte[]`
  62. public override ubyte[] readFile (in NativePath path) const scope
  63. {
  64. return this.cwd.readFile(path);
  65. }
  66.  
  67. /// Ditto
  68. public override string readText (in NativePath path) const scope
  69. {
  70. return this.cwd.readText(path);
  71. }
  72.  
  73. /// Ditto
  74. public override IterateDirDg iterateDirectory (in NativePath path) scope
  75. {
  76. enforce(this.existsDirectory(path),
  77. path.toNativeString() ~ " does not exists or is not a directory");
  78. auto dir = this.cwd.lookup(path);
  79. int iterator(scope int delegate(ref dub.internal.vibecompat.core.file.FileInfo) del) {
  80. foreach (c; dir.children) {
  81. dub.internal.vibecompat.core.file.FileInfo fi;
  82. fi.name = c.name;
  83. fi.timeModified = c.attributes.modification;
  84. final switch (c.attributes.type) {
  85. case FSEntry.Type.File:
  86. fi.size = c.content.length;
  87. break;
  88. case FSEntry.Type.Directory:
  89. fi.isDirectory = true;
  90. break;
  91. }
  92. if (auto res = del(fi))
  93. return res;
  94. }
  95. return 0;
  96. }
  97. return &iterator;
  98. }
  99.  
  100. /// Ditto
  101. public override void removeFile (in NativePath path, bool force = false) scope
  102. {
  103. return this.cwd.removeFile(path);
  104. }
  105.  
  106. ///
  107. public override void removeDir (in NativePath path, bool force = false)
  108. {
  109. this.cwd.removeDir(path, force);
  110. }
  111.  
  112. /// Ditto
  113. public override void setTimes (in NativePath path, in SysTime accessTime,
  114. in SysTime modificationTime)
  115. {
  116. auto e = this.cwd.lookup(path);
  117. enforce(e !is null,
  118. "setTimes: No such file or directory: " ~ path.toNativeString());
  119. e.setTimes(accessTime, modificationTime);
  120. }
  121.  
  122. /// Ditto
  123. public override void setAttributes (in NativePath path, uint attributes)
  124. {
  125. auto e = this.cwd.lookup(path);
  126. enforce(e !is null,
  127. "setAttributes: No such file or directory: " ~ path.toNativeString());
  128. e.setAttributes(attributes);
  129. }
  130.  
  131. /**
  132. * Converts an `Filesystem` and its children to a `ZipFile`
  133. */
  134. public ubyte[] serializeToZip (string rootPath) {
  135. import std.path;
  136. import std.zip;
  137.  
  138. scope z = new ZipArchive();
  139. void addToZip(scope string dir, scope FSEntry e) {
  140. auto m = new ArchiveMember();
  141. m.name = dir.buildPath(e.name);
  142. m.fileAttributes = e.attributes.attrs;
  143. m.time = e.attributes.modification;
  144.  
  145. final switch (e.attributes.type) {
  146. case FSEntry.Type.Directory:
  147. // We need to ensure the directory entry ends with a slash
  148. // otherwise it will be considered as a file.
  149. if (m.name[$-1] != '/')
  150. m.name ~= '/';
  151. z.addMember(m);
  152. foreach (c; e.children)
  153. addToZip(m.name, c);
  154. break;
  155. case FSEntry.Type.File:
  156. m.expandedData = e.content;
  157. z.addMember(m);
  158. }
  159. }
  160. addToZip(rootPath, this.cwd);
  161. return cast(ubyte[]) z.build();
  162. }
  163. }
  164.  
  165. /// The backing logic behind `MockFS`
  166. public class FSEntry
  167. {
  168. /// Type of file system entry
  169. public enum Type : ubyte {
  170. Directory,
  171. File,
  172. }
  173.  
  174. /// List FSEntry attributes
  175. protected struct Attributes {
  176. /// The type of FSEntry, see `FSEntry.Type`
  177. public Type type;
  178. /// System-specific attributes for this `FSEntry`
  179. public uint attrs;
  180. /// Last access time
  181. public SysTime access;
  182. /// Last modification time
  183. public SysTime modification;
  184. }
  185. /// Ditto
  186. protected Attributes attributes;
  187.  
  188. /// The name of this node
  189. protected string name;
  190. /// The parent of this entry (can be null for the root)
  191. protected FSEntry parent;
  192. union {
  193. /// Children for this FSEntry (with type == Directory)
  194. protected FSEntry[] children;
  195. /// Content for this FDEntry (with type == File)
  196. protected ubyte[] content;
  197. }
  198.  
  199. /// Creates a new FSEntry
  200. package(dub) this (FSEntry p, Type t, string n)
  201. {
  202. // Avoid 'DOS File Times cannot hold dates prior to 1980.' exception
  203. import std.datetime.date;
  204. SysTime DefaultTime = SysTime(DateTime(2020, 01, 01));
  205.  
  206. this.attributes.type = t;
  207. this.parent = p;
  208. this.name = n;
  209. this.attributes.access = DefaultTime;
  210. this.attributes.modification = DefaultTime;
  211. }
  212.  
  213. /// Create the root of the filesystem, only usable from this module
  214. package(dub) this ()
  215. {
  216. import std.datetime.date;
  217. SysTime DefaultTime = SysTime(DateTime(2020, 01, 01));
  218.  
  219. this.attributes.type = Type.Directory;
  220. this.attributes.access = DefaultTime;
  221. this.attributes.modification = DefaultTime;
  222. }
  223.  
  224. /// Get a direct children node, returns `null` if it can't be found
  225. protected inout(FSEntry) lookup(string name) inout return scope
  226. {
  227. assert(!name.canFind('/'));
  228. foreach (c; this.children)
  229. if (c.name == name)
  230. return c;
  231. return null;
  232. }
  233.  
  234. /// Get an arbitrarily nested children node
  235. protected inout(FSEntry) lookup(NativePath path) inout return scope
  236. {
  237. auto relp = this.relativePath(path);
  238. relp.normalize(); // try to get rid of `..`
  239. if (relp.empty)
  240. return this;
  241. auto segments = relp.bySegment;
  242. if (auto c = this.lookup(segments.front.name)) {
  243. segments.popFront();
  244. return !segments.empty ? c.lookup(NativePath(segments)) : c;
  245. }
  246. return null;
  247. }
  248.  
  249. /** Get the parent `FSEntry` of a `NativePath`
  250. *
  251. * If the parent doesn't exist, an `Exception` will be thrown
  252. * unless `silent` is provided. If the parent path is a file,
  253. * an `Exception` will be thrown regardless of `silent`.
  254. *
  255. * Params:
  256. * path = The path to look up the parent for
  257. * silent = Whether to error on non-existing parent,
  258. * default to `false`.
  259. */
  260. protected inout(FSEntry) getParent(NativePath path, bool silent = false)
  261. inout return scope
  262. {
  263. // Relative path in the current directory
  264. if (!path.hasParentPath())
  265. return this;
  266.  
  267. // If we're not in the right `FSEntry`, recurse
  268. const parentPath = path.parentPath();
  269. auto p = this.lookup(parentPath);
  270. enforce(silent || p !is null,
  271. "No such directory: " ~ parentPath.toNativeString());
  272. enforce(p is null || p.attributes.type == Type.Directory,
  273. "Parent path is not a directory: " ~ parentPath.toNativeString());
  274. return p;
  275. }
  276.  
  277. /// Returns: A path relative to `this.path`
  278. protected NativePath relativePath(NativePath path) const scope
  279. {
  280. assert(!path.absolute() || path.startsWith(this.path),
  281. "Calling relativePath with a differently rooted path");
  282. return path.absolute() ? path.relativeTo(this.path) : path;
  283. }
  284.  
  285. /*+*************************************************************************
  286.  
  287. Utility function
  288.  
  289. Below this banners are functions that are provided for the convenience
  290. of writing tests for `Dub`.
  291.  
  292. ***************************************************************************/
  293.  
  294. /// Prints a visual representation of the filesystem to stdout for debugging
  295. public void print(bool content = false) const scope
  296. {
  297. import std.range : repeat;
  298. static import std.stdio;
  299.  
  300. size_t indent;
  301. for (auto p = &this.parent; (*p) !is null; p = &p.parent)
  302. indent++;
  303. // Don't print anything (even a newline) for root
  304. if (this.parent is null)
  305. std.stdio.write('/');
  306. else
  307. std.stdio.write('|', '-'.repeat(indent), ' ', this.name, ' ');
  308.  
  309. final switch (this.attributes.type) {
  310. case Type.Directory:
  311. std.stdio.writeln('(', this.children.length, " entries):");
  312. foreach (c; this.children)
  313. c.print(content);
  314. break;
  315. case Type.File:
  316. if (!content)
  317. std.stdio.writeln('(', this.content.length, " bytes)");
  318. else if (this.name.endsWith(".json") || this.name.endsWith(".sdl"))
  319. std.stdio.writeln('(', this.content.length, " bytes): ",
  320. cast(string) this.content);
  321. else
  322. std.stdio.writeln('(', this.content.length, " bytes): ",
  323. this.content);
  324. break;
  325. }
  326. }
  327.  
  328. /*+*************************************************************************
  329.  
  330. Public filesystem functions
  331.  
  332. Below this banners are functions which mimic the behavior of a file
  333. system.
  334.  
  335. ***************************************************************************/
  336.  
  337. /// Returns: The `path` of this FSEntry
  338. public NativePath path () const scope
  339. {
  340. if (this.parent is null)
  341. return NativePath("/");
  342. auto thisPath = this.parent.path ~ this.name;
  343. thisPath.endsWithSlash = (this.attributes.type == Type.Directory);
  344. return thisPath;
  345. }
  346.  
  347. /// Implements `mkdir -p`, returns the created directory
  348. public FSEntry mkdir (in NativePath path) scope
  349. {
  350. auto relp = this.relativePath(path);
  351. // Check if the child already exists
  352. auto segments = relp.bySegment;
  353. auto child = this.lookup(segments.front.name);
  354. if (child is null) {
  355. child = new FSEntry(this, Type.Directory, segments.front.name);
  356. this.children ~= child;
  357. }
  358. // Recurse if needed
  359. segments.popFront();
  360. return !segments.empty ? child.mkdir(NativePath(segments)) : child;
  361. }
  362.  
  363. ///
  364. public bool isFile () const scope
  365. {
  366. return this.attributes.type == Type.File;
  367. }
  368.  
  369. ///
  370. public bool isDirectory () const scope
  371. {
  372. return this.attributes.type == Type.Directory;
  373. }
  374.  
  375. /// Reads a file, returns the content as `ubyte[]`
  376. public ubyte[] readFile (in NativePath path) const scope
  377. {
  378. auto entry = this.lookup(path);
  379. enforce(entry !is null, "No such file: " ~ path.toNativeString());
  380. enforce(entry.attributes.type == Type.File, "Trying to read a directory");
  381. // This is a hack to make poisoning a file possible.
  382. // However, it is rather crude and doesn't allow to poison directory.
  383. // Consider introducing a derived type to allow it.
  384. assert(entry.content != "poison".representation,
  385. "Trying to access poisoned path: " ~ path.toNativeString());
  386. return entry.content.dup;
  387. }
  388.  
  389. /// Reads a file, returns the content as text
  390. public string readText (in NativePath path) const scope
  391. {
  392. import std.utf : validate;
  393.  
  394. const content = this.readFile(path);
  395. // Ignore BOM: If it's needed for a test, add support for it.
  396. validate(cast(const(char[])) content);
  397. // `readFile` just `dup` the content, so it's safe to cast.
  398. return cast(string) content;
  399. }
  400.  
  401. /// Ditto
  402. public void writeFile (in NativePath path, const(ubyte)[] data) scope
  403. {
  404. enforce(!path.endsWithSlash(),
  405. "Cannot write to directory: " ~ path.toNativeString());
  406. if (auto file = this.lookup(path)) {
  407. // If the file already exists, override it
  408. enforce(file.attributes.type == Type.File,
  409. "Trying to write to directory: " ~ path.toNativeString());
  410. file.content = data.dup;
  411. } else {
  412. auto p = this.getParent(path);
  413. auto file = new FSEntry(p, Type.File, path.head.name());
  414. file.content = data.dup;
  415. p.children ~= file;
  416. }
  417. }
  418.  
  419. /** Remove a file
  420. *
  421. * Always error if the target is a directory.
  422. * Does not error if the target does not exists
  423. * and `force` is set to `true`.
  424. *
  425. * Params:
  426. * path = Path to the file to remove
  427. * force = Whether to ignore non-existing file,
  428. * default to `false`.
  429. */
  430. public void removeFile (in NativePath path, bool force = false)
  431. {
  432. import std.algorithm.searching : countUntil;
  433.  
  434. assert(!path.empty, "Empty path provided to `removeFile`");
  435. enforce(!path.endsWithSlash(),
  436. "Cannot remove file with directory path: " ~ path.toNativeString());
  437. auto p = this.getParent(path, force);
  438. const idx = p.children.countUntil!(e => e.name == path.head.name());
  439. if (idx < 0) {
  440. enforce(force,
  441. "removeFile: No such file: " ~ path.toNativeString());
  442. } else {
  443. enforce(p.children[idx].attributes.type == Type.File,
  444. "removeFile called on a directory: " ~ path.toNativeString());
  445. p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $];
  446. }
  447. }
  448.  
  449. /** Remove a directory
  450. *
  451. * Remove an existing empty directory.
  452. * If `force` is set to `true`, no error will be thrown
  453. * if the directory is empty or non-existing.
  454. *
  455. * Params:
  456. * path = Path to the directory to remove
  457. * force = Whether to ignore non-existing / non-empty directories,
  458. * default to `false`.
  459. */
  460. public void removeDir (in NativePath path, bool force = false)
  461. {
  462. import std.algorithm.searching : countUntil;
  463.  
  464. assert(!path.empty, "Empty path provided to `removeFile`");
  465. auto p = this.getParent(path, force);
  466. const idx = p.children.countUntil!(e => e.name == path.head.name());
  467. if (idx < 0) {
  468. enforce(force,
  469. "removeDir: No such directory: " ~ path.toNativeString());
  470. } else {
  471. enforce(p.children[idx].attributes.type == Type.Directory,
  472. "removeDir called on a file: " ~ path.toNativeString());
  473. enforce(force || p.children[idx].children.length == 0,
  474. "removeDir called on non-empty directory: " ~ path.toNativeString());
  475. p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $];
  476. }
  477. }
  478.  
  479. /// Implement `std.file.setTimes`
  480. public void setTimes (in SysTime accessTime, in SysTime modificationTime)
  481. {
  482. this.attributes.access = accessTime;
  483. this.attributes.modification = modificationTime;
  484. }
  485.  
  486. /// Implement `std.file.setAttributes`
  487. public void setAttributes (uint attributes)
  488. {
  489. this.attributes.attrs = attributes;
  490. }
  491. }