safe-rm 3.1.5 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -161,6 +161,31 @@ export SAFE_RM_TRASH=/path/to/trash
161
161
  export SAFE_RM_PERM_DEL_FILES_IN_TRASH=yes
162
162
  ```
163
163
 
164
+ ### Restrict Removals To A Safe Directory Scope
165
+
166
+ ```sh
167
+ export SAFE_RM_SCOPE="$HOME"
168
+ ```
169
+
170
+ When `SAFE_RM_SCOPE` is set, `safe-rm` only removes targets under that directory.
171
+ Targets outside the configured scope will be skipped and reported as unsafe.
172
+
173
+ For example:
174
+
175
+ ```sh
176
+ export SAFE_RM_SCOPE="$HOME"
177
+ safe-rm -rf /
178
+ # rm: target '/' skipped, unsafe directory scope
179
+ ```
180
+
181
+ You could also use a relative scope such as `.`:
182
+
183
+ ```sh
184
+ SAFE_RM_SCOPE=. safe-rm -rf ./foo
185
+ SAFE_RM_SCOPE=. safe-rm -rf ../
186
+ # rm: target '../' skipped, unsafe directory scope
187
+ ```
188
+
164
189
  ### Protect Files And Directories From Deleting
165
190
 
166
191
  If you want to protect some certain files or directories from deleting by mistake, you could create a `.gitignore` file under the `"~/.safe-rm/"` directory, you could write [.gitignore rules](https://git-scm.com/docs/gitignore) inside the file.
package/bin/rm.sh CHANGED
@@ -20,10 +20,21 @@ fi
20
20
  # ```
21
21
  SAFE_RM_CONFIG=${SAFE_RM_CONFIG:="$SAFE_RM_CONFIG_ROOT/config"}
22
22
 
23
+ SAFE_RM_SCOPE_ENV_DEFINED=
24
+ SAFE_RM_SCOPE_ENV_VALUE=
25
+ if [[ ${SAFE_RM_SCOPE+x} ]]; then
26
+ SAFE_RM_SCOPE_ENV_DEFINED=1
27
+ SAFE_RM_SCOPE_ENV_VALUE=$SAFE_RM_SCOPE
28
+ fi
29
+
23
30
  if [[ -f "$SAFE_RM_CONFIG" ]]; then
24
31
  source "$SAFE_RM_CONFIG"
25
32
  fi
26
33
 
34
+ if [[ -n "$SAFE_RM_SCOPE_ENV_DEFINED" ]]; then
35
+ SAFE_RM_SCOPE=$SAFE_RM_SCOPE_ENV_VALUE
36
+ fi
37
+
27
38
  # Print debug info or not
28
39
  SAFE_RM_DEBUG=${SAFE_RM_DEBUG:=}
29
40
 
@@ -351,6 +362,8 @@ check_return_status(){
351
362
  remove(){
352
363
  local file=$1
353
364
 
365
+ ensure_safe_scope "$file" || return 1
366
+
354
367
  # if is dir
355
368
  if [[ -d "$file" && ! -L "$file" ]]; then
356
369
 
@@ -492,9 +505,56 @@ do_trash(){
492
505
  get_absolute_path(){
493
506
  local dir
494
507
  local base
495
- dir=$(cd "$(dirname -- "$1")" && pwd)
508
+ if [[ "$1" == "/" ]]; then
509
+ printf '/\n'
510
+ return
511
+ fi
512
+
513
+ dir=$(cd "$(dirname -- "$1")" && pwd) || return 1
496
514
  base=$(basename -- "$1")
497
- printf '%s/%s\n' "$dir" "$base"
515
+
516
+ case "$base" in
517
+ .)
518
+ printf '%s\n' "$dir"
519
+ ;;
520
+
521
+ ..)
522
+ dir=$(cd "$dir/.." && pwd) || return 1
523
+ printf '%s\n' "$dir"
524
+ ;;
525
+
526
+ *)
527
+ printf '%s/%s\n' "$dir" "$base"
528
+ ;;
529
+ esac
530
+ }
531
+
532
+
533
+ expand_home_path(){
534
+ case $1 in
535
+ "~")
536
+ printf '%s\n' "$HOME"
537
+ ;;
538
+
539
+ "~/"*)
540
+ printf '%s/%s\n' "$HOME" "${1#~/}"
541
+ ;;
542
+
543
+ *)
544
+ printf '%s\n' "$1"
545
+ ;;
546
+ esac
547
+ }
548
+
549
+
550
+ resolve_directory_path(){
551
+ local path
552
+ path=$(expand_home_path "$1")
553
+
554
+ (
555
+ cd "$path" &> /dev/null || exit 1
556
+ pwd
557
+ )
498
558
  }
