├── .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 |
--------------------------------------------------------------------------------