Skip to content

Commit 0e12fbf

Browse files
MiaRomerolzybkr
authored andcommitted
Implement Format-Hex in C# (PowerShell#3320)
1 parent c624ae2 commit 0e12fbf

8 files changed

Lines changed: 765 additions & 498 deletions

File tree

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
using System;
2+
using System.IO;
3+
using System.Text;
4+
using System.Security;
5+
using System.Management.Automation;
6+
using System.Collections.Generic;
7+
using System.Management.Automation.Internal;
8+
// Once Serialization is available on CoreCLR: using System.Runtime.Serialization.Formatters.Binary;
9+
10+
namespace Microsoft.PowerShell.Commands
11+
{
12+
/// <summary>
13+
/// Displays the hexidecimal equivalent of the input data.
14+
/// <summary>
15+
[Cmdlet(VerbsCommon.Format, "Hex", SupportsShouldProcess = true, HelpUri ="https://go.microsoft.com/fwlink/?LinkId=526919")]
16+
[Alias ("fhx")]
17+
[OutputType(typeof(Microsoft.PowerShell.Commands.ByteCollection))]
18+
public sealed class FormatHex : PSCmdlet
19+
{
20+
private const int BUFFERSIZE = 16;
21+
22+
#region Parameters
23+
24+
/// <summary>
25+
/// Path of file(s) to process
26+
/// </summary>
27+
[Parameter(Mandatory = true, Position = 0, ParameterSetName = "Path")]
28+
[ValidateNotNullOrEmpty()]
29+
public string[] Path { get; set; }
30+
31+
/// <summary>
32+
/// Literal path of file to process
33+
/// </summary>
34+
[Parameter(Mandatory = true, ParameterSetName = "LiteralPath")]
35+
[ValidateNotNullOrEmpty()]
36+
[Alias("PSPath")]
37+
public string[] LiteralPath { get; set; }
38+
39+
/// <summary>
40+
/// Ojbect to process
41+
/// </summary>
42+
[Parameter(Mandatory = true, ParameterSetName = "ByInputObject", ValueFromPipeline = true)]
43+
public PSObject InputObject { get; set; }
44+
45+
/// <summary>
46+
/// Type of character encoding for InputObject
47+
/// </summary>
48+
[Parameter(ParameterSetName = "ByInputObject")]
49+
[ValidateSetAttribute(new string[] {
50+
EncodingConversion.Unicode,
51+
EncodingConversion.BigEndianUnicode,
52+
EncodingConversion.Utf8,
53+
EncodingConversion.Utf7,
54+
EncodingConversion.Utf32,
55+
EncodingConversion.Ascii})]
56+
public string Encoding { get; set; } = "Ascii";
57+
58+
/// <summary>
59+
/// This parameter is no-op
60+
/// </summary>
61+
[Parameter(ParameterSetName = "ByInputObject")]
62+
public SwitchParameter Raw { get; set; }
63+
64+
#endregion
65+
66+
#region Overrides
67+
68+
/// <summary>
69+
/// Implements the ProcessRecord method for the FormatHex command.
70+
/// </summary>
71+
protected override void ProcessRecord()
72+
{
73+
if (String.Equals(this.ParameterSetName, "ByInputObject", StringComparison.OrdinalIgnoreCase))
74+
{
75+
ProcessObjectContent(InputObject);
76+
}
77+
else
78+
{
79+
List<string> pathsToProcess = String.Equals(this.ParameterSetName, "LiteralPath", StringComparison.OrdinalIgnoreCase) ?
80+
ResolvePaths(LiteralPath, true) : ResolvePaths(Path, false);
81+
82+
ProcessPath(pathsToProcess);
83+
}
84+
}
85+
86+
#endregion
87+
88+
#region Paths
89+
90+
/// <summary>
91+
/// Validate each path provided and if valid, add to array of paths to process.
92+
/// If path is a literal path it is added to the array to process; we cannot validate them until we
93+
/// try to process file contents.
94+
/// </summary>
95+
/// <param name="path"></param>
96+
/// <param name="literalPath"></param>
97+
/// <returns></returns>
98+
private List<string> ResolvePaths(string[] path, bool literalPath)
99+
{
100+
List<string> pathsToProcess = new List<string>();
101+
ProviderInfo provider = null;
102+
PSDriveInfo drive = null;
103+
104+
foreach (string currentPath in path)
105+
{
106+
List<string> newPaths = new List<string>();
107+
108+
if (literalPath)
109+
{
110+
newPaths.Add(Context.SessionState.Path.GetUnresolvedProviderPathFromPSPath(currentPath, out provider, out drive));
111+
}
112+
else
113+
{
114+
try
115+
{
116+
newPaths.AddRange(Context.SessionState.Path.GetResolvedProviderPathFromPSPath(currentPath, out provider));
117+
}
118+
catch (ItemNotFoundException e)
119+
{
120+
if (!WildcardPattern.ContainsWildcardCharacters(currentPath))
121+
{
122+
ErrorRecord errorRecord = new ErrorRecord(e, "FileNotFound", ErrorCategory.ObjectNotFound, path);
123+
WriteError(errorRecord);
124+
continue;
125+
}
126+
}
127+
}
128+
129+
if (!provider.Name.Equals("FileSystem", StringComparison.OrdinalIgnoreCase))
130+
{
131+
// Write a non-terminating error message indicating that path specified is not supported.
132+
string errorMessage = StringUtil.Format(UtilityCommonStrings.FormatHexOnlySupportsFileSystemPaths, currentPath);
133+
ErrorRecord errorRecord = new ErrorRecord(new ArgumentException(errorMessage),
134+
"FormatHexOnlySupportsFileSystemPaths",
135+
ErrorCategory.InvalidArgument,
136+
currentPath);
137+
WriteError(errorRecord);
138+
continue;
139+
}
140+
141+
pathsToProcess.AddRange(newPaths);
142+
}
143+
144+
return pathsToProcess;
145+
}
146+
147+
/// <summary>
148+
/// Pass each valid path on to process its contents.
149+
/// </summary>
150+
/// <param name="pathsToProcess"></param>
151+
private void ProcessPath(List<string> pathsToProcess)
152+
{
153+
foreach (string path in pathsToProcess)
154+
{
155+
ProcessFileContent(path);
156+
}
157+
}
158+
159+
/// <summary>
160+
/// Creates a binary reader that reads the file content into a buffer (byte[]) 16 bytes at a time, and
161+
/// passes a copy of that array on to the ConvertToHexidecimal method to output.
162+
/// </summary>
163+
/// <param name="path"></param>
164+
private void ProcessFileContent(string path)
165+
{
166+
byte[] buffer = new byte[BUFFERSIZE];
167+
168+
try
169+
{
170+
using (BinaryReader reader = new BinaryReader(File.Open(path, FileMode.Open, FileAccess.Read)))
171+
{
172+
UInt32 offset = 0;
173+
Int32 bytesRead = 0;
174+
175+
while ((bytesRead = reader.Read(buffer, 0, BUFFERSIZE)) > 0)
176+
{
177+
if (bytesRead == BUFFERSIZE)
178+
{
179+
// We are reusing the same buffer so if we save the output to a variable, the variable
180+
// will just contain multiple references to the same buffer memory space (containing only the
181+
// last bytes of the file read). Copying the buffer allows us to pass the values on without
182+
// overwriting previous values.
183+
byte[] copyOfBuffer = new byte[16];
184+
Array.Copy(buffer, 0, copyOfBuffer, 0, bytesRead);
185+
ConvertToHexidecimal(copyOfBuffer, path, offset);
186+
}
187+
else
188+
{
189+
// Handle the case of a partial (and probably last) buffer. Copies the bytes read into a new,
190+
// shorter array so we do not have the extra bytes from the previous pass through at the end.
191+
byte[] remainingBytes = new byte[bytesRead];
192+
Array.Copy(buffer, 0, remainingBytes, 0, bytesRead);
193+
ConvertToHexidecimal(remainingBytes, path, offset);
194+
}
195+
// Update offset value.
196+
offset += (UInt32)bytesRead;
197+
}
198+
}
199+
}
200+
catch (IOException ioException)
201+
{
202+
// IOException takes care of FileNotFoundException, DirectoryNotFoundException, and PathTooLongException
203+
WriteError(new ErrorRecord(ioException, "FormatHexIOError", ErrorCategory.WriteError, path));
204+
}
205+
catch (ArgumentException argException)
206+
{
207+
WriteError(new ErrorRecord(argException, "FormatHexArgumentError", ErrorCategory.WriteError, path));
208+
}
209+
catch (NotSupportedException notSupportedException)
210+
{
211+
WriteError(new ErrorRecord(notSupportedException, "FormatHexPathRefersToANonFileDevice", ErrorCategory.InvalidArgument, path));
212+
}
213+
catch (SecurityException securityException)
214+
{
215+
WriteError(new ErrorRecord(securityException, "FormatHexUnauthorizedAccessError", ErrorCategory.PermissionDenied, path));
216+
}
217+
}
218+
219+
#endregion
220+
221+
#region InputObjects
222+
223+
/// <summary>
224+
/// Creates a byte array from the object passed to the cmdlet (based on type) and passes
225+
/// that array on to the ConvertToHexidecimal method to output.
226+
/// </summary>
227+
/// <param name="inputObject"></param>
228+
private void ProcessObjectContent(PSObject inputObject)
229+
{
230+
Object obj = inputObject.BaseObject;
231+
byte[] inputBytes = null;
232+
if (obj is System.IO.FileSystemInfo)
233+
{
234+
string[] path = { ((FileSystemInfo)obj).FullName };
235+
List<string> pathsToProcess = ResolvePaths(path, true);
236+
ProcessPath(pathsToProcess);
237+
}
238+
239+
else if (obj is string)
240+
{
241+
string inputString = obj.ToString();
242+
Encoding resolvedEncoding = EncodingConversion.Convert(this, Encoding);
243+
inputBytes = resolvedEncoding.GetBytes(inputString);
244+
}
245+
246+
else if (obj is byte)
247+
{
248+
inputBytes = new byte[] { (byte)obj };
249+
}
250+
251+
else if (obj is byte[])
252+
{
253+
inputBytes = ((byte[])obj);
254+
}
255+
256+
else if (obj is Int32)
257+
{
258+
inputBytes = BitConverter.GetBytes((Int32)obj);
259+
}
260+
261+
else if (obj is Int32[])
262+
{
263+
List<byte> inputStreamArray = new List<byte>();
264+
Int32[] inputInts = (Int32[])obj;
265+
foreach (Int32 value in inputInts)
266+
{
267+
byte[] tempBytes = BitConverter.GetBytes(value);
268+
inputStreamArray.AddRange(tempBytes);
269+
}
270+
inputBytes = inputStreamArray.ToArray();
271+
}
272+
273+
else if (obj is Int64)
274+
{
275+
inputBytes = BitConverter.GetBytes((Int64)obj);
276+
}
277+
278+
else if (obj is Int64[])
279+
{
280+
List<byte> inputStreamArray = new List<byte>();
281+
Int64[] inputInts = (Int64[])obj;
282+
foreach (Int64 value in inputInts)
283+
{
284+
byte[] tempBytes = BitConverter.GetBytes(value);
285+
inputStreamArray.AddRange(tempBytes);
286+
}
287+
inputBytes = inputStreamArray.ToArray();
288+
}
289+
290+
// If the object type is not supported, throw an error. Once Serialization is
291+
// available on CoreCLR, other types will be supported.
292+
else
293+
{
294+
string errorMessage = StringUtil.Format(UtilityCommonStrings.FormatHexTypeNotSupported, obj.GetType());
295+
ErrorRecord errorRecord = new ErrorRecord(new ArgumentException(errorMessage),
296+
"FormatHexTypeNotSupported",
297+
ErrorCategory.InvalidArgument,
298+
obj.GetType());
299+
WriteError(errorRecord);
300+
}
301+
302+
if (inputBytes != null)
303+
{
304+
ConvertToHexidecimal(inputBytes, null, 0);
305+
}
306+
}
307+
308+
#endregion
309+
310+
#region Output
311+
312+
/// <summary>
313+
/// Outputs the hexadecimial representaion of the of the input data.
314+
/// </summary>
315+
/// <param name="inputBytes"></param>
316+
/// <param name="path"></param>
317+
/// <param name="offset"></param>
318+
private void ConvertToHexidecimal(byte[] inputBytes, string path, UInt32 offset)
319+
{
320+
if (inputBytes != null)
321+
{
322+
ByteCollection byteCollectionObject = new ByteCollection(offset, inputBytes, path);
323+
WriteObject(byteCollectionObject);
324+
}
325+
}
326+
327+
#endregion
328+
}
329+
}

