#! /usr/bin/env bash

# Basic test suite for exacl tool (Darwin/macOS).

set -u -o pipefail

EXACL='../target/debug/examples/exacl'

ME=$(id -un)
ME_NUM=$(id -u)
MY_GROUP=$(id -gn)
MY_GROUP_NUM=$(id -g)

# Return true if file is readable.
isReadable() {
    cat "$1" >/dev/null 2>&1
    return $?
}

# Return true if file is writable (tries to overwrite file).
isWritable() {
    echo "x" 2>/dev/null >"$1"
    # shellcheck disable=SC2320
    return $?
}

# Return true if directory is readable.
isReadableDir() {
    ls "$1" >/dev/null 2>&1
    return $?
}

# Return true if link is readable.
isReadableLink() {
    readlink "$1" >/dev/null 2>&1
    return $?
}

# Put quotes back on JSON text.
quotifyJson() {
    echo "$1" | sed -E -e 's/([A-Za-z0-9_-]+)/"\1"/g' -e 's/:"false"/:false/g' -e 's/:"true"/:true/g'
}

getAcl() {
    # shellcheck disable=SC2010
    ls -le "$1" 2>/dev/null | grep -E '^ \d+: '
}

# Called by shunit2 before all tests run.
oneTimeSetUp() {
    # Use temp directory managed by shunit2.
    DIR="$SHUNIT_TMPDIR"
    FILE1="$DIR/file1"
    DIR1="$DIR/dir1"
    LINK1="$DIR/link1"
    LINK2="$DIR/link2"

    # Create empty file, dir, and links.
    umask 077
    touch "$FILE1"
    mkdir "$DIR1"
    ln -s link1_to_nowhere "$LINK1"
    ln -s file1 "$LINK2"
}

testReadAclFromMissingFile() {
    msg=$($EXACL $DIR/non_existant 2>&1)
    assertEquals 1 $?
    assertEquals \
        "File \"$DIR/non_existant\": No such file or directory (os error 2)" \
        "$msg"
}

testReadAclForFile1() {
    msg=$($EXACL $FILE1)
    assertEquals 0 $?
    assertEquals "[]" "$msg"

    isReadable "$FILE1" && isWritable "$FILE1"
    assertEquals 0 $?

    # Add ACL entry for current user to "deny read".
    chmod +a "$ME deny read" "$FILE1"

    msg=$($EXACL $FILE1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \
        "${msg//\"/}"

    ! isReadable "$FILE1" && isWritable "$FILE1"
    assertEquals 0 $?

    # Remove user write perm.
    chmod u-w "$FILE1"
    ! isReadable "$FILE1" && ! isWritable "$FILE1"
    assertEquals 0 $?

    # Add ACL entry for current group to "allow write".
    chmod +a "$MY_GROUP allow write" "$FILE1"

    msg=$($EXACL $FILE1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[],allow:false},{kind:group,name:$MY_GROUP,perms:[write],flags:[],allow:true}]" \
        "${msg//\"/}"

    ! isReadable "$FILE1" && isWritable "$FILE1"
    assertEquals 0 $?

    # Re-add user write perm that we removed above. Clear the ACL.
    chmod u+w "$FILE1"
    chmod -N "$FILE1"
}

testReadAclForDir1() {
    msg=$($EXACL $DIR1)
    assertEquals 0 $?
    assertEquals "[]" "$msg"

    # Add ACL entry for current user to "deny read" with inheritance flags.
    chmod +a "$ME deny read,file_inherit,directory_inherit,only_inherit" "$DIR1"

    msg=$($EXACL $DIR1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[file_inherit,directory_inherit,only_inherit],allow:false}]" \
        "${msg//\"/}"

    isReadableDir "$DIR1"
    assertEquals 0 $?

    # Create subfile in DIR1.
    subfile="$DIR1/subfile"
    touch "$subfile"

    ! isReadable "$subfile" && isWritable "$subfile"
    assertEquals 0 $?

    msg=$($EXACL $subfile)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[inherited],allow:false}]" \
        "${msg//\"/}"

    # Create subdirectory in DIR1.
    subdir="$DIR1/subdir"
    mkdir "$subdir"

    msg=$($EXACL $subdir)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[inherited,file_inherit,directory_inherit],allow:false}]" \
        "${msg//\"/}"

    # Clear directory ACL's so we can delete them.
    chmod -a# 0 "$subdir"
    chmod -a# 0 "$DIR1"

    rmdir "$subdir"
    rm "$subfile"
}

