├── .gitignore ├── .gitmodules ├── trimcheck.manifest ├── Makefile ├── README.md └── trimcheck.d /.gitignore: -------------------------------------------------------------------------------- 1 | /trimcheck.exe 2 | /trimcheck.bin 3 | /trimcheck-cont.json 4 | .witness-* 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ae"] 2 | path = ae 3 | url = git://github.com/CyberShadow/ae.git 4 | [submodule "win32"] 5 | path = win32 6 | url = git://github.com/CS-svnmirror/dsource-bindings-win32.git 7 | -------------------------------------------------------------------------------- /trimcheck.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT: trimcheck.exe trimcheck64.exe 2 | manifest: trimcheck-manifest.exe trimcheck64-manifest.exe 3 | sign : trimcheck-signed.exe trimcheck64-signed.exe 4 | 5 | 6 | trimcheck.exe : trimcheck.d 7 | rdmd --force --build-only -version=WindowsXP -version=Unicode trimcheck.d 8 | 9 | trimcheck-manifest.exe : trimcheck.exe trimcheck.manifest 10 | cp -f trimcheck.exe trimcheck-tmp.exe 11 | mt -manifest trimcheck.manifest -outputresource:trimcheck-tmp.exe 12 | mv -f trimcheck-tmp.exe trimcheck-manifest.exe 13 | 14 | trimcheck-signed.exe : trimcheck-manifest.exe 15 | cp -f trimcheck-manifest.exe trimcheck-tmp.exe 16 | signtool sign /a /n "Vladimir Panteleev" /d "TrimCheck" /du "https://github.com/CyberShadow/trimcheck" /t http://time.certum.pl/ trimcheck-tmp.exe 17 | mv -f trimcheck-tmp.exe trimcheck-signed.exe 18 | 19 | 20 | trimcheck64.exe : trimcheck.d 21 | rdmd --force --build-only -version=WindowsXP -version=Unicode -m64 -oftrimcheck64.exe trimcheck.d 22 | 23 | trimcheck64-manifest.exe : trimcheck64.exe trimcheck.manifest 24 | cp -f trimcheck64.exe trimcheck64-tmp.exe 25 | mt -manifest trimcheck.manifest -outputresource:trimcheck64-tmp.exe 26 | mv -f trimcheck64-tmp.exe trimcheck64-manifest.exe 27 | 28 | trimcheck64-signed.exe : trimcheck64-manifest.exe 29 | cp -f trimcheck64-manifest.exe trimcheck64-tmp.exe 30 | signtool sign /a /n "Vladimir Panteleev" /d "TrimCheck" /du "https://github.com/CyberShadow/trimcheck" /t http://time.certum.pl/ trimcheck64-tmp.exe 31 | mv -f trimcheck64-tmp.exe trimcheck64-signed.exe 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trimcheck 2 | 3 | This program provides an easy way to test whether TRIM works on your SSD. 4 | It uses a similar method to the one described [here][Anandtech], 5 | but uses sector calculations to avoid searching the entire drive for the sought pattern. 6 | It also pads the sought data with 32MB blocks of dummy data, to give some room 7 | to processes which may otherwise overwrite the tested deleted disk area. 8 | 9 | The program will set up a test by creating and deleting a file with unique contents, 10 | then (on the second run) checks if the data is still accessible at the file's previous location. 11 | 12 | [Anandtech]: http://www.anandtech.com/show/6477/trim-raid0-ssd-arrays-work-with-intel-6series-motherboards-too/2 13 | 14 | ## Download 15 | 16 | You can download a compiled version on my website, [here](http://files.thecybershadow.net/trimcheck/). 17 | 18 | ## Usage 19 | 20 | Place this program file on the same drive you'd like to test TRIM on, and run it. 21 | Administrator privileges and at least 64MB free disk space will be required. 22 | 23 | ## Building from source 24 | 25 | A [D compiler](http://dlang.org/download.html) is required. 26 | 27 | You can use the `rdmd` tool (included with DMD) to build `trimcheck`: 28 | 29 | $ git clone --recursive https://github.com/CyberShadow/trimcheck 30 | $ cd trimcheck 31 | $ rdmd --build-only trimcheck 32 | 33 | ## License 34 | 35 | `trimcheck` is available under the [Mozilla Public License, version 2.0](http://mozilla.org/MPL/2.0/). 36 | 37 | ## Changelog 38 | 39 | ### trimcheck v0.7 (2014-08-16) 40 | 41 | * Fix incorrect free space detection 42 | 43 | ### trimcheck v0.6 (2014-03-23) 44 | 45 | * Fix support for drives with big clusters 46 | * Fix false negatives due to compressed filesystems 47 | 48 | ### trimcheck v0.5 (2013-08-21) 49 | 50 | * Write fully random data as padding instead of a repeating pattern (to avoid possible intervention of deduplication components) 51 | * Cryptographically sign executable 52 | 53 | ### trimcheck v0.4 (2013-02-18) 54 | 55 | * Remove read checks, as they caused tested data to not be TRIMmed in some configurations 56 | * Add symlink detection 57 | 58 | ### trimcheck v0.3 (2013-01-09) 59 | 60 | * Add support for SSDs which present cleared sectors as filled with 1s instead of 0s 61 | 62 | ### trimcheck v0.2 (2012-12-10) 63 | 64 | * Pad tested data with 32MB of dummy data on either side 65 | 66 | ### trimcheck v0.1 (2012-12-09) 67 | 68 | * Initial release 69 | -------------------------------------------------------------------------------- /trimcheck.d: -------------------------------------------------------------------------------- 1 | // Written in the D programming language 2 | 3 | /** 4 | * An SSD TRIM testing tool. 5 | * 6 | * License: 7 | * This Source Code Form is subject to the terms of 8 | * the Mozilla Public License, v. 2.0. If a copy of 9 | * the MPL was not distributed with this file, You 10 | * can obtain one at http://mozilla.org/MPL/2.0/. 11 | * 12 | * Authors: 13 | * Vladimir Panteleev 14 | */ 15 | 16 | module trimcheck; 17 | 18 | import std.algorithm; 19 | import std.conv : to; 20 | import std.exception; 21 | import std.file; 22 | import std.path; 23 | import std.random; 24 | import std.stdio; 25 | import std.string; 26 | import std.utf; 27 | 28 | // http://dsource.org/projects/bindings/wiki/WindowsApi 29 | import win32.windows; 30 | import win32.winioctl; 31 | 32 | import ae.sys.windows; 33 | import ae.utils.json; 34 | 35 | alias max = std.algorithm.max; 36 | 37 | struct STORAGE_DEVICE_NUMBER 38 | { 39 | DEVICE_TYPE DeviceType; 40 | ULONG DeviceNumber; 41 | ULONG PartitionNumber; 42 | } 43 | 44 | struct STORAGE_ACCESS_ALIGNMENT_DESCRIPTOR 45 | { 46 | DWORD Version; 47 | DWORD Size; 48 | DWORD BytesPerCacheLine; 49 | DWORD BytesOffsetForCacheAlignment; 50 | DWORD BytesPerLogicalSector; 51 | DWORD BytesPerPhysicalSector; 52 | DWORD BytesOffsetForSectorAlignment; 53 | } 54 | 55 | alias DWORD STORAGE_PROPERTY_ID; 56 | enum : STORAGE_PROPERTY_ID 57 | { 58 | StorageDeviceProperty = 0, 59 | StorageAdapterProperty = 1, 60 | StorageDeviceIdProperty = 2, 61 | StorageDeviceUniqueIdProperty = 3, 62 | StorageDeviceWriteCacheProperty = 4, 63 | StorageMiniportProperty = 5, 64 | StorageAccessAlignmentProperty = 6, 65 | StorageDeviceSeekPenaltyProperty = 7, 66 | StorageDeviceTrimProperty = 8, 67 | StorageDeviceWriteAggregationProperty = 9, 68 | StorageDeviceDeviceTelemetryProperty = 10, // 0xA 69 | StorageDeviceLBProvisioningProperty = 11, // 0xB 70 | StorageDevicePowerProperty = 12, // 0xC 71 | StorageDeviceCopyOffloadProperty = 13, // 0xD 72 | StorageDeviceResiliencyProperty = 14, // 0xE 73 | } 74 | 75 | alias DWORD STORAGE_QUERY_TYPE; 76 | enum : STORAGE_QUERY_TYPE 77 | { 78 | PropertyStandardQuery = 0, 79 | PropertyExistsQuery = 1, 80 | PropertyMaskQuery = 2, 81 | PropertyQueryMaxDefined = 3, 82 | } 83 | 84 | struct STORAGE_PROPERTY_QUERY 85 | { 86 | STORAGE_PROPERTY_ID PropertyId; 87 | STORAGE_QUERY_TYPE QueryType; 88 | BYTE[1] AdditionalParameters; 89 | } 90 | 91 | enum IOCTL_STORAGE_QUERY_PROPERTY = CTL_CODE_T!(IOCTL_STORAGE_BASE, 0x0500, METHOD_BUFFERED, FILE_ANY_ACCESS); 92 | 93 | extern(Windows) alias DWORD function(HANDLE hFile, LPWSTR lpszFilePath, DWORD cchFilePath, DWORD dwFlags) GetFinalPathNameByHandleWFunc; 94 | 95 | enum FILE_NAME_NORMALIZED = 0x0; 96 | 97 | enum VOLUME_NAME_DOS = 0x0; 98 | enum VOLUME_NAME_GUID = 0x1; 99 | enum VOLUME_NAME_NT = 0x2; 100 | enum VOLUME_NAME_NONE = 0x4; 101 | 102 | enum DATAFILENAME = "trimcheck.bin"; 103 | enum SAVEFILENAME = "trimcheck-cont.json"; 104 | 105 | enum MB = 1024*1024; 106 | enum PADDINGSIZE_MB = 32; // Size to pad our tested sector (in MB). Total size = PADDINGSIZE_MB*MB + DATASIZE + PADDINGSIZE_MB*MB. 107 | 108 | void run() 109 | { 110 | writeln("TRIM check v0.7 - Written by Vladimir Panteleev"); 111 | writeln("https://github.com/CyberShadow/trimcheck"); 112 | writeln(); 113 | 114 | if (!SAVEFILENAME.exists) 115 | { 116 | create(); 117 | 118 | // This causes weird behavior: the file never gets TRIMmed even if the program is closed and reopened. 119 | version(none) 120 | { 121 | int n; 122 | while (SAVEFILENAME.exists) 123 | { 124 | Sleep(1000); 125 | writefln("========================== %d seconds ==========================", ++n); 126 | verify(); 127 | } 128 | } 129 | } 130 | else 131 | verify(); 132 | } 133 | 134 | struct SaveData 135 | { 136 | string ntDrivePath; 137 | ulong offset; 138 | ubyte[] rndBuffer; 139 | } 140 | 141 | ubyte[] readBufferFromDisk(string ntDrivePath, ulong offset, size_t dataSize) 142 | { 143 | writefln(" Opening %s...", ntDrivePath); 144 | HANDLE hDriveRead = CreateFileW(toUTF16z(ntDrivePath), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, null, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, null); 145 | wenforce(hDriveRead != INVALID_HANDLE_VALUE, "CreateFileW failed"); 146 | scope(exit) wenforce(CloseHandle(hDriveRead), "CloseHandle failed"); 147 | 148 | writefln(" Seeking to position %d...", offset); 149 | LARGE_INTEGER uliOffset; 150 | uliOffset.QuadPart = offset; 151 | wenforce(SetFilePointer(hDriveRead, uliOffset.LowPart, &uliOffset.HighPart, FILE_BEGIN) != INVALID_SET_FILE_POINTER, "SetFilePointer failed"); 152 | 153 | writefln(" Reading %d bytes...", dataSize); 154 | ubyte[] readBuffer = new ubyte[dataSize]; 155 | DWORD dwNumberOfBytesRead; 156 | wenforce(ReadFile(hDriveRead, readBuffer.ptr, readBuffer.length.to!uint(), &dwNumberOfBytesRead, null), "ReadFile failed"); 157 | enforce(dwNumberOfBytesRead == readBuffer.length, format("Read only %d out of %d bytes", dwNumberOfBytesRead, readBuffer.length)); 158 | 159 | writefln(" First 16 bytes: %(%02X %)...", readBuffer[0..16]); 160 | 161 | return readBuffer; 162 | } 163 | 164 | void flushDiskBuffers(string ntDrivePath) 165 | { 166 | writefln("Flushing buffers on %s...", ntDrivePath); 167 | 168 | writefln(" Opening %s...", ntDrivePath); 169 | HANDLE hDrive = CreateFileW(toUTF16z(ntDrivePath), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, null, OPEN_EXISTING, 0, null); 170 | wenforce(hDrive != INVALID_HANDLE_VALUE, "CreateFileW failed"); 171 | scope(exit) wenforce(CloseHandle(hDrive), "CloseHandle failed"); 172 | 173 | writeln(" Flushing buffers..."); 174 | wenforce(FlushFileBuffers(hDrive), "FlushFileBuffers failed"); 175 | } 176 | 177 | STORAGE_ACCESS_ALIGNMENT_DESCRIPTOR detectSectorSize(string devName) 178 | { 179 | writefln(" Obtaining sector size on %s...", devName); 180 | 181 | 182 | writefln(" Opening %s...", devName); 183 | HANDLE hFile = CreateFileW(toUTF16z(devName), STANDARD_RIGHTS_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, null, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, null); 184 | wenforce(hFile != INVALID_HANDLE_VALUE, "CreateFileW failed"); 185 | scope(exit) wenforce(CloseHandle(hFile), "CloseHandle failed"); 186 | 187 | STORAGE_PROPERTY_QUERY query; 188 | query.QueryType = PropertyStandardQuery; 189 | query.PropertyId = StorageAccessAlignmentProperty; 190 | 191 | writeln(" Querying storage alignment property..."); 192 | DWORD dwBytes; 193 | STORAGE_ACCESS_ALIGNMENT_DESCRIPTOR result; 194 | wenforce(DeviceIoControl(hFile, IOCTL_STORAGE_QUERY_PROPERTY, &query, query.sizeof, &result, result.sizeof, &dwBytes, null), "DeviceIoControl(IOCTL_STORAGE_QUERY_PROPERTY) failed"); 195 | 196 | writefln(" BytesPerCacheLine = %d", result.BytesPerCacheLine ); 197 | writefln(" BytesOffsetForCacheAlignment = %d", result.BytesOffsetForCacheAlignment ); 198 | writefln(" BytesPerLogicalSector = %d", result.BytesPerLogicalSector ); 199 | writefln(" BytesPerPhysicalSector = %d", result.BytesPerPhysicalSector ); 200 | writefln(" BytesOffsetForSectorAlignment = %d", result.BytesOffsetForSectorAlignment); 201 | 202 | return result; 203 | } 204 | 205 | void writeBuf(HANDLE hFile, ubyte[] data) 206 | { 207 | DWORD dwNumberOfBytesWritten; 208 | wenforce(WriteFile(hFile, data.ptr, data.length.to!uint, &dwNumberOfBytesWritten, null), "WriteFile failed"); 209 | enforce(data.length == dwNumberOfBytesWritten, format("Wrote only %d out of %d bytes", dwNumberOfBytesWritten, data.length)); 210 | } 211 | 212 | /+ 213 | size_t getDataSize() 214 | { 215 | writeln("Determining size of test data..."); 216 | 217 | // BUG: This will break if a path element is a symlink or junction to another partition 218 | auto ntDrivePath = `\\.\` ~ driveName(absolutePath(DATAFILENAME)); 219 | writefln(" Opening %s...", ntDrivePath); 220 | HANDLE hDrive = CreateFileW(toUTF16z(ntDrivePath), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, null, OPEN_EXISTING, 0, null); 221 | wenforce(hDrive != INVALID_HANDLE_VALUE, "CreateFileW failed"); 222 | scope(exit) wenforce(CloseHandle(hDrive), "CloseHandle failed"); 223 | 224 | writeln(" Querying drive information..."); 225 | STORAGE_DEVICE_NUMBER sdn; 226 | DWORD c; 227 | wenforce(DeviceIoControl(hDrive, IOCTL_STORAGE_GET_DEVICE_NUMBER, null, 0, &sdn, sdn.sizeof, &c, null), "DeviceIoControl(IOCTL_STORAGE_GET_DEVICE_NUMBER) failed"); 228 | 229 | // Device types are listed here: http://msdn.microsoft.com/en-us/library/windows/hardware/ff563821(v=vs.85).aspx 230 | writefln(" Drive is located on device %d (type 0x%08x), partition %d.", sdn.DeviceNumber, sdn.DeviceType, sdn.PartitionNumber); 231 | 232 | auto physicalDrivePath = format(`\\.\PhysicalDrive%d`, sdn.DeviceNumber); 233 | STORAGE_ACCESS_ALIGNMENT_DESCRIPTOR saad = detectSectorSize(physicalDrivePath); 234 | 235 | auto dataSize = saad.BytesPerPhysicalSector; 236 | 237 | // Size needs to be bigger than 512 to have a sector number 238 | // (otherwise NTFS inlnes it into MFT or something). 239 | while (dataSize <= 512) 240 | dataSize *= 2; 241 | 242 | writefln(" Using data size of %d", dataSize); 243 | return dataSize; 244 | } 245 | +/ 246 | 247 | void create() 248 | { 249 | writeln("USAGE: Place this program file on the same drive"); 250 | writeln("you'd like to test TRIM on, and run it."); 251 | writeln(); 252 | writefln("Press Enter to test drive %s...", driveName(absolutePath(DATAFILENAME))); 253 | readln(); 254 | 255 | auto drivePathBS = driveName(absolutePath(DATAFILENAME)) ~ `\`; 256 | writefln("Querying %s disk space and sector size information...", drivePathBS); 257 | DWORD dwSectorsPerCluster, dwBytesPerSector, dwNumberOfFreeClusters, dwTotalNumberOfClusters; 258 | wenforce(GetDiskFreeSpaceW(toUTF16z(drivePathBS), &dwSectorsPerCluster, &dwBytesPerSector, &dwNumberOfFreeClusters, &dwTotalNumberOfClusters), "GetDiskFreeSpaceW failed"); 259 | writefln(" %s has %d bytes per sector, and %d sectors per cluster.", drivePathBS, dwBytesPerSector, dwSectorsPerCluster); 260 | writefln(" %d out of %d clusters are free.", dwNumberOfFreeClusters, dwTotalNumberOfClusters); 261 | 262 | auto dataSize = max(16*1024, dwBytesPerSector * dwSectorsPerCluster); 263 | enforce(dataSize % (dwBytesPerSector * dwSectorsPerCluster)==0, format("Unsupported cluster size (%d*%d), please report this.", dwBytesPerSector, dwSectorsPerCluster)); 264 | enforce(cast(ulong)dwNumberOfFreeClusters * dwBytesPerSector * dwSectorsPerCluster > dataSize + PADDINGSIZE_MB * MB * 2, "Disk space is too low!"); 265 | 266 | writefln("Generating random target data block (%d bytes)...", dataSize); 267 | auto rndBuffer = new ubyte[dataSize]; 268 | foreach (ref b; rndBuffer) 269 | b = uniform!ubyte(); 270 | writefln(" First 16 bytes: %(%02X %)...", rndBuffer[0..16]); 271 | 272 | writefln("Creating %s...", absolutePath(DATAFILENAME)); 273 | HANDLE hFile = CreateFileW(toUTF16z(DATAFILENAME), GENERIC_READ | GENERIC_WRITE, 0, null, CREATE_ALWAYS, FILE_FLAG_WRITE_THROUGH | FILE_FLAG_NO_BUFFERING, null); 274 | wenforce(hFile != INVALID_HANDLE_VALUE, "CreateFileW failed"); 275 | scope(exit) if (hFile) { wenforce(CloseHandle(hFile), "CloseHandle failed"); DeleteFileW(toUTF16z(DATAFILENAME)); } 276 | 277 | BY_HANDLE_FILE_INFORMATION fileInformation; 278 | GetFileInformationByHandle(hFile, &fileInformation); 279 | enforce((fileInformation.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED) == 0, "TrimCheck cannot reliably work on a compressed filesystem. Please rerun from an uncompressed directory."); 280 | 281 | auto ntDrivePath = `\\.\` ~ driveName(absolutePath(DATAFILENAME)); 282 | 283 | writeln("Querying file final paths..."); 284 | GetFinalPathNameByHandleWFunc GetFinalPathNameByHandleW = cast(GetFinalPathNameByHandleWFunc) GetProcAddress(GetModuleHandle("kernel32.dll"), "GetFinalPathNameByHandleW"); 285 | if (GetFinalPathNameByHandleW) 286 | { 287 | string getFinalPathName(DWORD dwKind) 288 | { 289 | static WCHAR[4096] buf; 290 | DWORD len = wenforce(GetFinalPathNameByHandleW(hFile, buf.ptr, buf.length, dwKind | FILE_NAME_NORMALIZED), "GetFinalPathNameByHandleW failed"); 291 | return toUTF8(buf[0..len]); 292 | } 293 | 294 | string[int] paths; 295 | foreach (kind; [VOLUME_NAME_DOS, VOLUME_NAME_GUID, VOLUME_NAME_NT, VOLUME_NAME_NONE]) 296 | paths[kind] = getFinalPathName(kind); 297 | 298 | writeln(" DOS : ", paths[VOLUME_NAME_DOS ]); 299 | writeln(" GUID : ", paths[VOLUME_NAME_GUID]); 300 | writeln(" NT : ", paths[VOLUME_NAME_NT ]); 301 | writeln(" NONE : ", paths[VOLUME_NAME_NONE]); 302 | 303 | enforce(paths[VOLUME_NAME_DOS ].startsWith(`\\?\`), `DOS path does not start with \\?\`); 304 | enforce(paths[VOLUME_NAME_GUID].startsWith(`\\?\`), `GUID path does not start with \\?\`); 305 | 306 | enforce(paths[VOLUME_NAME_DOS ].endsWith(paths[VOLUME_NAME_NONE]), "DOS path does not end with NONE path"); 307 | enforce(paths[VOLUME_NAME_GUID].endsWith(paths[VOLUME_NAME_NONE]), "GUID path does not end with NONE path"); 308 | enforce(paths[VOLUME_NAME_NT ].endsWith(paths[VOLUME_NAME_NONE]), "NT path does not end with NONE path"); 309 | 310 | enforce(icmp(ntDrivePath[4..$], paths[VOLUME_NAME_DOS][4..$-paths[VOLUME_NAME_NONE].length]) == 0, 311 | "Current directory seems to be located under a reparse point\nwhich points to another drive. Try placing the program file in the\nroot directory of the drive you wish to test."); 312 | } 313 | else 314 | writeln("WARNING: This system does not have GetFinalPathNameByHandle.\nSymlink detection skipped."); 315 | 316 | auto garbageData = new ubyte[MB]; 317 | 318 | void write1MBGarbage() 319 | { 320 | foreach (ref b; garbageData) 321 | b = uniform!ubyte(); 322 | writeBuf(hFile, garbageData); 323 | } 324 | 325 | writefln("Writing padding (%d bytes)...", PADDINGSIZE_MB*MB); 326 | foreach (n; 0..PADDINGSIZE_MB) write1MBGarbage(); 327 | 328 | writefln("Writing data (%d bytes)...", dataSize); 329 | writeBuf(hFile, rndBuffer); 330 | 331 | writefln("Writing padding (%d bytes)...", PADDINGSIZE_MB*MB); 332 | foreach (n; 0..PADDINGSIZE_MB) write1MBGarbage(); 333 | 334 | writeln("Flushing file..."); 335 | wenforce(FlushFileBuffers(hFile), "FlushFileBuffers failed"); 336 | 337 | writeln("Checking file size..."); 338 | enforce(GetFileSize(hFile, null) == PADDINGSIZE_MB*MB + dataSize + PADDINGSIZE_MB*MB, "Unexpected file size"); 339 | 340 | auto dataStartVCN = (PADDINGSIZE_MB*MB) / (dwBytesPerSector * dwSectorsPerCluster); 341 | auto dataEndVCN = dataStartVCN + (dataSize / (dwBytesPerSector * dwSectorsPerCluster)); 342 | writefln(" Data is located at Virtual Cluster Numbers %d-%d within file.", dataStartVCN, dataEndVCN-1); 343 | 344 | writeln("Querying file physical location..."); 345 | STARTING_VCN_INPUT_BUFFER svib; 346 | svib.StartingVcn.QuadPart = 0; 347 | auto rpbBuf = new ubyte[64*1024]; 348 | PRETRIEVAL_POINTERS_BUFFER prpb = cast(PRETRIEVAL_POINTERS_BUFFER)rpbBuf; 349 | 350 | DWORD c; 351 | wenforce(DeviceIoControl(hFile, FSCTL_GET_RETRIEVAL_POINTERS, &svib, svib.sizeof, prpb, rpbBuf.length.to!uint, &c, null), "DeviceIoControl(FSCTL_GET_RETRIEVAL_POINTERS) failed"); 352 | 353 | writefln(" %s has %d extent%s:", DATAFILENAME, prpb.ExtentCount, prpb.ExtentCount==1?"":"s"); 354 | ulong offset = 0; 355 | auto prevVcn = prpb.StartingVcn; // Should be 0 356 | foreach (n; 0..prpb.ExtentCount) 357 | { 358 | auto vcnStr = prevVcn.QuadPart == prpb.Extents()[n].NextVcn.QuadPart-1 ? format("Virtual cluster %d is", prevVcn.QuadPart) : format("Virtual clusters %d-%d are", prevVcn.QuadPart, prpb.Extents()[n].NextVcn.QuadPart-1); 359 | writefln(" Extent %d: %s located at LCN %d", n, vcnStr, prpb.Extents()[n].Lcn.QuadPart); 360 | 361 | auto startVCN = prevVcn.QuadPart; 362 | auto endVCN = prpb.Extents()[n].NextVcn.QuadPart; 363 | if (startVCN <= dataStartVCN && endVCN >= dataEndVCN) 364 | { 365 | writeln(" (this is the extent containing our data)"); 366 | auto dataLCN = prpb.Extents()[n].Lcn.QuadPart + (dataStartVCN - startVCN); 367 | offset = dataLCN * dwBytesPerSector * dwSectorsPerCluster; 368 | } 369 | 370 | prevVcn = prpb.Extents()[n].NextVcn; 371 | } 372 | 373 | foreach (n, extent; prpb.Extents()[0..prpb.ExtentCount]) 374 | enforce(extent.Lcn.QuadPart>0, format("The Logical Cluster Number of extent %d is not set. Perhaps the file is compressed?", n)); 375 | enforce(offset, "Could not find the extent of the data part of file."); 376 | 377 | writeln("Closing file."); 378 | wenforce(CloseHandle(hFile), "CloseHandle failed"); 379 | hFile = null; 380 | 381 | writefln("Saving continuation data to %s...", absolutePath(SAVEFILENAME)); 382 | std.file.write(SAVEFILENAME, toJson(SaveData(ntDrivePath, offset, rndBuffer[]))); 383 | scope(failure) SAVEFILENAME[].remove(); 384 | 385 | flushDiskBuffers(ntDrivePath); 386 | /+ 387 | writeln("Checking if file and raw volume data matches..."); 388 | auto readBuffer = readBufferFromDisk(ntDrivePath, offset, dataSize); 389 | enforce(readBuffer == rndBuffer[], "Mismatch between file and raw volume data.\nIs the file under a symlink or directory junction?"); 390 | +/ 391 | writeln("Deleting file..."); 392 | wenforce(DeleteFileW(toUTF16z(DATAFILENAME)), "DeleteFile failed"); 393 | 394 | flushDiskBuffers(ntDrivePath); 395 | /+ 396 | writeln("Re-checking raw volume data..."); 397 | readBuffer = readBufferFromDisk(ntDrivePath, offset, dataSize); 398 | enforce(readBuffer == rndBuffer[], "Data mismatch (data was clobbered directly after deleting it).\nThis could indicate that TRIM occurred immediately,\nor TRIM-unrelated unusual file delete behavior."); 399 | +/ 400 | writeln(); 401 | writeln("Test file created and deleted, and continuation data saved."); 402 | writeln("Do what needs to be done to activate the SSD's TRIM functionality,"); 403 | writeln("and run this program again."); 404 | writeln("Usually, you just need to wait a bit (around 20 seconds)."); 405 | writeln("Sometimes, a reboot is necessary."); 406 | } 407 | 408 | void verify() 409 | { 410 | scope(failure) writefln("\nAn error has occurred during verification.\nTo start from scratch, delete %s.\n", SAVEFILENAME); 411 | 412 | writefln("Loading continuation data from %s...", absolutePath(SAVEFILENAME)); 413 | auto saveData = jsonParse!SaveData(readText(SAVEFILENAME)); 414 | writefln(" Drive path : %s", saveData.ntDrivePath); 415 | writefln(" Offset : %s", saveData.offset); 416 | writefln(" Random data : %(%02X %)...", saveData.rndBuffer[0..16]); 417 | writeln(); 418 | 419 | auto dataSize = saveData.rndBuffer.length; 420 | 421 | writeln("Reading raw volume data..."); 422 | auto readBuffer = readBufferFromDisk(saveData.ntDrivePath, saveData.offset, dataSize); 423 | auto nullBuffer0 = new ubyte[dataSize]; nullBuffer0[] = 0x00; 424 | auto nullBuffer1 = new ubyte[dataSize]; nullBuffer1[] = 0xFF; 425 | 426 | if (readBuffer == saveData.rndBuffer) 427 | { 428 | writeln("Data unchanged."); 429 | writeln(); 430 | writeln("CONCLUSION: TRIM appears to be NOT WORKING (or has not kicked in yet)."); 431 | writeln(); 432 | writeln("Note: This may also indicate that the drive does not support DZAT"); 433 | writeln("(Deterministic Zero After Trim), even when TRIM is working."); 434 | writeln(); 435 | writeln("You can re-run this program to test again with the same data block,"); 436 | writefln("or delete %s to create a new test file.", SAVEFILENAME); 437 | } 438 | else 439 | if (readBuffer == nullBuffer0 || readBuffer == nullBuffer1) 440 | { 441 | writefln("Data is empty (filled with 0x%02X bytes).", readBuffer[0]); 442 | writeln(); 443 | writeln("CONCLUSION: TRIM appears to be WORKING!"); 444 | 445 | SAVEFILENAME[].remove(); 446 | } 447 | else 448 | { 449 | writeln("Data is neither unchanged nor empty."); 450 | writeln("Possible cause: another program saved data to disk,"); 451 | writeln("overwriting the sector containing our test data."); 452 | writeln(); 453 | writeln("CONCLUSION: INDETERMINATE."); 454 | writefln("Re-run this program and wait less before verifying / try to\nminimize writes to drive %s.", saveData.ntDrivePath[$-2..$]); 455 | 456 | SAVEFILENAME[].remove(); 457 | } 458 | } 459 | 460 | void main() 461 | { 462 | try 463 | run(); 464 | catch (Throwable e) 465 | writeln("Error: " ~ e.msg); 466 | 467 | writeln(); 468 | writeln("Press Enter to exit..."); 469 | readln(); 470 | } 471 | --------------------------------------------------------------------------------