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