From 971783a7fd2790c9c62d646f3c1ecca3af3fd2c8 Mon Sep 17 00:00:00 2001
From: Jordan Borean <jborean93@gmail.com>
Date: Fri, 27 Jan 2017 23:51:12 +0100
Subject: [PATCH] Feature/win stat extra info (#19148)

* Added more return results to win_stat

* Changed Win2012 methods to support older versions in setup

* staging of the tests to work with older servers
---
 lib/ansible/module_utils/powershell.ps1       |  24 +-
 lib/ansible/modules/windows/win_stat.ps1      | 170 ++++-
 lib/ansible/modules/windows/win_stat.py       | 183 ++++-
 .../targets/win_stat/defaults/main.yml        |   1 +
 .../targets/win_stat/files/set_attributes.ps1 |   9 +
 .../targets/win_stat/files/set_filedate.ps1   |   6 +
 .../targets/win_stat/files/setup_share.ps1    |   6 +
 .../targets/win_stat/tasks/main.yml           | 634 ++++++++++++++++--
 8 files changed, 925 insertions(+), 108 deletions(-)
 create mode 100644 test/integration/targets/win_stat/defaults/main.yml
 create mode 100644 test/integration/targets/win_stat/files/set_attributes.ps1
 create mode 100644 test/integration/targets/win_stat/files/set_filedate.ps1
 create mode 100644 test/integration/targets/win_stat/files/setup_share.ps1

diff --git a/lib/ansible/module_utils/powershell.ps1 b/lib/ansible/module_utils/powershell.ps1
index 55aaab18472..3c58b210d82 100644
--- a/lib/ansible/module_utils/powershell.ps1
+++ b/lib/ansible/module_utils/powershell.ps1
@@ -225,14 +225,28 @@ Function Parse-Args($arguments, $supports_check_mode = $false)
 
 # Helper function to calculate a hash of a file in a way which powershell 3
 # and above can handle:
-Function Get-FileChecksum($path)
+Function Get-FileChecksum($path, $algorithm = 'sha1')
 {
     If (Test-Path -PathType Leaf $path)
     {
-        $sp = new-object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider;
-        $fp = [System.IO.File]::Open($path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite);
-        $hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower();
-        $fp.Dispose();
+        switch ($algorithm)
+        {
+            'md5' { $sp = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
+            'sha1' { $sp = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
+            'sha256' { $sp = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
+            'sha384' { $sp = New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
+            'sha512' { $sp = New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
+            default { Fail-Json (New-Object PSObject) "Unsupported hash algorithm supplied '$algorithm'" }
+        }
+
+        If ($PSVersionTable.PSVersion.Major -ge 4) {
+            $raw_hash = Get-FileHash $path -Algorithm $algorithm
+            $hash = $raw_hash.Hash.ToLower()
+        } Else {
+            $fp = [System.IO.File]::Open($path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite);
+            $hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower();
+            $fp.Dispose();
+        }
     }
     ElseIf (Test-Path -PathType Container $path)
     {
diff --git a/lib/ansible/modules/windows/win_stat.ps1 b/lib/ansible/modules/windows/win_stat.ps1
index 366652249b6..300cf531f25 100644
--- a/lib/ansible/modules/windows/win_stat.ps1
+++ b/lib/ansible/modules/windows/win_stat.ps1
@@ -17,7 +17,51 @@
 # WANT_JSON
 # POWERSHELL_COMMON
 
-$params = Parse-Args $args $true;
+$params = Parse-Args -arguments $args -supports_check_mode $true;
+
+# C# code to determine link target, copied from http://chrisbensen.blogspot.com.au/2010/06/getfinalpathnamebyhandle.html
+$symlink_util = @"
+using System;
+using System.Text;
+using Microsoft.Win32.SafeHandles;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+
+namespace Ansible.Command
+{
+    public class SymLinkHelper
+    {
+        private const int FILE_SHARE_WRITE = 2;
+        private const int CREATION_DISPOSITION_OPEN_EXISTING = 3;
+        private const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
+
+        [DllImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Unicode, SetLastError = true)]
+        public static extern int GetFinalPathNameByHandle(IntPtr handle, [In, Out] StringBuilder path, int bufLen, int flags);
+
+        [DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)] 
+        public static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, 
+        int dwShareMode, IntPtr SecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
+
+        public static string GetSymbolicLinkTarget(System.IO.DirectoryInfo symlink) 
+        { 
+            SafeFileHandle directoryHandle = CreateFile(symlink.FullName, 0, 2, System.IntPtr.Zero, CREATION_DISPOSITION_OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, System.IntPtr.Zero);
+            if(directoryHandle.IsInvalid)
+                throw new Win32Exception(Marshal.GetLastWin32Error());
+
+            StringBuilder path = new StringBuilder(512);
+            int size = GetFinalPathNameByHandle(directoryHandle.DangerousGetHandle(), path, path.Capacity, 0);
+
+            if (size<0)
+                throw new Win32Exception(Marshal.GetLastWin32Error()); // The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\" // More information about "\\?\" here -> http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx
+            if (path[0] == '\\' && path[1] == '\\' && path[2] == '?' && path[3] == '\\')
+                return path.ToString().Substring(4); 
+            else 
+                return path.ToString(); 
+        }
+    }
+}
+"@
+Add-Type -TypeDefinition $symlink_util
 
 function Date_To_Timestamp($start_date, $end_date)
 {
@@ -27,15 +71,26 @@ function Date_To_Timestamp($start_date, $end_date)
     }
 }
 
-$path = Get-Attr $params "path" $FALSE;
-If ($path -eq $FALSE)
-{
-    Fail-Json (New-Object psobject) "missing required argument: path";
+function Get-Hash($path, $algorithm) {
+    # Using PowerShell V4 and above we can use some powershell cmdlets instead of .net
+    If ($PSVersionTable.PSVersion.Major -ge 4)
+    {
+        $hash = (Get-FileHash $path -Algorithm $algorithm).Hash
+    }
+    Else
+    {
+        $net_algorithm = [Security.Cryptography.HashAlgorithm]::Create($algorithm)
+        $raw_hash = [System.BitConverter]::ToString($net_algorithm.ComputeHash([System.IO.File]::ReadAllBytes($path)))
+        $hash = $raw_hash -replace '-'
+    }
+
+    $hash.ToLower()
 }
 
-$get_md5 = Get-Attr $params "get_md5" $TRUE | ConvertTo-Bool;
-# until we support real aliasing, get the default value from get_md5
-$get_checksum = Get-Attr $params "get_checksum" $get_md5 | ConvertTo-Bool;
+$path = Get-AnsibleParam -obj $params -name 'path' -failifempty $true;
+$get_md5 = Get-AnsibleParam -obj $params -name 'get_md5' -failifempty $false -default $true | ConvertTo-Bool;
+$get_checksum = Get-AnsibleParam -obj $params -name 'get_checksum' -failifempty $false -default $true | ConvertTo-Bool;
+$checksum_algorithm = Get-AnsibleParam -obj $params -name 'checksum_algorithm' -failifempty $false -default 'sha1' -ValidateSet 'sha1','sha256','sha384','sha512'
 
 $result = New-Object psobject @{
     stat = New-Object psobject
@@ -46,11 +101,20 @@ If (Test-Path $path)
 {
     Set-Attr $result.stat "exists" $TRUE;
 
-    $info = Get-Item $path;
-    $iscontainer = Get-Attr $info "PSIsContainer" $null;
-    $length = Get-Attr $info "Length" $null;
-    $extension = Get-Attr $info "Extension" $null;
-    $attributes = Get-Attr $info "Attributes" "";
+    # Need to use -Force so it picks up hidden files
+    $info = Get-Item -Force $path;
+    $iscontainer = $info.PSIsContainer;
+    $filename = $info.Name;
+    $filepath = $info.FullName;
+    $attributes = @()
+    foreach ($attribute in ($info.Attributes -split ',')) {
+        $attributes += $attribute.Trim();
+    }
+    $attributes_string = $info.Attributes.ToString();
+    $isreadonly = $attributes -contains 'ReadOnly';
+    $ishidden = $attributes -contains 'Hidden';
+    $isarchive = $attributes -contains 'Archive';
+
     If ($info)
     {
         $accesscontrol = $info.GetAccessControl();
@@ -59,26 +123,72 @@ If (Test-Path $path)
     {
         $accesscontrol = $null;
     }
-    $owner = Get-Attr $accesscontrol "Owner" $null;
-    $creationtime = Get-Attr $info "CreationTime" $null;
-    $lastaccesstime = Get-Attr $info "LastAccessTime" $null;
-    $lastwritetime = Get-Attr $info "LastWriteTime" $null;
-
+    $owner = $accessControl.Owner;
+    $creationtime = $info.CreationTime;
+    $lastaccesstime = $info.LastAccessTime;
+    $lastwritetime = $info.LastAccessTime;
 
     $epoch_date = Get-Date -Date "01/01/1970"
-    If ($iscontainer)
+    $islink = $false
+    $isdir = $false
+    $isshared = $false
+
+    If ($attributes -contains 'ReparsePoint')
     {
-        Set-Attr $result.stat "isdir" $TRUE;
+        # TODO: Find a way to differenciate between soft and junction links
+        $islink = $true
+        $isdir = $true
+        # Try and get the symlink source, can result in failure if link is broken
+        try {
+            $lnk_source = [Ansible.Command.SymLinkHelper]::GetSymbolicLinkTarget($path)
+            Set-Attr $result.stat "lnk_source" $lnk_source
+        } catch {}
+    } 
+    ElseIf ($iscontainer)
+    {
+        $isdir = $true
+
+        $share_info = Get-WmiObject -Class Win32_Share -Filter "Path='$($info.Fullname -replace '\\', '\\')'";
+        If ($share_info -ne $null) 
+        {
+            $isshared = $true
+            Set-Attr $result.stat "sharename" $share_info.Name;
+        }
+
+        $dir_files_sum = Get-ChildItem $info.FullName -Recurse | Measure-Object -property length -sum;
+        If ($dir_files_sum -eq $null)
+        {
+            Set-Attr $result.stat "size" 0;
+        }
+        Else{
+            Set-Attr $result.stat "size" $dir_files_sum.Sum;
+        }
     }
     Else
     {
-        Set-Attr $result.stat "isdir" $FALSE;
-        Set-Attr $result.stat "size" $length;
+        Set-Attr $result.stat "size" $info.Length;
+        Set-Attr $result.stat "extension" $info.extension;
+
+        If ($get_md5) {
+            $md5 = Get-Hash -path $path -algorithm 'md5'
+            Set-Attr $result.stat "md5" $md5
+        }
+
+        If ($get_checksum) {
+            $checksum = Get-Hash -path $path -algorithm $checksum_algorithm
+            Set-Attr $result.stat "checksum" $checksum
+        }
     }
-    Set-Attr $result.stat "extension" $extension;
-    Set-Attr $result.stat "attributes" $attributes.ToString();
-    # Set-Attr $result.stat "owner" $getaccesscontrol.Owner;
-    # Set-Attr $result.stat "owner" $info.GetAccessControl().Owner;
+
+    Set-Attr $result.stat "islink" $islink;
+    Set-Attr $result.stat "isdir" $isdir;
+    Set-Attr $result.stat "isshared" $isshared;
+    Set-Attr $result.stat "isreadonly" $isreadonly;
+    Set-Attr $result.stat "ishidden" $ishidden;
+    Set-Attr $result.stat "isarchive" $isarchive;
+    Set-Attr $result.stat "filename" $filename;
+    Set-Attr $result.stat "path" $filepath;
+    Set-Attr $result.stat "attributes" $attributes_string;
     Set-Attr $result.stat "owner" $owner;
     Set-Attr $result.stat "creationtime" (Date_To_Timestamp $epoch_date $creationtime);
     Set-Attr $result.stat "lastaccesstime" (Date_To_Timestamp $epoch_date $lastaccesstime);
@@ -89,12 +199,4 @@ Else
     Set-Attr $result.stat "exists" $FALSE;
 }
 
-# only check get_checksum- it either got its value from get_md5 or was set directly.
-If (($get_checksum) -and $result.stat.exists -and -not $result.stat.isdir)
-{
-    $hash = Get-FileChecksum($path);
-    Set-Attr $result.stat "md5" $hash;
-    Set-Attr $result.stat "checksum" $hash;
-}
-
 Exit-Json $result;
diff --git a/lib/ansible/modules/windows/win_stat.py b/lib/ansible/modules/windows/win_stat.py
index ce874871d35..73c8e436839 100644
--- a/lib/ansible/modules/windows/win_stat.py
+++ b/lib/ansible/modules/windows/win_stat.py
@@ -29,27 +29,33 @@ short_description: returns information about a Windows file
 description:
      - Returns information about a Windows file
 options:
-  path:
-    description:
-      - The full path of the file/object to get the facts of; both forward and
-        back slashes are accepted.
-    required: true
-    default: null
-    aliases: []
-  get_md5:
-    description:
-      - Whether to return the checksum sum of the file. As of Ansible 1.9 this
-        is no longer a MD5, but a SHA1 instead.
-    required: false
-    default: yes
-    aliases: []
-  get_checksum:
-    description:
-      - Whether to return a checksum of the file
-        (only sha1 currently supported)
-    required: false
-    default: yes
-    version_added: "2.1"
+    path:
+        description:
+            - The full path of the file/object to get the facts of; both forward and
+              back slashes are accepted.
+        required: yes
+    get_md5:
+        description:
+            - Whether to return the checksum sum of the file. Between Ansible 1.9
+              and 2.2 this is no longer an MD5, but a SHA1 isntead. As of Ansible
+              2.3 this is back to an MD5. Will return None if host is unable to
+              use specified algorithm
+        required: no
+        default: True
+    get_checksum:
+        description:
+            - Whether to return a checksum of the file (default sha1)
+        required: no
+        default: True
+        version_added: "2.1"
+    checksum_algorithm:
+        description:
+            - Algorithm to determine checksum of file. Will throw an error if
+              the host is unable to use specified algorithm.
+        required: no
+        default: sha1
+        choices: ['sha1', 'sha256', 'sha384', 'sha512']
+        version_added: "2.3"
 author: "Chris Church (@cchurch)"
 '''
 
@@ -59,7 +65,140 @@ EXAMPLES = r'''
     path: C:\foo.ini
   register: file_info
 
+# Obtain information about a folder
+- win_stat:
+    path: C:\\bar
+  register: folder_info
+
+# Get MD5 checksum of a file
+- win_stat:
+    path: C:\\foo.ini
+    get_md5: True
+  register: md5_checksum
+
 - debug:
-    var: file_info
+    var: md5_checksum.stat.md5
+
+# Get SHA1 checksum of file
+- win_stat:
+    path: C:\\foo.ini
+    get_checksum: True
+  register: sha1_checksum
+
+- debug:
+    var: sha1_checksum.stat.checksum
+
+# Get SHA256 checksum of file
+- win_stat:
+    path: C:\\foo.ini
+    get_checksum: True
+    checksum_algorithm: sha256
+  register: sha256_checksum
+
+- debug:
+    var: sha256_checksum.stat.checksum
 '''
 
+RETURN = '''
+changed:
+    description: Whether anything was changed
+    returned: always
+    type: boolean
+    sample: True
+stat:
+    description: dictionary containing all the stat data
+    returned: success
+    type: dictionary
+    contains:
+        attributes:
+            description: attributes of the file at path in raw form
+            returned: success, path exists
+            type: string
+            sample: "Archive, Hidden"
+        checksum:
+            description: The checksum of a file based on checksum_algorithm specified
+            returned: success, path exist, path is a file, get_checksum == True
+              checksum_algorithm specified is supported
+            type: string
+            sample: 09cb79e8fc7453c84a07f644e441fd81623b7f98
+        creationtime:
+            description: the create time of the file represented in seconds since epoch
+            returned: success, path exists
+            type: float
+            sample: 1477984205.15
+        extension:
+            description: the extension of the file at path
+            returned: success, path exists, path is a file
+            type: string
+            sample: ".ps1"
+        isarchive:
+            description: if the path is ready for archiving or not
+            returned: success, path exists
+            type: boolean
+            sample: True
+        isdir:
+            description: if the path is a directory or not
+            returned: success, path exists
+            type: boolean
+            sample: True
+        ishidden:
+            description: if the path is hidden or not
+            returned: success, path exists
+            type: boolean
+            sample: True
+        islink:
+            description: if the path is a symbolic link or junction or not
+            returned: success, path exists
+            type: boolean
+            sample: True
+        isreadonly:
+            description: if the path is read only or not
+            returned: success, path exists
+            type: boolean
+            sample: True
+        isshared:
+            description: if the path is shared or not
+            returned: success, path exists
+            type: boolean
+            sample: True
+        lastaccesstime:
+            description: the last access time of the file represented in seconds since epoch
+            returned: success, path exists
+            type: float
+            sample: 1477984205.15
+        lastwritetime:
+            description: the last modification time of the file represented in seconds since epoch
+            returned: success, path exists
+            type: float
+            sample: 1477984205.15
+        lnk_source:
+            description: the target of the symbolic link, will return null if not a link or the link is broken
+            return: success, path exists, file is a symbolic link
+            type: string
+            sample: C:\\temp
+        md5:
+            description: The MD5 checksum of a file (Between Ansible 1.9 and 2.2 this was returned as a SHA1 hash)
+            returned: success, path exist, path is a file, get_md5 == True, md5 is supported
+            type: string
+            sample: 09cb79e8fc7453c84a07f644e441fd81623b7f98
+        owner:
+            description: the owner of the file
+            returned: success, path exists
+            type: string
+            sample: BUILTIN\\Administrators
+        path:
+            description: the full absolute path to the file
+            returned: success, path exists
+            type: string
+            sample: BUILTIN\\Administrators
+        sharename:
+            description: the name of share if folder is shared 
+            returned: success, path exists, file is a directory and isshared == True
+            type: string
+            sample: file-share
+        size:
+            description: the size in bytes of a file or folder
+            returned: success, path exists, file is not a link
+            type: int
+            sample: 1024
+'''
diff --git a/test/integration/targets/win_stat/defaults/main.yml b/test/integration/targets/win_stat/defaults/main.yml
new file mode 100644
index 00000000000..121d9468b58
--- /dev/null
+++ b/test/integration/targets/win_stat/defaults/main.yml
@@ -0,0 +1 @@
+win_stat_dir: "{{win_output_dir}}\\win_stat"
\ No newline at end of file
diff --git a/test/integration/targets/win_stat/files/set_attributes.ps1 b/test/integration/targets/win_stat/files/set_attributes.ps1
new file mode 100644
index 00000000000..b57a368c438
--- /dev/null
+++ b/test/integration/targets/win_stat/files/set_attributes.ps1
@@ -0,0 +1,9 @@
+$path = $args[0]
+$attr = $args[1]
+$item = Get-Item "$path"
+
+$attributes = $item.Attributes -split ','
+If ($attributes -notcontains $attr) {
+    $attributes += $attr
+}
+$item.Attributes = $attributes -join ','
diff --git a/test/integration/targets/win_stat/files/set_filedate.ps1 b/test/integration/targets/win_stat/files/set_filedate.ps1
new file mode 100644
index 00000000000..3bc525a4ff0
--- /dev/null
+++ b/test/integration/targets/win_stat/files/set_filedate.ps1
@@ -0,0 +1,6 @@
+$date = Get-Date -Year 2016 -Month 11 -Day 1 -Hour 7 -Minute 10 -Second 5 -Millisecond 0
+
+$item = Get-Item -Path "$($args[0])"
+$item.CreationTime = $date
+$item.LastAccessTime = $date
+$item.LastWriteTime = $date
diff --git a/test/integration/targets/win_stat/files/setup_share.ps1 b/test/integration/targets/win_stat/files/setup_share.ps1
new file mode 100644
index 00000000000..a024c2855f5
--- /dev/null
+++ b/test/integration/targets/win_stat/files/setup_share.ps1
@@ -0,0 +1,6 @@
+$share_stat = Get-WmiObject -Class Win32_Share -Filter "name='folder-share'"
+If ($share_stat) {
+    $share_stat.Delete()
+}
+$wmi = [wmiClass] 'Win32_Share'
+$wmi.Create($args[0], 'folder-share', 0)
diff --git a/test/integration/targets/win_stat/tasks/main.yml b/test/integration/targets/win_stat/tasks/main.yml
index 56553d4d755..d80a01785f9 100644
--- a/test/integration/targets/win_stat/tasks/main.yml
+++ b/test/integration/targets/win_stat/tasks/main.yml
@@ -16,70 +16,587 @@
 # You should have received a copy of the GNU General Public License
 # along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 
+- name: remove links if they exist as win_file struggles
+  win_command: cmd.exe /c rmdir "{{item}}"
+  ignore_errors: True
+  with_items:
+  - "{{win_stat_dir}}\\link"
+  - "{{win_stat_dir}}\\nested\\hard-link.ps1"
+  - "{{win_stat_dir}}\\junction-link"
+
+- name: ensure the testing directory is cleared before the run
+  win_file:
+    path: "{{win_stat_dir}}"
+    state: absent
+
+- name: ensure win testing directories exist
+  win_file:
+    path: "{{item}}"
+    state: directory
+  with_items:
+  - "{{win_stat_dir}}\\folder"
+  - "{{win_stat_dir}}\\folder space"
+  - "{{win_stat_dir}}\\nested"
+  - "{{win_stat_dir}}\\nested\\nested"
+  - "{{win_stat_dir}}\\shared"
+  - "{{win_stat_dir}}\\hidden"
+  - "{{win_stat_dir}}\\link-dest"
+  - "{{win_stat_dir}}\\junction-dest"
+
+- name: create empty test files
+  win_file:
+    path: "{{item}}"
+    state: touch
+  with_items:
+  - "{{win_stat_dir}}\\nested\\file.ps1"
+  - "{{win_stat_dir}}\\nested\\read-only.ps1"
+  - "{{win_stat_dir}}\\nested\\archive.ps1"
+  - "{{win_stat_dir}}\\nested\\hidden.ps1"
+  - "{{win_stat_dir}}\\nested\\nested\\file.ps1"
+  - "{{win_stat_dir}}\\folder space\\file.ps1"
+
+- name: populate files with a test string
+  win_lineinfile:
+    dest: "{{item}}"
+    line: abc
+  with_items:
+  - "{{win_stat_dir}}\\nested\\file.ps1"
+  - "{{win_stat_dir}}\\nested\\read-only.ps1"
+  - "{{win_stat_dir}}\\nested\\archive.ps1"
+  - "{{win_stat_dir}}\\nested\\hidden.ps1"
+  - "{{win_stat_dir}}\\nested\\nested\\file.ps1"
+  - "{{win_stat_dir}}\\folder space\\file.ps1"
+
+- name: create share
+  script: setup_share.ps1 {{win_stat_dir}}\shared
+
+- name: create links
+  win_command: cmd.exe /c mklink /{{item.type}} {{item.source}} {{item.target}}
+  with_items:
+  - { type: 'D', source: "{{win_stat_dir}}\\link", target: "{{win_stat_dir}}\\link-dest" }
+  - { type: 'H', source: "{{win_stat_dir}}\\nested\\hard-link.ps1", target: "{{win_stat_dir}}\\nested\\file.ps1" }
+  - { type: 'J', source: "{{win_stat_dir}}\\junction-link", target: "{{win_stat_dir}}\\junction-dest" }
+
+- name: set modification date on files/folders
+  script: set_filedate.ps1 "{{item}}"
+  with_items:
+  - "{{win_stat_dir}}\\nested\\file.ps1"
+  - "{{win_stat_dir}}\\nested\\read-only.ps1"
+  - "{{win_stat_dir}}\\nested\\archive.ps1"
+  - "{{win_stat_dir}}\\nested\\hidden.ps1"
+  - "{{win_stat_dir}}\\nested\\nested\\file.ps1"
+  - "{{win_stat_dir}}\\folder space\\file.ps1"
+  - "{{win_stat_dir}}\\folder"
+  - "{{win_stat_dir}}\\folder space"
+  - "{{win_stat_dir}}\\nested"
+  - "{{win_stat_dir}}\\nested\\nested"
+  - "{{win_stat_dir}}\\shared"
+  - "{{win_stat_dir}}\\hidden"
+  - "{{win_stat_dir}}\\link-dest"
+  - "{{win_stat_dir}}\\junction-dest"
+
+- name: set file attributes for test
+  script: set_attributes.ps1 {{item.path}} {{item.attr}}
+  with_items:
+  - { path: "{{win_stat_dir}}\\hidden", attr: "Hidden" }
+  - { path: "{{win_stat_dir}}\\nested\\read-only.ps1", attr: "ReadOnly" }
+  - { path: "{{win_stat_dir}}\\nested\\archive.ps1", attr: "Archive" }
+  - { path: "{{win_stat_dir}}\\nested\\hidden.ps1", attr: "Hidden" }
+# End setup of test files
+
 - name: test win_stat module on file
-  win_stat: path="C:/Windows/win.ini"
-  register: win_stat_file
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\nested\\file.ps1"
+  register: actual
 
-- name: check win_stat file result
+- name: set expected fact for file
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: Archive
+        checksum: 5943831380243c1269c8b68bb92de6e2f1844590
+        creationtime: 1477984205
+        exists: True
+        extension: .ps1
+        filename: file.ps1
+        isarchive: True
+        isdir: False
+        ishidden: False
+        islink: False
+        isreadonly: False
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        md5: c627105fad747457b7e88ed1016e065f
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\nested\\file.ps1"
+        size: 8
+
+- name: check actual for file
   assert:
     that:
-      - "win_stat_file.stat.exists"
-      - "not win_stat_file.stat.isdir"
-      - "win_stat_file.stat.size > 0"
-      - "win_stat_file.stat.md5"
-      - "win_stat_file.stat.extension"
-      - "win_stat_file.stat.attributes"
-      - "win_stat_file.stat.owner"
-      - "win_stat_file.stat.creationtime"
-      - "win_stat_file.stat.lastaccesstime"
-      - "win_stat_file.stat.lastwritetime"
-      - "not win_stat_file|failed"
-      - "not win_stat_file|changed"
+    - "actual == expected"
 
-- name: test win_stat module on file without md5 and backslashes
-  win_stat: path="C:\Windows\win.ini" get_md5=no
-  register: win_stat_file_no_md5
+- name: test win_stat module on file without md5
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\nested\\file.ps1"
+    get_md5: False
+  register: actual
 
-- name: check win_stat file result without md5
+- name: set expected fact for file without md5
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: Archive
+        checksum: 5943831380243c1269c8b68bb92de6e2f1844590
+        creationtime: 1477984205
+        exists: True
+        extension: .ps1
+        filename: file.ps1
+        isarchive: True
+        isdir: False
+        ishidden: False
+        islink: False
+        isreadonly: False
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\nested\\file.ps1"
+        size: 8
+
+- name: check actual for file without md5
   assert:
     that:
-      - "win_stat_file_no_md5.stat.exists"
-      - "not win_stat_file_no_md5.stat.isdir"
-      - "win_stat_file_no_md5.stat.size > 0"
-      - "not win_stat_file_no_md5.stat.md5|default('')"
-      - "win_stat_file_no_md5.stat.extension"
-      - "win_stat_file_no_md5.stat.attributes"
-      - "win_stat_file_no_md5.stat.owner"
-      - "win_stat_file_no_md5.stat.creationtime"
-      - "win_stat_file_no_md5.stat.lastaccesstime"
-      - "win_stat_file_no_md5.stat.lastwritetime"
-      - "not win_stat_file_no_md5|failed"
-      - "not win_stat_file_no_md5|changed"
+    - "actual == expected"
 
-- name: test win_stat module on directory
-  win_stat: path="C:\\Windows"
-  register: win_stat_dir
+- name: test win_stat module on file with sha256
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\nested\\file.ps1"
+    checksum_algorithm: sha256
+  register: actual
 
-- name: check win_stat dir result
+- name: set expected fact for file with sha256
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: Archive
+        checksum: aec4cd5ed4c05ed5cf9dea5a8b30f5e655ad87b937b36e3c059840ce3cf3642f
+        creationtime: 1477984205
+        exists: True
+        extension: .ps1
+        filename: file.ps1
+        isarchive: True
+        isdir: False
+        ishidden: False
+        islink: False
+        isreadonly: False
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        md5: c627105fad747457b7e88ed1016e065f
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\nested\\file.ps1"
+        size: 8
+
+- name: check actual for file with sha256
   assert:
     that:
-      - "win_stat_dir.stat.exists"
-      - "win_stat_dir.stat.isdir"
-      - "win_stat_dir.stat.extension == ''"
-      - "win_stat_dir.stat.attributes"
-      - "win_stat_dir.stat.owner"
-      - "win_stat_dir.stat.creationtime"
-      - "win_stat_dir.stat.lastaccesstime"
-      - "win_stat_dir.stat.lastwritetime"
-      - "not win_stat_dir|failed"
-      - "not win_stat_dir|changed"
+    - "actual == expected"
+
+- name: test win_stat module on file with sha384
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\nested\\file.ps1"
+    checksum_algorithm: sha384
+  register: actual
+
+- name: set expected fact for file with sha384
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: Archive
+        checksum: 862076ed4ef117cb61395daa839ccfe20a880e8cd048e76b3b1503d39d72f4c09d58ab3c44cdd5a3272ca8da9d8d8666
+        creationtime: 1477984205
+        exists: True
+        extension: .ps1
+        filename: file.ps1
+        isarchive: True
+        isdir: False
+        ishidden: False
+        islink: False
+        isreadonly: False
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        md5: c627105fad747457b7e88ed1016e065f
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\nested\\file.ps1"
+        size: 8
+
+- name: check actual for file with sha384
+  assert:
+    that:
+    - "actual == expected"
+
+- name: test win_stat module on file with sha512
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\nested\\file.ps1"
+    checksum_algorithm: sha512
+  register: actual
+
+- name: set expected fact for file with sha512
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: Archive
+        checksum: 6d821d068cd8f09582de9ef14550666547eb6d4ae1d34cfc5aea921e595dbb9c462bf7f4c88a90190a69e52d69ac157e8146b555ed679a3e997061ca69632d0f
+        creationtime: 1477984205
+        exists: True
+        extension: .ps1
+        filename: file.ps1
+        isarchive: True
+        isdir: False
+        ishidden: False
+        islink: False
+        isreadonly: False
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        md5: c627105fad747457b7e88ed1016e065f
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\nested\\file.ps1"
+        size: 8
+
+- name: check actual for file with sha512
+  assert:
+    that:
+    - "actual == expected"
+
+- name: test win_stat on hidden file
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\nested\\hidden.ps1"
+  register: actual
+
+- name: set expected fact for hidden file
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: "Hidden, Archive"
+        checksum: 5943831380243c1269c8b68bb92de6e2f1844590
+        creationtime: 1477984205
+        exists: True
+        extension: .ps1
+        filename: hidden.ps1
+        isarchive: True
+        isdir: False
+        ishidden: True
+        islink: False
+        isreadonly: False
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        md5: c627105fad747457b7e88ed1016e065f
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\nested\\hidden.ps1"
+        size: 8
+
+- name: check actual for hidden file
+  assert:
+    that:
+    - "actual == expected"
+
+- name: test win_stat on readonly file
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\nested\\read-only.ps1"
+  register: actual
+
+- name: set expected fact for readonly file
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: "ReadOnly, Archive"
+        checksum: 5943831380243c1269c8b68bb92de6e2f1844590
+        creationtime: 1477984205
+        exists: True
+        extension: .ps1
+        filename: read-only.ps1
+        isarchive: True
+        isdir: False
+        ishidden: False
+        islink: False
+        isreadonly: True
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        md5: c627105fad747457b7e88ed1016e065f
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\nested\\read-only.ps1"
+        size: 8
+
+- name: check actual for readonly file
+  assert:
+    that:
+    - "actual == expected"
+
+- name: test win_stat on hard link file
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\nested\\hard-link.ps1"
+  register: actual
+
+- name: set expected fact for hard link file
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: Archive
+        checksum: 5943831380243c1269c8b68bb92de6e2f1844590
+        creationtime: 1477984205
+        exists: True
+        extension: .ps1
+        filename: hard-link.ps1
+        isarchive: True
+        isdir: False
+        ishidden: False
+        islink: False
+        isreadonly: False
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        md5: c627105fad747457b7e88ed1016e065f
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\nested\\hard-link.ps1"
+        size: 8
+
+- name: check actual for hard link file
+  assert:
+    that:
+    - "actual == expected"
+
+- name: test win_stat on directory
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\nested"
+  register: actual
+
+- name: set expected fact for directory
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: Directory
+        creationtime: 1477984205
+        exists: True
+        filename: nested
+        isarchive: False
+        isdir: True
+        ishidden: False
+        islink: False
+        isreadonly: False
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\nested"
+        size: 40
+
+- name: check actual for directory
+  assert:
+    that:
+    - "actual == expected"
+
+- name: test win_stat on empty directory
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\folder"
+  register: actual
+
+- name: set expected fact for empty directory
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: Directory
+        creationtime: 1477984205
+        exists: True
+        filename: folder
+        isarchive: False
+        isdir: True
+        ishidden: False
+        islink: False
+        isreadonly: False
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\folder"
+        size: 0
+
+- name: check actual for empty directory
+  assert:
+    that:
+    - "actual == expected"
+
+- name: test win_stat on directory with space in name
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\folder space"
+  register: actual
+
+- name: set expected fact for directory with space in name
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: Directory
+        creationtime: 1477984205
+        exists: True
+        filename: folder space
+        isarchive: False
+        isdir: True
+        ishidden: False
+        islink: False
+        isreadonly: False
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\folder space"
+        size: 8
+
+- name: check actual for directory with space in name
+  assert:
+    that:
+    - "actual == expected"
+
+- name: test win_stat on hidden directory
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\hidden"
+  register: actual
+
+- name: set expected fact for hidden directory
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: "Hidden, Directory"
+        creationtime: 1477984205
+        exists: True
+        filename: hidden
+        isarchive: False
+        isdir: True
+        ishidden: True
+        islink: False
+        isreadonly: False
+        isshared: False
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\hidden"
+        size: 0
+
+- name: check actual for hidden directory
+  assert:
+    that:
+    - "actual == expected"
+
+- name: test win_stat on shared directory
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\shared"
+  register: actual
+
+- name: set expected fact for shared directory
+  set_fact:
+    expected:
+      changed: False
+      stat:
+        attributes: Directory
+        creationtime: 1477984205
+        exists: True
+        filename: shared
+        isarchive: False
+        isdir: True
+        ishidden: False
+        islink: False
+        isreadonly: False
+        isshared: True
+        lastaccesstime: 1477984205
+        lastwritetime: 1477984205
+        owner: BUILTIN\Administrators
+        path: "{{win_output_dir}}\\win_stat\\shared"
+        sharename: folder-share
+        size: 0
+
+- name: check actual for shared directory
+  assert:
+    that:
+    - "actual == expected"
+
+- name: test win_stat on symlink
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\link"
+  register: actual
+
+# I cannot change modification time on links so we are resorting to checking the dict against known valuea
+- name: assert symlink actual
+  assert:
+    that:
+    - "actual.stat.attributes == 'Directory, ReparsePoint'"
+    - "actual.stat.creationtime is defined"
+    - "actual.stat.exists == True"
+    - "actual.stat.filename == 'link'"
+    - "actual.stat.isarchive == False"
+    - "actual.stat.isdir == True"
+    - "actual.stat.ishidden == False"
+    - "actual.stat.islink == True"
+    - "actual.stat.isreadonly == False"
+    - "actual.stat.isshared == False"
+    - "actual.stat.lastaccesstime is defined"
+    - "actual.stat.lastwritetime is defined"
+    - "actual.stat.lnk_source == '{{win_output_dir|regex_replace('\\\\', '\\\\\\\\')}}\\\\win_stat\\\\link-dest'"
+    - "actual.stat.owner == 'BUILTIN\\Administrators'"
+    - "actual.stat.path == '{{win_output_dir|regex_replace('\\\\', '\\\\\\\\')}}\\\\win_stat\\\\link'"
+    - "actual.stat.size is not defined"
+    - "actual.stat.checksum is not defined"
+    - "actual.stat.md5 is not defined"
+
+- name: test win_stat on junction
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\junction-link"
+  register: actual
+
+- name: assert symlink actual
+  assert:
+    that:
+    - "actual.stat.attributes == 'Directory, ReparsePoint'"
+    - "actual.stat.creationtime is defined"
+    - "actual.stat.exists == True"
+    - "actual.stat.filename == 'junction-link'"
+    - "actual.stat.isarchive == False"
+    - "actual.stat.isdir == True"
+    - "actual.stat.ishidden == False"
+    - "actual.stat.islink == True"
+    - "actual.stat.isreadonly == False"
+    - "actual.stat.isshared == False"
+    - "actual.stat.lastaccesstime is defined"
+    - "actual.stat.lastwritetime is defined"
+    - "actual.stat.lnk_source == '{{win_output_dir|regex_replace('\\\\', '\\\\\\\\')}}\\\\win_stat\\\\junction-dest'"
+    - "actual.stat.owner == 'BUILTIN\\Administrators'"
+    - "actual.stat.path == '{{win_output_dir|regex_replace('\\\\', '\\\\\\\\')}}\\\\win_stat\\\\junction-link'"
+    - "actual.stat.size is not defined"
+    - "actual.stat.checksum is not defined"
+    - "actual.stat.md5 is not defined"
 
 - name: test win_stat module non-existent path
-  win_stat: path="C:/this_file_should_not_exist.txt"
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\this_file_should_not_exist"
   register: win_stat_missing
 
 - name: check win_stat missing result
-  assert:
+  assert: 
     that:
       - "not win_stat_missing.stat.exists"
       - "not win_stat_missing|failed"
@@ -96,3 +613,26 @@
       - "win_stat_no_args|failed"
       - "win_stat_no_args.msg"
       - "not win_stat_no_args|changed"
+
+- name: check if junction symbolic link exists
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\junction-link"
+  register: junction_link_exists
+
+- name: delete junction symbolic link if it exists
+  win_command: cmd.exe /c rmdir {{win_output_dir}}\win_stat\junction-link
+  when: junction_link_exists.stat.exists
+
+- name: check if symbolic link exists
+  win_stat:
+    path: "{{win_output_dir}}\\win_stat\\link"
+  register: nested_link_exists
+
+- name: delete nested symbolic link if it exists
+  win_shell: cmd.exe /c rmdir {{win_output_dir}}\win_stat\link
+  when: nested_link_exists.stat.exists
+
+- name: remove testing folder
+  win_file:
+    path: "{{win_output_dir}}\\win_stat"
+    state: absent