src/Microsoft.PowerShell.Commands.Utility/commands/utility/UtilityCommon.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,6 @@ public static class UtilityResources
124124
/// </summary>
125125
public static string FileReadError { get { return UtilityCommonStrings.FileReadError; } }
126126

127-
/// <summary>
128-
/// Error message to indicate that Format-Hex cmdlet does not directly support the type provided as input.
129-
/// </summary>
130-
public static string FormatHexTypeNotSupported { get { return UtilityCommonStrings.FormatHexTypeNotSupported; } }
131-
132-
/// <summary>
133-
/// Error message to indicate that Format-Hex cmdlet does not directly support the type provided as input.
134-
/// </summary>
135-
public static string FormatHexResolvePathError { get { return UtilityCommonStrings.FormatHexResolvePathError; } }
136-
137127
/// <summary>
138128
/// The resource string used to indicate 'PATH:' in the formating header.
139129
/// </summary>

src/Microsoft.PowerShell.Commands.Utility/resources/UtilityCommonStrings.resx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,10 @@
145145
<value>The file '{0}' cannot be read: {1}</value>
146146
</data>
147147
<data name="FormatHexTypeNotSupported" xml:space="preserve">
148-
<value>Cannot convert input of type {0} to hexadecimal. To view the hexadecimal formatting of its string representation, pipe it to the Out-String cmdlet before piping it to Format-Hex.</value>
148+
<value>Cannot convert input of type '{0}' to hexadecimal. To view the hexadecimal formatting of its string representation, pipe it to the Out-String cmdlet before piping it to Format-Hex.</value>
149149
</data>
150-
<data name="FormatHexResolvePathError" xml:space="preserve">
151-
<value>Cannot display the context of {0} as hex. The path resolves to multiple files.</value>
150+
<data name="FormatHexOnlySupportsFileSystemPaths" xml:space="preserve">
151+
<value>The given path '{0}' is not supported. This command only supports the FileSystem Provider paths.</value>
152152
</data>
153153
<data name="FormatHexPathPrefix" xml:space="preserve">
154154
<value>Path: </value>

0 commit comments

Comments
 (0)