From d1908de3e3b02a57bfb4d89fcd0c55b9b4c91870 Mon Sep 17 00:00:00 2001 From: Shaun Becker Date: Fri, 18 Sep 2026 11:29:53 -0400 Subject: [PATCH 1/2] Client: Allow SMB2 create contexts on CreateFile, and add SMB2_CREATE_TIMEWARP_TOKEN The SMB2 layer already carried create contexts in both directions -- CreateRequest.CreateContexts and CreateResponse.CreateContexts are parsed and serialized -- but the client had no way to send or read them, so a caller could not open a previous version of a file. A shadow copy is addressed with the SMB2_CREATE_TIMEWARP_TOKEN create context. The @GMT- path prefix is not a server-side convention: smbclient and the Windows redirector translate such a path into a TimeWarp token, which is why a raw SMB2 client that sends the token in the path gets STATUS_OBJECT_PATH_NOT_FOUND from Samba while smbclient reads the same file. - SMB2FileStore.CreateFile gains an overload taking the create contexts to send and returning those the server answered with. The existing overload delegates to it, so behaviour is unchanged for current callers. - CreateContextName holds the context names from [MS-SMB2] 2.2.13.2. - CreateContextHelper builds and reads SMB2_CREATE_TIMEWARP_TOKEN and looks a context up by name. Verified against Samba with shadow_copy2 and two snapshots: opening report.txt with a TimeWarp token for the older snapshot reads that snapshot's content, while opening it without one reads the live file. Those integration tests are inconclusive unless ServerAddress is set, since the in-process SMBServer does not serve shadow copies. --- .../IntegrationTests/TimeWarpTokenTests.cs | 113 ++++++++++++++++++ SMBLibrary.Tests/SMB2/CreateContextTests.cs | 70 +++++++++++ SMBLibrary/Client/SMB2FileStore.cs | 25 ++++ .../SMB2/Enums/Create/CreateContextName.cs | 45 +++++++ .../SMB2/Structures/CreateContextHelper.cs | 71 +++++++++++ 5 files changed, 324 insertions(+) create mode 100644 SMBLibrary.Tests/IntegrationTests/TimeWarpTokenTests.cs create mode 100644 SMBLibrary.Tests/SMB2/CreateContextTests.cs create mode 100644 SMBLibrary/SMB2/Enums/Create/CreateContextName.cs create mode 100644 SMBLibrary/SMB2/Structures/CreateContextHelper.cs diff --git a/SMBLibrary.Tests/IntegrationTests/TimeWarpTokenTests.cs b/SMBLibrary.Tests/IntegrationTests/TimeWarpTokenTests.cs new file mode 100644 index 00000000..41bbba8a --- /dev/null +++ b/SMBLibrary.Tests/IntegrationTests/TimeWarpTokenTests.cs @@ -0,0 +1,113 @@ +/* Copyright (C) 2026 Tal Aloni . All rights reserved. + * + * You can redistribute this program and/or modify it under the terms of + * the GNU Lesser Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + */ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SMBLibrary.Client; +using SMBLibrary.SMB2; + +namespace SMBLibrary.Tests.IntegrationTests +{ + /// + /// Exercises SMB2_CREATE_TIMEWARP_TOKEN against a server that actually serves shadow copies, + /// which the in-process SMBServer does not. Point these at a Samba share configured with + /// shadow_copy2 and two snapshots holding a file named report.txt. + /// + [TestClass] + public class TimeWarpTokenTests + { + // Set to the address of a shadow-copy enabled share to run these. + private const string ServerAddress = null; + private const string ShareName = "testshare_vss"; + private const string UserName = "testuser"; + private const string Password = "testpass"; + + private SMB2Client m_client; + private ISMBFileStore m_fileStore; + + [TestInitialize] + public void Initialize() + { + if (ServerAddress == null) + { + Assert.Inconclusive("No shadow-copy enabled server configured."); + } + + m_client = new SMB2Client(); + Assert.IsTrue(m_client.Connect(IPAddress.Parse(ServerAddress), SMBTransportType.DirectTCPTransport), "connect"); + Assert.AreEqual(NTStatus.STATUS_SUCCESS, m_client.Login(String.Empty, UserName, Password), "login"); + m_fileStore = m_client.TreeConnect(ShareName, out NTStatus status); + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status, "tree connect"); + } + + [TestCleanup] + public void Cleanup() + { + if (m_fileStore != null) + { + m_fileStore.Disconnect(); + } + if (m_client != null) + { + m_client.Logoff(); + m_client.Disconnect(); + } + } + + [TestMethod] + public void EnumerateSnapshotsReturnsTheServersTokens() + { + object handle; + FileStatus fileStatus; + Assert.AreEqual(NTStatus.STATUS_SUCCESS, m_fileStore.CreateFile(out handle, out fileStatus, String.Empty, AccessMask.GENERIC_READ, 0, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null)); + + byte[] output; + NTStatus status = m_fileStore.DeviceIOControl(handle, (uint)IoControlCode.FSCTL_SRV_ENUMERATE_SNAPSHOTS, new byte[0], out output, 64 * 1024); + m_fileStore.CloseFile(handle); + + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status); + Assert.IsTrue(output.Length > 12, "snapshot array should carry at least one token"); + } + + [TestMethod] + public void TimeWarpTokenOpensTheFileHeldByThatSnapshot() + { + DateTime snapshot = new DateTime(2026, 9, 17, 6, 30, 0, DateTimeKind.Utc); + List createContexts = new List(); + createContexts.Add(CreateContextHelper.CreateTimeWarpToken(snapshot)); + + object handle; + FileStatus fileStatus; + List responseCreateContexts; + NTStatus status = ((SMB2FileStore)m_fileStore).CreateFile(out handle, out fileStatus, out responseCreateContexts, "report.txt", AccessMask.GENERIC_READ, 0, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, 0, null, createContexts); + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status, "open with TimeWarp token"); + Assert.IsNotNull(responseCreateContexts, "an open that succeeded reports the server's contexts, empty or not"); + + byte[] data; + m_fileStore.ReadFile(out data, handle, 0, 64); + m_fileStore.CloseFile(handle); + + Assert.AreEqual("oldest", Encoding.ASCII.GetString(data).Trim()); + } + + [TestMethod] + public void OpeningWithoutATimeWarpTokenReadsTheLiveFile() + { + object handle; + FileStatus fileStatus; + Assert.AreEqual(NTStatus.STATUS_SUCCESS, m_fileStore.CreateFile(out handle, out fileStatus, "report.txt", AccessMask.GENERIC_READ, 0, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, 0, null)); + + byte[] data; + m_fileStore.ReadFile(out data, handle, 0, 64); + m_fileStore.CloseFile(handle); + + Assert.AreEqual("live", Encoding.ASCII.GetString(data).Trim()); + } + } +} diff --git a/SMBLibrary.Tests/SMB2/CreateContextTests.cs b/SMBLibrary.Tests/SMB2/CreateContextTests.cs new file mode 100644 index 00000000..a1ead26e --- /dev/null +++ b/SMBLibrary.Tests/SMB2/CreateContextTests.cs @@ -0,0 +1,70 @@ +/* Copyright (C) 2026 Tal Aloni . All rights reserved. + * + * You can redistribute this program and/or modify it under the terms of + * the GNU Lesser Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + */ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SMBLibrary.SMB2; + +namespace SMBLibrary.Tests.SMB2 +{ + [TestClass] + public class CreateContextTests + { + [TestMethod] + public void TimeWarpTokenUsesTheNameFromTheSpecification() + { + CreateContext context = CreateContextHelper.CreateTimeWarpToken(DateTime.UtcNow); + + Assert.AreEqual(CreateContextName.TimeWarpToken, context.Name); + } + + [TestMethod] + public void TimeWarpTokenCarriesTheTimestampAsAFileTime() + { + DateTime timestamp = new DateTime(2026, 9, 18, 12, 0, 0, DateTimeKind.Utc); + + CreateContext context = CreateContextHelper.CreateTimeWarpToken(timestamp); + + Assert.AreEqual(8, context.Data.Length); + Assert.AreEqual(timestamp, CreateContextHelper.ReadTimeWarpToken(context)); + } + + [TestMethod] + public void CreateRequestRoundTripsATimeWarpToken() + { + DateTime timestamp = new DateTime(2026, 9, 18, 12, 0, 0, DateTimeKind.Utc); + CreateRequest request = new CreateRequest(); + request.Name = "reports\\q3.xlsx"; + request.CreateContexts.Add(CreateContextHelper.CreateTimeWarpToken(timestamp)); + + CreateRequest parsed = new CreateRequest(request.GetBytes(), 0); + + Assert.AreEqual("reports\\q3.xlsx", parsed.Name); + Assert.AreEqual(1, parsed.CreateContexts.Count); + Assert.AreEqual(CreateContextName.TimeWarpToken, parsed.CreateContexts[0].Name); + Assert.AreEqual(timestamp, CreateContextHelper.ReadTimeWarpToken(parsed.CreateContexts[0])); + } + + [TestMethod] + public void CreateRequestRoundTripsSeveralCreateContexts() + { + CreateRequest request = new CreateRequest(); + request.Name = "reports\\q3.xlsx"; + request.CreateContexts.Add(CreateContextHelper.CreateTimeWarpToken(DateTime.UtcNow)); + request.CreateContexts.Add(new CreateContext() { Name = CreateContextName.QueryMaximalAccess, Data = new byte[0] }); + + CreateRequest parsed = new CreateRequest(request.GetBytes(), 0); + + List names = new List(); + foreach (CreateContext context in parsed.CreateContexts) + { + names.Add(context.Name); + } + CollectionAssert.AreEqual(new List { CreateContextName.TimeWarpToken, CreateContextName.QueryMaximalAccess }, names); + } + } +} diff --git a/SMBLibrary/Client/SMB2FileStore.cs b/SMBLibrary/Client/SMB2FileStore.cs index a6bc4218..12014b95 100644 --- a/SMBLibrary/Client/SMB2FileStore.cs +++ b/SMBLibrary/Client/SMB2FileStore.cs @@ -28,9 +28,29 @@ public SMB2FileStore(SMB2Client client, uint treeID, bool encryptShareData) } public NTStatus CreateFile(out object handle, out FileStatus fileStatus, string path, AccessMask desiredAccess, FileAttributes fileAttributes, ShareAccess shareAccess, CreateDisposition createDisposition, CreateOptions createOptions, SecurityContext securityContext) + { + List responseCreateContexts; + return CreateFile(out handle, out fileStatus, out responseCreateContexts, path, desiredAccess, fileAttributes, shareAccess, createDisposition, createOptions, securityContext, null); + } + + /// + /// Creates or opens a file, sending the given create contexts with the request and returning + /// the create contexts the server answered with. + /// + /// + /// [MS-SMB2] 2.2.13.2 create contexts to send, or null to send none. Use + /// to open the copy of the file held by + /// a shadow copy (previous version) of the share. + /// + /// + /// The create contexts returned by the server, or null when the file was not opened. An open + /// that succeeded but drew no contexts from the server yields an empty list. + /// + public NTStatus CreateFile(out object handle, out FileStatus fileStatus, out List responseCreateContexts, string path, AccessMask desiredAccess, FileAttributes fileAttributes, ShareAccess shareAccess, CreateDisposition createDisposition, CreateOptions createOptions, SecurityContext securityContext, List createContexts) { handle = null; fileStatus = FileStatus.FILE_DOES_NOT_EXIST; + responseCreateContexts = null; CreateRequest request = new CreateRequest(); if (m_isDfsOperation) { @@ -45,6 +65,10 @@ public NTStatus CreateFile(out object handle, out FileStatus fileStatus, string request.CreateDisposition = createDisposition; request.CreateOptions = createOptions; request.ImpersonationLevel = ImpersonationLevel.Impersonation; + if (createContexts != null) + { + request.CreateContexts.AddRange(createContexts); + } TrySendCommand(request); SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); @@ -55,6 +79,7 @@ public NTStatus CreateFile(out object handle, out FileStatus fileStatus, string CreateResponse createResponse = ((CreateResponse)response); handle = createResponse.FileId; fileStatus = ToFileStatus(createResponse.CreateAction); + responseCreateContexts = createResponse.CreateContexts; } return response.Header.Status; } diff --git a/SMBLibrary/SMB2/Enums/Create/CreateContextName.cs b/SMBLibrary/SMB2/Enums/Create/CreateContextName.cs new file mode 100644 index 00000000..19fc3af4 --- /dev/null +++ b/SMBLibrary/SMB2/Enums/Create/CreateContextName.cs @@ -0,0 +1,45 @@ +/* Copyright (C) 2026 Tal Aloni . All rights reserved. + * + * You can redistribute this program and/or modify it under the terms of + * the GNU Lesser Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + */ + +namespace SMBLibrary.SMB2 +{ + /// + /// [MS-SMB2] 2.2.13.2 - The names of the create contexts defined by the specification. + /// + public static class CreateContextName + { + /// SMB2_CREATE_EA_BUFFER + public const string ExtendedAttributes = "ExtA"; + + /// SMB2_CREATE_SD_BUFFER + public const string SecurityDescriptor = "SecD"; + + /// SMB2_CREATE_DURABLE_HANDLE_REQUEST + public const string DurableHandleRequest = "DHnQ"; + + /// SMB2_CREATE_DURABLE_HANDLE_RECONNECT + public const string DurableHandleReconnect = "DHnC"; + + /// SMB2_CREATE_ALLOCATION_SIZE + public const string AllocationSize = "AlSi"; + + /// SMB2_CREATE_QUERY_MAXIMAL_ACCESS_REQUEST + public const string QueryMaximalAccess = "MxAc"; + + /// + /// SMB2_CREATE_TIMEWARP_TOKEN. Opens the version of the file that existed at the given + /// timestamp, i.e. the copy held by the matching shadow copy (previous version) of the share. + /// + public const string TimeWarpToken = "TWrp"; + + /// SMB2_CREATE_QUERY_ON_DISK_ID + public const string QueryOnDiskID = "QFid"; + + /// SMB2_CREATE_REQUEST_LEASE + public const string RequestLease = "RqLs"; + } +} diff --git a/SMBLibrary/SMB2/Structures/CreateContextHelper.cs b/SMBLibrary/SMB2/Structures/CreateContextHelper.cs new file mode 100644 index 00000000..c5bea052 --- /dev/null +++ b/SMBLibrary/SMB2/Structures/CreateContextHelper.cs @@ -0,0 +1,71 @@ +/* Copyright (C) 2026 Tal Aloni . All rights reserved. + * + * You can redistribute this program and/or modify it under the terms of + * the GNU Lesser Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + */ +using System; +using System.Collections.Generic; + +namespace SMBLibrary.SMB2 +{ + /// + /// Builds and reads the create contexts defined by [MS-SMB2] 2.2.13.2. + /// + public static class CreateContextHelper + { + /// + /// [MS-SMB2] 2.2.13.2.7 - SMB2_CREATE_TIMEWARP_TOKEN. + /// Opens the version of the file that existed at , which is how a + /// client reads a shadow copy (previous version). The timestamp must match one of the tokens + /// the server returned from FSCTL_SRV_ENUMERATE_SNAPSHOTS; a server will not search for the + /// nearest one. + /// + public static CreateContext CreateTimeWarpToken(DateTime timestamp) + { + byte[] data = new byte[8]; + FileTimeHelper.WriteFileTime(data, 0, timestamp); + return new CreateContext() { Name = CreateContextName.TimeWarpToken, Data = data }; + } + + /// + /// Reads the timestamp out of an SMB2_CREATE_TIMEWARP_TOKEN create context. + /// + public static DateTime ReadTimeWarpToken(CreateContext createContext) + { + if (createContext == null) + { + throw new ArgumentNullException("createContext"); + } + + if (createContext.Data.Length < 8) + { + throw new ArgumentException("SMB2_CREATE_TIMEWARP_TOKEN must carry an 8 byte timestamp", "createContext"); + } + + return FileTimeHelper.ReadFileTime(createContext.Data, 0); + } + + /// + /// Returns the first create context with the given name, or null when the list does not + /// contain one. + /// + public static CreateContext FindByName(List createContexts, string name) + { + if (createContexts == null) + { + return null; + } + + foreach (CreateContext createContext in createContexts) + { + if (String.Equals(createContext.Name, name, StringComparison.Ordinal)) + { + return createContext; + } + } + + return null; + } + } +} From a433882b6a9550955463f5d8bf4153ca70b8c954 Mon Sep 17 00:00:00 2001 From: Shaun Becker Date: Fri, 18 Sep 2026 14:21:03 -0400 Subject: [PATCH 2/2] Code review feedback --- .../IntegrationTests/TimeWarpTokenTests.cs | 113 ------------------ SMBLibrary.Tests/SMB2/CreateContextTests.cs | 70 ----------- SMBLibrary/Client/SMB2FileStore.cs | 4 +- .../SMB2/Enums/Create/CreateContextName.cs | 45 ------- .../SMB2/Structures/CreateContextHelper.cs | 71 ----------- 5 files changed, 1 insertion(+), 302 deletions(-) delete mode 100644 SMBLibrary.Tests/IntegrationTests/TimeWarpTokenTests.cs delete mode 100644 SMBLibrary.Tests/SMB2/CreateContextTests.cs delete mode 100644 SMBLibrary/SMB2/Enums/Create/CreateContextName.cs delete mode 100644 SMBLibrary/SMB2/Structures/CreateContextHelper.cs diff --git a/SMBLibrary.Tests/IntegrationTests/TimeWarpTokenTests.cs b/SMBLibrary.Tests/IntegrationTests/TimeWarpTokenTests.cs deleted file mode 100644 index 41bbba8a..00000000 --- a/SMBLibrary.Tests/IntegrationTests/TimeWarpTokenTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -/* Copyright (C) 2026 Tal Aloni . All rights reserved. - * - * You can redistribute this program and/or modify it under the terms of - * the GNU Lesser Public License as published by the Free Software Foundation, - * either version 3 of the License, or (at your option) any later version. - */ -using System; -using System.Collections.Generic; -using System.Net; -using System.Text; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SMBLibrary.Client; -using SMBLibrary.SMB2; - -namespace SMBLibrary.Tests.IntegrationTests -{ - /// - /// Exercises SMB2_CREATE_TIMEWARP_TOKEN against a server that actually serves shadow copies, - /// which the in-process SMBServer does not. Point these at a Samba share configured with - /// shadow_copy2 and two snapshots holding a file named report.txt. - /// - [TestClass] - public class TimeWarpTokenTests - { - // Set to the address of a shadow-copy enabled share to run these. - private const string ServerAddress = null; - private const string ShareName = "testshare_vss"; - private const string UserName = "testuser"; - private const string Password = "testpass"; - - private SMB2Client m_client; - private ISMBFileStore m_fileStore; - - [TestInitialize] - public void Initialize() - { - if (ServerAddress == null) - { - Assert.Inconclusive("No shadow-copy enabled server configured."); - } - - m_client = new SMB2Client(); - Assert.IsTrue(m_client.Connect(IPAddress.Parse(ServerAddress), SMBTransportType.DirectTCPTransport), "connect"); - Assert.AreEqual(NTStatus.STATUS_SUCCESS, m_client.Login(String.Empty, UserName, Password), "login"); - m_fileStore = m_client.TreeConnect(ShareName, out NTStatus status); - Assert.AreEqual(NTStatus.STATUS_SUCCESS, status, "tree connect"); - } - - [TestCleanup] - public void Cleanup() - { - if (m_fileStore != null) - { - m_fileStore.Disconnect(); - } - if (m_client != null) - { - m_client.Logoff(); - m_client.Disconnect(); - } - } - - [TestMethod] - public void EnumerateSnapshotsReturnsTheServersTokens() - { - object handle; - FileStatus fileStatus; - Assert.AreEqual(NTStatus.STATUS_SUCCESS, m_fileStore.CreateFile(out handle, out fileStatus, String.Empty, AccessMask.GENERIC_READ, 0, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null)); - - byte[] output; - NTStatus status = m_fileStore.DeviceIOControl(handle, (uint)IoControlCode.FSCTL_SRV_ENUMERATE_SNAPSHOTS, new byte[0], out output, 64 * 1024); - m_fileStore.CloseFile(handle); - - Assert.AreEqual(NTStatus.STATUS_SUCCESS, status); - Assert.IsTrue(output.Length > 12, "snapshot array should carry at least one token"); - } - - [TestMethod] - public void TimeWarpTokenOpensTheFileHeldByThatSnapshot() - { - DateTime snapshot = new DateTime(2026, 9, 17, 6, 30, 0, DateTimeKind.Utc); - List createContexts = new List(); - createContexts.Add(CreateContextHelper.CreateTimeWarpToken(snapshot)); - - object handle; - FileStatus fileStatus; - List responseCreateContexts; - NTStatus status = ((SMB2FileStore)m_fileStore).CreateFile(out handle, out fileStatus, out responseCreateContexts, "report.txt", AccessMask.GENERIC_READ, 0, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, 0, null, createContexts); - Assert.AreEqual(NTStatus.STATUS_SUCCESS, status, "open with TimeWarp token"); - Assert.IsNotNull(responseCreateContexts, "an open that succeeded reports the server's contexts, empty or not"); - - byte[] data; - m_fileStore.ReadFile(out data, handle, 0, 64); - m_fileStore.CloseFile(handle); - - Assert.AreEqual("oldest", Encoding.ASCII.GetString(data).Trim()); - } - - [TestMethod] - public void OpeningWithoutATimeWarpTokenReadsTheLiveFile() - { - object handle; - FileStatus fileStatus; - Assert.AreEqual(NTStatus.STATUS_SUCCESS, m_fileStore.CreateFile(out handle, out fileStatus, "report.txt", AccessMask.GENERIC_READ, 0, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, 0, null)); - - byte[] data; - m_fileStore.ReadFile(out data, handle, 0, 64); - m_fileStore.CloseFile(handle); - - Assert.AreEqual("live", Encoding.ASCII.GetString(data).Trim()); - } - } -} diff --git a/SMBLibrary.Tests/SMB2/CreateContextTests.cs b/SMBLibrary.Tests/SMB2/CreateContextTests.cs deleted file mode 100644 index a1ead26e..00000000 --- a/SMBLibrary.Tests/SMB2/CreateContextTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -/* Copyright (C) 2026 Tal Aloni . All rights reserved. - * - * You can redistribute this program and/or modify it under the terms of - * the GNU Lesser Public License as published by the Free Software Foundation, - * either version 3 of the License, or (at your option) any later version. - */ -using System; -using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SMBLibrary.SMB2; - -namespace SMBLibrary.Tests.SMB2 -{ - [TestClass] - public class CreateContextTests - { - [TestMethod] - public void TimeWarpTokenUsesTheNameFromTheSpecification() - { - CreateContext context = CreateContextHelper.CreateTimeWarpToken(DateTime.UtcNow); - - Assert.AreEqual(CreateContextName.TimeWarpToken, context.Name); - } - - [TestMethod] - public void TimeWarpTokenCarriesTheTimestampAsAFileTime() - { - DateTime timestamp = new DateTime(2026, 9, 18, 12, 0, 0, DateTimeKind.Utc); - - CreateContext context = CreateContextHelper.CreateTimeWarpToken(timestamp); - - Assert.AreEqual(8, context.Data.Length); - Assert.AreEqual(timestamp, CreateContextHelper.ReadTimeWarpToken(context)); - } - - [TestMethod] - public void CreateRequestRoundTripsATimeWarpToken() - { - DateTime timestamp = new DateTime(2026, 9, 18, 12, 0, 0, DateTimeKind.Utc); - CreateRequest request = new CreateRequest(); - request.Name = "reports\\q3.xlsx"; - request.CreateContexts.Add(CreateContextHelper.CreateTimeWarpToken(timestamp)); - - CreateRequest parsed = new CreateRequest(request.GetBytes(), 0); - - Assert.AreEqual("reports\\q3.xlsx", parsed.Name); - Assert.AreEqual(1, parsed.CreateContexts.Count); - Assert.AreEqual(CreateContextName.TimeWarpToken, parsed.CreateContexts[0].Name); - Assert.AreEqual(timestamp, CreateContextHelper.ReadTimeWarpToken(parsed.CreateContexts[0])); - } - - [TestMethod] - public void CreateRequestRoundTripsSeveralCreateContexts() - { - CreateRequest request = new CreateRequest(); - request.Name = "reports\\q3.xlsx"; - request.CreateContexts.Add(CreateContextHelper.CreateTimeWarpToken(DateTime.UtcNow)); - request.CreateContexts.Add(new CreateContext() { Name = CreateContextName.QueryMaximalAccess, Data = new byte[0] }); - - CreateRequest parsed = new CreateRequest(request.GetBytes(), 0); - - List names = new List(); - foreach (CreateContext context in parsed.CreateContexts) - { - names.Add(context.Name); - } - CollectionAssert.AreEqual(new List { CreateContextName.TimeWarpToken, CreateContextName.QueryMaximalAccess }, names); - } - } -} diff --git a/SMBLibrary/Client/SMB2FileStore.cs b/SMBLibrary/Client/SMB2FileStore.cs index 12014b95..8f08ba99 100644 --- a/SMBLibrary/Client/SMB2FileStore.cs +++ b/SMBLibrary/Client/SMB2FileStore.cs @@ -38,9 +38,7 @@ public NTStatus CreateFile(out object handle, out FileStatus fileStatus, string /// the create contexts the server answered with. /// /// - /// [MS-SMB2] 2.2.13.2 create contexts to send, or null to send none. Use - /// to open the copy of the file held by - /// a shadow copy (previous version) of the share. + /// [MS-SMB2] 2.2.13.2 create contexts to send, or null to send none. /// /// /// The create contexts returned by the server, or null when the file was not opened. An open diff --git a/SMBLibrary/SMB2/Enums/Create/CreateContextName.cs b/SMBLibrary/SMB2/Enums/Create/CreateContextName.cs deleted file mode 100644 index 19fc3af4..00000000 --- a/SMBLibrary/SMB2/Enums/Create/CreateContextName.cs +++ /dev/null @@ -1,45 +0,0 @@ -/* Copyright (C) 2026 Tal Aloni . All rights reserved. - * - * You can redistribute this program and/or modify it under the terms of - * the GNU Lesser Public License as published by the Free Software Foundation, - * either version 3 of the License, or (at your option) any later version. - */ - -namespace SMBLibrary.SMB2 -{ - /// - /// [MS-SMB2] 2.2.13.2 - The names of the create contexts defined by the specification. - /// - public static class CreateContextName - { - /// SMB2_CREATE_EA_BUFFER - public const string ExtendedAttributes = "ExtA"; - - /// SMB2_CREATE_SD_BUFFER - public const string SecurityDescriptor = "SecD"; - - /// SMB2_CREATE_DURABLE_HANDLE_REQUEST - public const string DurableHandleRequest = "DHnQ"; - - /// SMB2_CREATE_DURABLE_HANDLE_RECONNECT - public const string DurableHandleReconnect = "DHnC"; - - /// SMB2_CREATE_ALLOCATION_SIZE - public const string AllocationSize = "AlSi"; - - /// SMB2_CREATE_QUERY_MAXIMAL_ACCESS_REQUEST - public const string QueryMaximalAccess = "MxAc"; - - /// - /// SMB2_CREATE_TIMEWARP_TOKEN. Opens the version of the file that existed at the given - /// timestamp, i.e. the copy held by the matching shadow copy (previous version) of the share. - /// - public const string TimeWarpToken = "TWrp"; - - /// SMB2_CREATE_QUERY_ON_DISK_ID - public const string QueryOnDiskID = "QFid"; - - /// SMB2_CREATE_REQUEST_LEASE - public const string RequestLease = "RqLs"; - } -} diff --git a/SMBLibrary/SMB2/Structures/CreateContextHelper.cs b/SMBLibrary/SMB2/Structures/CreateContextHelper.cs deleted file mode 100644 index c5bea052..00000000 --- a/SMBLibrary/SMB2/Structures/CreateContextHelper.cs +++ /dev/null @@ -1,71 +0,0 @@ -/* Copyright (C) 2026 Tal Aloni . All rights reserved. - * - * You can redistribute this program and/or modify it under the terms of - * the GNU Lesser Public License as published by the Free Software Foundation, - * either version 3 of the License, or (at your option) any later version. - */ -using System; -using System.Collections.Generic; - -namespace SMBLibrary.SMB2 -{ - /// - /// Builds and reads the create contexts defined by [MS-SMB2] 2.2.13.2. - /// - public static class CreateContextHelper - { - /// - /// [MS-SMB2] 2.2.13.2.7 - SMB2_CREATE_TIMEWARP_TOKEN. - /// Opens the version of the file that existed at , which is how a - /// client reads a shadow copy (previous version). The timestamp must match one of the tokens - /// the server returned from FSCTL_SRV_ENUMERATE_SNAPSHOTS; a server will not search for the - /// nearest one. - /// - public static CreateContext CreateTimeWarpToken(DateTime timestamp) - { - byte[] data = new byte[8]; - FileTimeHelper.WriteFileTime(data, 0, timestamp); - return new CreateContext() { Name = CreateContextName.TimeWarpToken, Data = data }; - } - - /// - /// Reads the timestamp out of an SMB2_CREATE_TIMEWARP_TOKEN create context. - /// - public static DateTime ReadTimeWarpToken(CreateContext createContext) - { - if (createContext == null) - { - throw new ArgumentNullException("createContext"); - } - - if (createContext.Data.Length < 8) - { - throw new ArgumentException("SMB2_CREATE_TIMEWARP_TOKEN must carry an 8 byte timestamp", "createContext"); - } - - return FileTimeHelper.ReadFileTime(createContext.Data, 0); - } - - /// - /// Returns the first create context with the given name, or null when the list does not - /// contain one. - /// - public static CreateContext FindByName(List createContexts, string name) - { - if (createContexts == null) - { - return null; - } - - foreach (CreateContext createContext in createContexts) - { - if (String.Equals(createContext.Name, name, StringComparison.Ordinal)) - { - return createContext; - } - } - - return null; - } - } -}