testReadAclForLink1() {
    # Test symlink that goes nowhere.
    msg=$($EXACL $LINK1 2>&1)
    assertEquals 1 $?
    assertEquals "File \"$LINK1\": No such file or directory (os error 2)" "$msg"

    # Test symlink with no ACL.
    msg=$($EXACL --symlink $LINK1)
    assertEquals 0 $?
    assertEquals "[]" "$msg"

    # Add ACL entry for current user to "deny read".
    chmod -h +a "$ME deny read" "$LINK1"
    assertEquals 0 $?

    ! isReadableLink "$LINK1"
    assertEquals 0 $?

    msg=$($EXACL --symlink $LINK1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \
        "${msg//\"/}"

    # It appears that you can't further modify the ACL of a symbolic link if
    # you don't have 'read' access to the link anymore.
    msg=$(chmod -h -a# 0 "$LINK1" 2>&1)
    assertEquals 1 $?
    assertEquals \
        "chmod: No ACL present '$LINK1'
chmod: Failed to set ACL on file '$LINK1': Permission denied" \
        "$msg"

    # Recreate the symlink here.
    ln -fs link1_to_nowhere "$LINK1"
}

testReadAclForLink2() {
    # Test symlink to file1.
    msg=$($EXACL $LINK2)
    assertEquals 0 $?
    assertEquals "[]" "$msg"

    # Add ACL entry for current user to "deny read".
    chmod +a "$ME deny read" "$LINK2"

    msg=$($EXACL $LINK2)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \
        "${msg//\"/}"
}

testWriteAclToMissingFile() {
    input="[]"
    msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1)
    assertEquals 1 $?
    assertEquals \
        "File \"$DIR/non_existant\": No such file or directory (os error 2)" \
        "$msg"
}

testWriteAclToFile1() {
    # Set ACL to empty.
    input="[]"
    msg=$(echo "$input" | $EXACL --set $FILE1 2>&1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    # Verify it's empty.
    msg=$($EXACL $FILE1)
    assertEquals 0 $?
    assertEquals "[]" "$msg"

    isReadable "$FILE1" && isWritable "$FILE1"
    assertEquals 0 $?

    # Set ACL for current user to "deny read".
    input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]")
    msg=$(echo "$input" | $EXACL --set $FILE1 2>&1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    ! isReadable "$FILE1" && isWritable "$FILE1"
    assertEquals 0 $?

    # Check ACL using ls.
    msg=$(getAcl $FILE1)
    assertEquals \
        " 0: user:$ME deny read" \
        "$msg"

    # Check ACL again.
    msg=$($EXACL $FILE1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \
        "${msg//\"/}"
}

testWriteAclToDir1() {
    # Set ACL to empty.
    input="[]"
    msg=$(echo "$input" | $EXACL --set $DIR1 2>&1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    # Verify it's empty.
    msg=$($EXACL $DIR1)
    assertEquals 0 $?
    assertEquals "[]" "$msg"

    isReadableDir "$DIR1"
    assertEquals 0 $?

    # Set ACL for current user to "deny read".
    input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]")
    msg=$(echo "$input" | $EXACL --set $DIR1 2>&1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    ! isReadable "$DIR1"
    assertEquals 0 $?

    # Read ACL back.
    msg=$($EXACL $DIR1)
    assertEquals 0 $?
    assertEquals "$input" "$msg"
}

testWriteAclToLink1() {
    # Set ACL to empty.
    input="[]"
    msg=$(echo "$input" | $EXACL --symlink --set $LINK1 2>&1)
    assertEquals 0 $?
    assertEquals \
        "" \
        "$msg"

    isReadableLink "$LINK1"
    assertEquals 0 $?

    # Set ACL for current user to "deny read".
    input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]")
    msg=$(echo "$input" | $EXACL --symlink --set $LINK1 2>&1)
    assertEquals 0 $?
    assertEquals \
        "" \
        "$msg"

    ! isReadableLink "$LINK1"
    assertEquals 0 $?

    # Check ACL using ls.
    msg=$(getAcl $LINK1)
    assertEquals \
        " 0: user:$ME deny read" \
        "$msg"

    # Check ACL again.
    msg=$($EXACL --symlink $LINK1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \
        "${msg//\"/}"

    # Set ACL back to empty. We've removed READ permission for the link, so
    # this will fail.
    input="[]"
    msg=$(echo "$input" | $EXACL --symlink --set $LINK1 2>&1)
    assertEquals 1 $?
    assertEquals \
        "File \"$LINK1\": Permission denied (os error 13)" \
        "$msg"
}

testWriteAllFilePerms() {
    all="read,write,execute,delete,append,delete_child,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,sync"
    input=$(quotifyJson "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]")
    msg=$(echo "$input" | $EXACL --set $FILE1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    msg=$($EXACL $FILE1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]" \
        "${msg//\"/}"

    # ls output omits delete_child and sync.
    ls_perms="read,write,execute,delete,append,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown"
    msg=$(getAcl $FILE1)
    assertEquals \
        " 0: user:$ME allow $ls_perms" \
        "$msg"
}

testWriteAllFileFlags() {
    entry_flags="inherited,file_inherit,directory_inherit,limit_inherit,only_inherit"
    all="$entry_flags"
    input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[$all],allow:true}]")
    msg=$(echo "$input" | $EXACL --set $FILE1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    # N.B. "defer_inherit" flag is not returned.
    msg=$($EXACL $FILE1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[$entry_flags],allow:true}]" \
        "${msg//\"/}"

    # ls output only shows inherited and limit_inherit.
    ls_perms="read,limit_inherit"
    msg=$(getAcl $FILE1)
    assertEquals \
        " 0: user:$ME inherited allow $ls_perms" \
        "$msg"
}

testWriteAllDirPerms() {
    all="read,write,execute,delete,append,delete_child,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,sync"
    input=$(quotifyJson "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]")
    msg=$(echo "$input" | $EXACL --set $DIR1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    msg=$($EXACL $DIR1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[$all],flags:[],allow:true}]" \
        "${msg//\"/}"
}

testWriteAllDirFlags() {
    entry_flags="inherited,file_inherit,directory_inherit,limit_inherit,only_inherit"
    all="$entry_flags"
    input=$(quotifyJson "[{kind:user,name:$ME,perms:[read],flags:[$all],allow:true}]")
    msg=$(echo "$input" | $EXACL --set $DIR1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    # N.B. "defer_inherit" flag is not returned.
    msg=$($EXACL $DIR1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[$entry_flags],allow:true}]" \
        "${msg//\"/}"
}

testWriteAclNumericUID() {
    # Set ACL for current user to "deny read".
    input=$(quotifyJson "[{kind:user,name:$ME_NUM,perms:[read],flags:[],allow:false}]")
    msg=$(echo "$input" | $EXACL --set $FILE1 2>&1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    ! isReadable "$FILE1" && isWritable "$FILE1"
    assertEquals 0 $?

    # Check ACL again.
    msg=$($EXACL $FILE1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$ME,perms:[read],flags:[],allow:false}]" \
        "${msg//\"/}"
}

testWriteAclNumericGID() {
    # Set ACL for current group to "deny read".
    input=$(quotifyJson "[{kind:group,name:$MY_GROUP_NUM,perms:[read],flags:[],allow:false}]")
    msg=$(echo "$input" | $EXACL --set $FILE1 2>&1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    ! isReadable "$FILE1" && isWritable "$FILE1"
    assertEquals 0 $?

    # Check ACL again.
    msg=$($EXACL $FILE1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:group,name:$MY_GROUP,perms:[read],flags:[],allow:false}]" \
        "${msg//\"/}"
}

testWriteAclGUID() {
    # Set ACL for _spotlight group to "deny read" using GUID.
    spotlight_group="ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000059"
    input=$(quotifyJson "[{kind:group,name:$spotlight_group,perms:[read],flags:[],allow:false}]")
    msg=$(echo "$input" | $EXACL --set $FILE1 2>&1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    # Check ACL again.
    msg=$($EXACL $FILE1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:group,name:_spotlight,perms:[read],flags:[],allow:false}]" \
        "${msg//\"/}"
}

testWriteAclGUID_nil() {
    # Set ACL for _spotlight group to "deny read" using GUID.
    nil_uuid="00000000-0000-0000-0000-000000000000"
    input=$(quotifyJson "[{kind:group,name:$nil_uuid,perms:[read],flags:[],allow:false}]")
    msg=$(echo "$input" | $EXACL --set $FILE1 2>&1)
    assertEquals 0 $?
    assertEquals "" "$msg"

    # Check ACL again. Note: change in kind.
    msg=$($EXACL $FILE1)
    assertEquals 0 $?
    assertEquals \
        "[{kind:user,name:$nil_uuid,perms:[read],flags:[],allow:false}]" \
        "${msg//\"/}"
}

testDefaultAclFails() {
    # Test that exacl returns an error; default acl not supported on macOS.
    msg=$($EXACL --default $DIR1 2>&1)
    assertEquals 1 $?
    assertEquals \
        "File \"$DIR1\": macOS does not support default ACL" \
        "$msg"

    msg=$(echo "[]" | $EXACL --set --default $DIR1 2>&1)
    assertEquals 1 $?
    assertEquals \
        "File \"$DIR1\": macOS does not support default ACL" \
        "$msg"
}

testMissingFlags() {
    input=$(quotifyJson "[{kind:user,name:501,perms:[execute],allow:true}]")
    msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1)
    assertEquals 1 $?
    assertEquals \
        "File \"$DIR/non_existant\": No such file or directory (os error 2)" \
        "${msg//\`/}"
}

testMissingAllow() {
    input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[]}]")
    msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1)
    assertEquals 1 $?
    assertEquals \
        "File \"$DIR/non_existant\": No such file or directory (os error 2)" \
        "${msg//\`/}"
}

# Duplicate entry is not an error on macOS.
testDuplicateEntry() {
    input=$(quotifyJson "[{kind:user,name:501,perms:[execute],flags:[]},{kind:user,name:501,perms:[execute],flags:[]}]")
    msg=$(echo "$input" | $EXACL --set $DIR/non_existant 2>&1)
    assertEquals 1 $?
    assertEquals \
        "File \"$DIR/non_existant\": No such file or directory (os error 2)" \
        "${msg//\`/}"
}

# shellcheck disable=SC1091
. shunit2
