-
Notifications
You must be signed in to change notification settings - Fork 145
/
FileSpec.scala
649 lines (574 loc) · 25.5 KB
/
FileSpec.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
package better.files
import java.nio.file.{FileAlreadyExistsException, Files => JFiles, FileSystems}
import java.nio.file.AccessDeniedException
import scala.collection.compat.immutable.LazyList
import scala.language.postfixOps
import scala.util.{Properties, Try}
import better.files.Dsl._
import better.files.File.{home, root}
class FileSpec extends CommonSpec {
/** try to cope with windows, which will return e.g. c:\ as root */
val rootStr = FileSystems.getDefault.getRootDirectories.iterator().next().toString
import java.io.File.{separator, separatorChar}
/** Helper for unix -> windows path references (as strings).
*
* @param path as unix path
* @return path in native format
*/
def unixToNative(path: String): String = {
if (Properties.isWin) {
path
.replaceFirst("^/", rootStr.replace("\\", "\\\\")) // we must escape '\' in C:\
.replace("/", separator)
} else {
path
}
}
var testRoot: File = _
var fa: File = _
var a1: File = _
var a2: File = _
var t1: File = _
var t2: File = _
var t3: File = _
var fb: File = _
var b1: File = _
var b2: File = _
/** Setup the following directory structure under root
* /a
* /a1
* /a2
* a21.txt
* a22.txt
* /b
* b1/ --> ../a1
* b2.txt --> ../a2/a22.txt
*/
override def beforeEach() = {
testRoot = File.newTemporaryDirectory("better-files")
fa = testRoot / "a"
a1 = testRoot / "a" / "a1"
a2 = testRoot / "a" / "a2"
t1 = testRoot / "a" / "a1" / "t1.txt"
t2 = testRoot / "a" / "a1" / "t2.txt"
t3 = testRoot / "a" / "a1" / "t3.scala.txt"
fb = testRoot / "b"
b1 = testRoot / "b" / "b1"
b2 = testRoot / Symbol("b") / "b2.txt"
Seq(a1, a2, fb) foreach mkdirs
Seq(t1, t2) foreach touch
}
override def afterEach() =
rm(testRoot)
override def withFixture(test: NoArgTest) = {
// val before = File.numberOfOpenFileDescriptors()
val result = super.withFixture(test)
// val after = File.numberOfOpenFileDescriptors()
// assert(before == after, s"Resource leakage detected in $test")
result
}
"files" can "be instantiated" in {
import java.io.{File => JFile}
val f = File("/User/johndoe/Documents") // using constructor
val f1: File = file"/User/johndoe/Documents" // using string interpolator
val f2: File = "/User/johndoe/Documents".toFile // convert a string path to a file
val f3: File = new JFile("/User/johndoe/Documents").toScala // convert a Java file to Scala
val f4: File = root / "User" / "johndoe" / "Documents" // using root helper to start from root
// val f5: File = `~` / "Documents" // also equivalent to `home / "Documents"`
val f6: File = "/User" / "johndoe" / "Documents" // using file separator DSL
val f7: File = home / "Documents" / "presentations" / `..` // Use `..` to navigate up to parent
val f8: File = root / "User" / "johndoe" / "Documents" / `.`
val f9: File = File(f.uri)
val f10: File = File("../a") // using a relative path
Seq(f, f1, f2, f3, f4, /* f5,*/ f6, f7, f8, f9, f10) foreach { f =>
f.pathAsString should not include ".."
}
root.toString shouldEqual rootStr
home.toString.count(_ == separatorChar) should be > 1
(root / "usr" / "johndoe" / "docs").toString shouldEqual unixToNative("/usr/johndoe/docs")
Seq(f, f1, f2, f4, /*f5,*/ f6, f8, f9).map(_.toString).toSet shouldBe Set(f.toString)
}
it can "be instantiated with anchor" in {
// testRoot / a / a1 / t1.txt
val basedir = a1
File(basedir, "/abs/path/to/loc").toString should be(unixToNative("/abs/path/to/loc"))
File(basedir, "/abs", "path", "to", "loc").toString should be(unixToNative("/abs/path/to/loc"))
File(basedir, "rel/path/to/loc").toString should be(unixToNative(basedir.toString + "/rel/path/to/loc"))
File(basedir, "../rel/path/to/loc").toString should be(unixToNative(fa.toString + "/rel/path/to/loc"))
File(basedir, "../", "rel", "path", "to", "loc").toString should be(unixToNative(fa.toString + "/rel/path/to/loc"))
val baseref = t1
File(baseref, "/abs/path/to/loc").toString should be(unixToNative("/abs/path/to/loc"))
File(baseref, "/abs", "path", "to", "loc").toString should be(unixToNative("/abs/path/to/loc"))
File(baseref, "rel/path/to/loc").toString should be(unixToNative(a1.toString + "/rel/path/to/loc"))
File(baseref, "../rel/path/to/loc").toString should be(unixToNative(fa.toString + "/rel/path/to/loc"))
File(basedir, "../", "rel", "path", "to", "loc").toString should be(unixToNative(fa.toString + "/rel/path/to/loc"))
}
it can "be instantiated with non-existing abs anchor" in {
val anchorStr = "/abs/to/nowhere"
val anchorStr_a = anchorStr + "/a"
val basedir = File(anchorStr_a + "/last")
File(basedir, "/abs/path/to/loc").toString should be(unixToNative("/abs/path/to/loc"))
File(basedir, "/abs", "path", "to", "loc").toString should be(unixToNative("/abs/path/to/loc"))
File(basedir, "rel/path/to/loc").toString should be(unixToNative(anchorStr_a + "/rel/path/to/loc"))
File(basedir, "../rel/path/to/loc").toString should be(unixToNative(anchorStr + "/rel/path/to/loc"))
File(basedir, "../", "rel", "path", "to", "loc").toString should be(unixToNative(anchorStr + "/rel/path/to/loc"))
}
it can "be instantiated with non-existing relative anchor" in {
val relAnchor = File("rel/anc/b/last")
val basedir = relAnchor
File(basedir, "/abs/path/to/loc").toString should be(unixToNative("/abs/path/to/loc"))
File(basedir, "/abs", "path", "to", "loc").toString should be(unixToNative("/abs/path/to/loc"))
File(basedir, "rel/path/to/loc").toString should be(unixToNative(File("rel/anc/b").toString + "/rel/path/to/loc"))
File(basedir, "../rel/path/to/loc").toString should be(unixToNative(File("rel/anc").toString + "/rel/path/to/loc"))
File(basedir, "../", "rel", "path", "to", "loc").toString should be(
unixToNative(File("rel/anc").toString + "/rel/path/to/loc")
)
}
it should "do basic I/O" in {
t1 < "hello"
t1.contentAsString() shouldEqual "hello"
t1.appendLine() << "world"
(t1 !) shouldEqual String.format("hello%nworld%n")
t1.chars().to(LazyList) should contain theSameElementsInOrderAs String.format("hello%nworld%n").toSeq
"foo" `>:` t1
"bar" >>: t1
t1.contentAsString() shouldEqual String.format("foobar%n")
t1.appendLines("hello", "world")
t1.contentAsString() shouldEqual String.format("foobar%nhello%nworld%n")
t2.writeText("hello").appendText("world").contentAsString() shouldEqual "helloworld"
(testRoot / "diary")
.createFileIfNotExists()
.appendLine()
.appendLines("My name is", "Inigo Montoya")
.printLines(Iterator("x", 1))
.lines()
.toSeq should contain theSameElementsInOrderAs Seq("", "My name is", "Inigo Montoya", "x", "1")
}
it should "handle BOM" in {
val lines = Seq("Line 1", "Line 2")
val expectedContent = lines.mkString(start = "", sep = "\n", end = "\n")
File.temporaryFile() foreach { file =>
file.appendLines(lines, charset = UnicodeCharset("UTF-8", writeByteOrderMarkers = true))
file.contentAsString(charset = "UTF-8") should not equal expectedContent
file.contentAsString() shouldEqual expectedContent
}
}
// it should "glob" in {
// assume(isCI)
// a1.glob("*.txt").map(_.name).toSeq.sorted shouldEqual Seq("t1.txt", "t2.txt")
// //a1.glob("*.txt").map(_.name).toSeq shouldEqual Seq("t1.txt", "t2.txt")
// testRoot.glob("**/*.txt").map(_.name).toSeq.sorted shouldEqual Seq("t1.txt", "t2.txt")
// val path = testRoot.path.toString.ensuring(testRoot.path.isAbsolute)
// File(path).glob("**/*.{txt}").map(_.name).toSeq.sorted shouldEqual Seq("t1.txt", "t2.txt")
// ("benchmarks"/"src").glob("**/*.{scala,java}").map(_.name).toSeq.sorted shouldEqual Seq("ArrayBufferScanner.java", "Benchmark.scala", "EncodingBenchmark.scala", "ScannerBenchmark.scala", "Scanners.scala")
// ("benchmarks"/"src").glob("**/*.{scala}").map(_.name).toSeq.sorted shouldEqual Seq( "Benchmark.scala", "EncodingBenchmark.scala", "ScannerBenchmark.scala", "Scanners.scala")
// ("benchmarks"/"src").glob("**/*.scala").map(_.name).toSeq.sorted shouldEqual Seq("Benchmark.scala", "EncodingBenchmark.scala", "ScannerBenchmark.scala", "Scanners.scala")
// ("benchmarks"/"src").listRecursively.filter(_.extension.contains(".scala")).map(_.name).toSeq.sorted shouldEqual Seq( "Benchmark.scala", "EncodingBenchmark.scala", "ScannerBenchmark.scala", "Scanners.scala")
// ls("core"/"src"/"test") should have length 1
// ("core"/"src"/"test").walk(maxDepth = 1) should have length 2
// ("core"/"src"/"test").walk(maxDepth = 0) should have length 1
// ("core"/"src"/"test").walk() should have length (("core"/"src"/"test").listRecursively.length + 1L)
// ls_r("core"/"src"/"test") should have length 8
// }
it should "support names/extensions" in {
assert(File("zzz").changeExtensionTo("ddd").name === "zzz.ddd")
assert(File("zzz").changeExtensionTo(".ddd").name === "zzz.ddd")
fa.extension() shouldBe None
fa.nameWithoutExtension shouldBe fa.name
t1.extension() shouldBe Some(".txt")
t1.extension(includeDot = false) shouldBe Some("txt")
t3.extension() shouldBe Some(".txt")
t3.extension(includeAll = true) shouldBe Some(".scala.txt")
t3.extension(includeDot = false, includeAll = true) shouldBe Some("scala.txt")
t1.name shouldBe "t1.txt"
t1.nameWithoutExtension shouldBe "t1"
t1.changeExtensionTo(".md").name shouldBe "t1.md"
(t1 < "hello world").changeExtensionTo(".txt").name shouldBe "t1.txt"
// t1.contentType shouldBe Some("text/plain")
("src" / "test").toString should include("better-files")
(t1.contentAsString() == t1.toString) shouldBe false
t1.root shouldEqual fa.root
file"/tmp/foo.scala.html".extension() shouldBe Some(".html")
file"/tmp/foo.scala.html".nameWithoutExtension shouldBe "foo"
file"/tmp/foo.scala.html".nameWithoutExtension(includeAll = false) shouldBe "foo.scala"
root.name shouldBe ""
}
it should "hide/unhide" in {
t1.isHidden shouldBe false
}
it should "support parent/child" in {
fa isChildOf testRoot shouldBe true
testRoot isChildOf root shouldBe true
root isChildOf root shouldBe false
fa isChildOf fa shouldBe false
fa isParentOf fa shouldBe false
b2 isChildOf b2 shouldBe false
b2 isChildOf b2.parent shouldBe true
root.parent shouldBe null
}
it should "support siblings" in {
(file"/tmp/foo.txt" sibling "bar.txt").pathAsString shouldBe unixToNative("/tmp/bar.txt")
fa.siblings.toList.map(_.name) shouldBe List("b")
fb isSiblingOf fa shouldBe true
}
it should "support sorting" in {
testRoot.list.toSeq.sorted(File.Order.byName) should not be empty
testRoot.list.toSeq.max(File.Order.bySize).isEmpty() shouldBe false
Seq(fa, fb).contains(testRoot.list.toSeq.min(File.Order.byDepth)) shouldBe true
sleep()
t2.appendLine("modified!")
a1.list.toSeq.min(File.Order.byModificationTime) shouldBe t1
testRoot.list.toSeq.sorted(File.Order.byDirectoriesFirst) should not be empty
}
it must "have .size" in {
fb.isEmpty() shouldBe true
t1.size() shouldBe 0
t1.writeText("Hello World")
t1.size() should be > 0L
testRoot.size() should be > (t1.size() + t2.size())
}
// NOTE: Commented out because it's no longer needed. Expected to be vetted and removed by the project maintainer.
// it should "set/unset permissions" in {
// assume(isCI)
// import java.nio.file.attribute.PosixFilePermission
// //an[UnsupportedOperationException] should be thrownBy t1.dosAttributes
// t1.permissions()(PosixFilePermission.OWNER_EXECUTE) shouldBe false
//
// chmod_+(PosixFilePermission.OWNER_EXECUTE, t1)
// t1.testPermission(PosixFilePermission.OWNER_EXECUTE) shouldBe true
// t1.permissionsAsString shouldBe "rwxrw-r--"
//
// chmod_-(PosixFilePermission.OWNER_EXECUTE, t1)
// t1.isOwnerExecutable shouldBe false
// t1.permissionsAsString shouldBe "rw-rw-r--"
// }
it should "support equality" in {
import better.files.Dsl._
fa shouldEqual (testRoot / "a")
fa shouldNot equal(testRoot / "b")
val c1 = fa.md5()
fa.md5() shouldEqual c1
t1 < "hello"
t2 < "hello"
(t1 == t2) shouldBe false
(t1 === t2) shouldBe true
t2 < "hello world"
(t1 == t2) shouldBe false
(t1 === t2) shouldBe false
fa.md5() should not equal c1
}
it should "create if not exist directory structures" in {
File.usingTemporaryDirectory() { dir =>
val file = dir / "a" / "b" / "c.txt"
assert(file.notExists())
assert(file.parent.notExists())
file.createIfNotExists(createParents = true)
assert(file.exists())
assert(file.parent.exists())
file.writeText("Hello world")
assert(file.contentAsString() === "Hello world")
}
}
it should "treat symlinks transparently in convenience methods" in {
File.usingTemporaryDirectory() { dir =>
val realDir = dir / "a"
val dirSymlink = dir / "b"
realDir.createDirectory()
JFiles.createSymbolicLink(dirSymlink.path, realDir.path)
dirSymlink.createDirectories()
a[FileAlreadyExistsException] should be thrownBy dirSymlink.createDirectories(linkOptions = File.LinkOptions.noFollow)
/*a[FileAlreadyExistsException] shouldNot be thrownBy*/
dirSymlink.createDirectories()
}
}
it should "support chown/chgrp" in {
fa.ownerName() should not be empty
fa.groupName() should not be empty
a[java.nio.file.attribute.UserPrincipalNotFoundException] should be thrownBy chown("hitler", fa)
// a[java.nio.file.FileSystemException] should be thrownBy chown("root", fa)
a[java.nio.file.attribute.UserPrincipalNotFoundException] should be thrownBy chgrp("cool", fa)
// a[java.nio.file.FileSystemException] should be thrownBy chown("admin", fa)
// fa.chown("nobody").chgrp("nobody")
stat(t1) shouldBe a[java.nio.file.attribute.PosixFileAttributes]
}
it should "detect file locks" in {
File.temporaryFile() foreach { file =>
def lockInfo() = file.isReadLocked() -> file.isWriteLocked()
// TODO: Why is file.isReadLocked() should be false?
lockInfo() shouldBe (true -> false)
val channel = file.newRandomAccess(File.RandomAccessMode.readWrite).getChannel
val lock = channel.tryLock()
lockInfo() shouldBe (true -> true)
lock.release()
channel.close()
lockInfo() shouldBe (true -> false)
}
}
it should "support ln/cp/mv" in {
val magicWord = "Hello World"
t1 writeText magicWord
// link
// to relative target
val b0 = b1.sibling("b0")
java.nio.file.Files.createSymbolicLink(b0.path, java.nio.file.Paths.get("b1"))
b0.symbolicLink should not be empty
b0.symbolicLink.get.path.isAbsolute shouldBe false
// to absolute target
b1.linkTo(a1, symbolic = true)
ln_s(b2, t2)
(b1 / "t1.txt").contentAsString() shouldEqual magicWord
// copy
b2.contentAsString() shouldBe empty
t1.md5() should not equal t2.md5()
a[java.nio.file.FileAlreadyExistsException] should be thrownBy (t1 copyTo t2)
t1.copyTo(t2, overwrite = true)
t1.exists() shouldBe true
t1.md5() shouldEqual t2.md5()
b2.contentAsString() shouldEqual magicWord
// rename
t2.name shouldBe "t2.txt"
t2.exists() shouldBe true
val t3 = t2 renameTo "t3.txt"
t3.name shouldBe "t3.txt"
t2.exists() shouldBe false
t3.exists() shouldBe true
// move
t3 moveTo t2
t2.exists() shouldBe true
t3.exists() shouldBe false
}
it should "support creating hard links with ln" in {
val magicWord = "Hello World"
t1 writeText magicWord
t1.linkTo(t3, symbolic = false)
(a1 / "t3.scala.txt").contentAsString() shouldEqual magicWord
}
it should "support custom charset" in {
import java.nio.charset.Charset
t1.writeText("你好世界", charset = "UTF8")
t1.contentAsString(charset = "ISO-8859-1") should not equal "你好世界"
t1.contentAsString(charset = "UTF8") shouldEqual "你好世界"
val c1 = md5(t1)
val c2 = t1.overwrite("你好世界", File.OpenOptions.default, Charset.forName("ISO-8859-1")).md5()
c1 should not equal c2
c2 shouldEqual t1.checksum("md5")
}
it should "read chinese" in {
val lines = "src/test/resources/better/files/issues-326.txt".toFile.lines().toSeq
assert(lines.length > 20)
}
it should "support hashing algos" in {
val charset = java.nio.charset.StandardCharsets.UTF_8
t1.writeText("", charset = charset)
md5(t1) shouldEqual "D41D8CD98F00B204E9800998ECF8427E"
sha1(t1) shouldEqual "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"
sha256(t1) shouldEqual "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
sha512(t1) shouldEqual
"CF83E1357EEFB8BDF1542850D66D8007D620E4050B5715DC83F4A921D36CE9CE47D0D13C5D85F2B0FF8318D2877EEC2F63B931BD47417A81A538327AF927DA3E"
}
it should "compute correct checksum for non-zero length string" in {
val charset = java.nio.charset.StandardCharsets.UTF_8
t1.writeText("test", charset = charset)
md5(t1) shouldEqual "098F6BCD4621D373CADE4E832627B4F6"
sha1(t1) shouldEqual "A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"
sha256(t1) shouldEqual "9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"
sha512(t1) shouldEqual
"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"
}
it should "copy" in {
(fb / "t3" / "t4.txt").createIfNotExists(createParents = true).writeText("Hello World")
(fb / "t5" / "t5.txt").createIfNotExists(createParents = true).writeText("Scala Awesome")
(fb / "t5" / "t3").notExists() shouldBe true
cp(fb / "t3", fb / "t5")
(fb / "t3").exists() shouldBe true
(fb / "t5" / "t3").exists() shouldBe true
(fb / "t5" / "t5.txt").contentAsString() shouldEqual "Scala Awesome"
assert((fb / "t3") isSameContentAs (fb / "t5" / "t3"))
}
it should "move" in {
(fb / "t3" / "t4.txt").createIfNotExists(createParents = true).writeText("Hello World")
mv(fb / "t3", fb / "t5")
(fb / "t5" / "t4.txt").contentAsString() shouldEqual "Hello World"
(fb / "t3").notExists() shouldBe true
}
it should "delete" in {
fb.exists() shouldBe true
fb.delete()
fb.exists() shouldBe false
}
it should "touch" in {
(fb / "z1").exists() shouldBe false
(fb / "z1").isEmpty() shouldBe true
(fb / "z1").touch()
(fb / "z1").exists() shouldBe true
(fb / "z1").isEmpty() shouldBe true
Thread.sleep(1000)
(fb / "z1").lastModifiedTime().getEpochSecond should be < (fb / "z1").touch().lastModifiedTime().getEpochSecond
}
it should "md5" in {
val h1 = t1.hashCode
val actual = (t1 < "hello world").md5()
val h2 = t1.hashCode
h1 shouldEqual h2
import scala.sys.process._
val expected = Try(s"md5sum ${t1.path}" !!) getOrElse (s"md5 ${t1.path}" !!)
expected.toUpperCase should include(actual)
actual should not equal h1
actual shouldEqual t1.newInputStream().withMessageDigest("md5").hexDigest()
}
it should "support file in/out" in {
t1 < "hello world"
for {
in <- t1.inputStream()
out <- t2.outputStream()
} in.pipeTo(out)
t2.contentAsString() shouldEqual "hello world"
t2.newInputStream().asString() shouldEqual "hello world"
}
it should "zip/unzip directories" in {
assume(Properties.javaVersion.startsWith("1.8"))
t1.writeText("hello world")
val zipFile = testRoot.zip()
zipFile.size() should be > 100L
zipFile.name should endWith(".zip")
def test(output: File) = {
import better.files.Dsl._
(output / "a" / "a1" / "t1.txt").contentAsString() shouldEqual "hello world"
output === testRoot shouldBe true
(output / "a" / "a1" / "t1.txt").overwrite("hello")
(output !== testRoot) shouldBe true
}
test(zipFile.unzip())
test(zipFile.streamedUnzip())
}
it should "zip/unzip single files" in {
assume(Properties.javaVersion.startsWith("1.8"))
t1.writeText("hello world")
val zipFile = t1.zip()
zipFile.size() should be > 100L
zipFile.name should endWith(".zip")
val destination = unzip(zipFile)(File.newTemporaryDirectory())
(destination / "t1.txt").contentAsString() shouldEqual "hello world"
}
it should "zip/unzip multiple files" in {
File.usingTemporaryDirectory() { dir =>
val f1 = (dir / Symbol("f1")).touch().appendLines("Line 1", "Line 2")
val f2 = (dir / Symbol("f2")).touch().appendLines("Line 3", "Line 4")
val zipFile = (dir / "f.zip").zipIn(Iterator(f1, f2))
val lines = zipFile.newZipInputStream().foldMap(_.lines().toSeq).flatten
lines.toSeq shouldEqual Seq("Line 1", "Line 2", "Line 3", "Line 4")
}
}
it should "exclude destination zip when it's under directory to be zipped" in {
File.usingTemporaryDirectory() { dir =>
(dir / Symbol("f1")).touch().appendLines("Line 1", "Line 2")
(dir / Symbol("f2")).touch().appendLines("Line 3", "Line 4")
val zipFile = (dir / "f.zip")
val zipped = dir.zipTo(zipFile.path)
zipped.unzipTo().listRecursively().toList.map(_.name).forall(!_.contains("zip")) shouldBe true
}
}
it should "handle backslashes in zip entry name" in {
val list = File("src/test/resources/better/files/issues-262.zip")
.unzipTo()
.listRecursively()
.toList
assert(list.length === 3)
}
it should "unzip safely by default" in {
val list = File("src/test/resources/better/files/issue-624.zip")
.unzipTo()
.listRecursively()
.toList
// Three total entries in the zip file: 1 without directory traversal characters and 2 with. Default secure unzip should only unzip the entry without directory traversal characters.
assert(list.length === 1)
}
it should "unzip unsafely when safe unzip is disabled" in {
// Unsafe unzip with a zipslip attack may result in OS access denied exceptions. If these occur, the test should still pass.
var destList = List.empty[File]
var list = List.empty[File]
try {
// create the directory structure safe for issue 624 zip file
val toplevel = File.newTemporaryDirectory("issue-624")
val dest = (toplevel / "a" / "b").createDirectories()
destList = File("src/test/resources/better/files/issue-624.zip")
.unzipTo(destination = dest, safeUnzip = false)
.listRecursively()
.toList
list = toplevel
.listRecursively()
.filterNot(_.isDirectory())
.toList
} catch {
// Unsafe unzip should try to extract all entries, might result in access denied exception from OS.
case exception: Throwable => assert(exception.isInstanceOf[AccessDeniedException])
}
// Three total entries in the zip file: 1 without directory traversal characters and 2 with.
assert(destList.length === 1)
assert(list.length === 3)
}
it should "ungzip" in {
val data = Seq("hello", "world")
for {
pw <- (testRoot / "test.gz").newOutputStream().asGzipOutputStream().printWriter().autoClosed
line <- data
} pw.println(line)
(testRoot / "test.gz").inputStream().flatMap(_.asGzipInputStream().lines()).toSeq shouldEqual data
}
it should "gzip" in {
val actual = t1
.writeText("hello world")
.gzipTo()
.unGzipTo()
.contentAsString()
assert(actual === "hello world")
t1.clear().newGzipOutputStream().writeAndClose("hello world2")
assert(t1.newGzipInputStream().asString() === "hello world2")
}
it should "read bytebuffers" in {
t1.writeText("hello world")
for {
fileChannel <- t1.newFileChannel().autoClosed
} fileChannel.toMappedByteBuffer.remaining() shouldEqual t1.bytes.length
(t2 writeBytes t1.bytes).contentAsString() shouldEqual t1.contentAsString()
t1.newInputStream().bytes.toArray shouldEqual t1.newInputStream().byteArray
}
it should "convert readers to inputstreams and writers to outputstreams" in {
File.temporaryFile() foreach { f =>
val text = List.fill(10000)("hello world")
for {
writer <- f.bufferedWriter()
out <- writer.outputstream().autoClosed
} out.write(text.mkString("\n").getBytes)
val t = f.bufferedReader().flatMap(_.toInputStream().lines())
t.toList shouldEqual text
}
}
it should "serialize/deserialize" in {
assume(
scalaVersion.startsWith("2.12") || scalaVersion.startsWith("2.13")
) // inline classes not serializable in Scala 2.11 because of https://github.com/scala/bug/issues/10233
class Person(val name: String, val age: Int) extends Serializable
val p1 = new Person("Chris", 34)
File.temporaryFile() foreach { f => // serialization round-trip test
assert(f.isEmpty())
f.writeSerialized(p1)
assert(f.nonEmpty())
val p2: Person = f.readDeserialized[Person]()
assert(p1.name === p2.name)
assert(p1.age === p2.age)
val p3 = f.inputStream().apply(_.asObjectInputStreamUsingClassLoader().deserialize[Person])
assert(p3.name === p2.name)
assert(p3.age === p2.age)
}
}
it should "serialize/deserialize primitives" in {
assert(t1.writeSerialized(23).readDeserialized[Int]() === 23)
}
it should "count number of open file descriptors" in {
val expected = java.lang.management.ManagementFactory.getOperatingSystemMXBean
.asInstanceOf[com.sun.management.UnixOperatingSystemMXBean]
.getOpenFileDescriptorCount
assert((File.numberOfOpenFileDescriptors() - expected).abs <= 10)
}
}