499
559
 
500
560
 
@@ -535,6 +595,44 @@ is_in_trash(){
535
595
  }
536
596
 
537
597
 
598
+ SAFE_RM_SCOPE_ROOT=
599
+ init_safe_rm_scope(){
600
+ if [[ -z "$SAFE_RM_SCOPE" ]]; then
601
+ return 0
602
+ fi
603
+
604
+ SAFE_RM_SCOPE_ROOT=$(resolve_directory_path "$SAFE_RM_SCOPE") || {
605
+ error "$COMMAND: invalid SAFE_RM_SCOPE '$SAFE_RM_SCOPE': not an existing directory"
606
+ return 1
607
+ }
608
+
609
+ debug "$LINENO: safe rm scope enabled: $SAFE_RM_SCOPE_ROOT"
610
+ }
611
+
612
+
613
+ is_in_scope(){
614
+ local target_abs
615
+
616
+ if [[ -z "$SAFE_RM_SCOPE_ROOT" ]]; then
617
+ return 0
618
+ fi
619
+
620
+ target_abs=$(get_absolute_path "$1") || return 1
621
+
622
+ [[ "$target_abs" == "$SAFE_RM_SCOPE_ROOT" || "$target_abs" == "$SAFE_RM_SCOPE_ROOT"/* ]]
623
+ }
624
+
625
+
626
+ ensure_safe_scope(){
627
+ if is_in_scope "$1"; then
628
+ return 0
629
+ fi
630
+
631
+ error "$COMMAND: target '$1' skipped, unsafe directory scope"
632
+ return 1
633
+ }
634
+
635
+
538
636
  # Returns
539
637
  # - 0: the target is protected
540
638
  # - 1: the target is not protected
@@ -806,6 +904,8 @@ list_files(){
806
904
  # debug: get $FILE_NAME array length
807
905
  debug "$LINENO: ${#FILE_NAME[@]} files or directory to process: ${FILE_NAME[@]}"
808
906
 
907
+ init_safe_rm_scope || do_exit $LINENO 1
908
+
809
909
  # test remove interactive_once: ask for 3 or more files or with recursive option
810
910
  if [[ (${#FILE_NAME[@]} > 3 || $OPT_RECURSIVE == 1) && $OPT_INTERACTIVE_ONCE == 1 ]]; then
811
911
  echo -n "$COMMAND: remove all arguments? "
@@ -821,6 +921,11 @@ fi
821
921
  for file in "${FILE_NAME[@]}"; do
822
922
  debug "$LINENO: result file $file"
823
923
 
924
+ if ! ensure_safe_scope "$file"; then
925
+ EXIT_CODE=1
926
+ continue
927
+ fi
928
+
824
929
  if [[ $file == "/" ]]; then
825
930
  error "it is dangerous to operate recursively on /"
826
931
  error "are you insane?"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safe-rm",
3
- "version": "3.1.5",
3
+ "version": "3.2.0",
4
4
  "description": "A much safer replacement of bash rm with nearly full functionalities and options of the rm command!",
5
5
  "bin": {
6
6
  "safe-rm": "./bin/rm.sh"
package/test/cases.js CHANGED
@@ -402,6 +402,149 @@ module.exports = (
402
402
  ])
403
403
  })
404
404
 
405
+ !is_rm(type) && test(`scope "." allows removing targets under cwd only`, async t => {
406
+ const {
407
+ root,
408
+ source_path,
409
+ createDir,
410
+ createFile,
411
+ runRm,
412
+ pathExists
413
+ } = t.context
414
+
415
+ const trash = await createDir({
416
+ name: 'scope-trash-dot',
417
+ under: root
418
+ })
419
+
420
+ const cwd = await createDir({
421
+ name: 'scope-cwd',
422
+ under: source_path
423
+ })
424
+
425
+ const inside = await createDir({
426
+ name: 'foo',
427
+ under: cwd
428
+ })
429
+
430
+ await createFile({
431
+ name: 'bar',
432
+ under: inside
433
+ })
434
+
435
+ const resultInside = await runRm(['-rf', 'foo'], {
436
+ cwd,
437
+ env: {
438
+ SAFE_RM_SCOPE: '.',
439
+ SAFE_RM_TRASH: trash
440
+ }
441
+ })
442
+
443
+ assertEmptySuccess(t, resultInside)
444
+ t.false(await pathExists(inside), 'in-scope target should be removed')
445
+
446
+ const resultOutside = await runRm(['-rf', '../'], {
447
+ cwd,
448
+ env: {
449
+ SAFE_RM_SCOPE: '.',
450
+ SAFE_RM_TRASH: trash
451
+ }
452
+ })
453
+
454
+ t.is(resultOutside.code, 1, 'out-of-scope target should fail')
455
+ t.true(resultOutside.stderr.includes('unsafe directory scope'))
456
+ t.true(await pathExists(source_path), 'parent directory should remain')
457
+ })
458
+
459
+ !is_rm(type) && test(`scope "~" expands HOME and blocks targets outside home`, async t => {
460
+ const {
461
+ root,
462
+ createDir,
463
+ createFile,
464
+ runRm,
465
+ pathExists
466
+ } = t.context
467
+
468
+ const home = await createDir({
469
+ name: 'scope-home',
470
+ under: root
471
+ })
472
+
473
+ const trash = await createDir({
474
+ name: 'scope-trash-home',
475
+ under: root
476
+ })
477
+
478
+ const inside = await createFile({
479
+ name: 'inside-home',
480
+ under: home
481
+ })
482
+
483
+ const outside = await createFile({
484
+ name: 'outside-home'
485
+ })
486
+
487
+ const env = {
488
+ HOME: home,
489
+ SAFE_RM_SCOPE: '~',
490
+ SAFE_RM_TRASH: trash
491
+ }
492
+
493
+ const resultInside = await runRm([inside], {
494
+ env
495
+ })
496
+
497
+ assertEmptySuccess(t, resultInside)
498
+ t.false(await pathExists(inside), 'home-scoped target should be removed')
499
+
500
+ const resultOutside = await runRm([outside], {
501
+ env
502
+ })
503
+
504
+ t.is(resultOutside.code, 1, 'target outside home scope should fail')
505
+ t.true(resultOutside.stderr.includes('unsafe directory scope'))
506
+ t.true(await pathExists(outside), 'out-of-scope file should remain')
507
+ })
508
+
509
+ !is_rm(type) && test(`scope checks symlink path instead of symlink target`, async t => {
510
+ const {
511
+ root,
512
+ createDir,
513
+ createFile,
514
+ runRm,
515
+ pathExists
516
+ } = t.context
517
+
518
+ const trash = await createDir({
519
+ name: 'scope-trash-link',
520
+ under: root
521
+ })
522
+
523
+ const cwd = await createDir({
524
+ name: 'scope-link-cwd',
525
+ under: root
526
+ })
527
+
528
+ const outsideTarget = await createFile({
529
+ name: 'scope-link-target'
530
+ })
531
+
532
+ const link = path.join(cwd, 'scope-link')
533
+ await fs.symlink(outsideTarget, link)
534
+
535
+ const result = await runRm(['scope-link'], {
536
+ cwd,
537
+ env: {
538
+ SAFE_RM_SCOPE: '.',
539
+ SAFE_RM_TRASH: trash
540
+ }
541
+ })
542
+
543
+ assertEmptySuccess(t, result)
544
+ t.false(await pathExists(link), 'symlink should be removed')
545
+ t.true(await pathExists(outsideTarget), 'symlink target should remain')
546
+ })
547
+
405
548
  !is_rm(type) && !is_as(type) && test(`-I only prompts for more than three files`, async t => {
406
549
  const {
407
550
  createFile,