diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1a308ab..db73aa3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,9 @@ ### New Features +* **Added `ConvertFrom-ByteArrayToString`** - Converts byte arrays to strings with encoding support (Issue #15) + * Supports ASCII, BigEndianUnicode, Default, Unicode, UTF32, and UTF8 encodings + * Inverse operation of `ConvertFrom-StringToByteArray` * **Added `-Encoding` parameter** to `ConvertFrom-MemoryStreamToString` (Issue #21) * **Added `-Encoding` parameter** to `ConvertFrom-MemoryStreamToSecureString` (Issue #21) * **Added pipeline support** to `ConvertFrom-Base64ToByteArray` (Issue #16) diff --git a/docs/functions/ConvertFrom-Base64ToByteArray.md b/docs/functions/ConvertFrom-Base64ToByteArray.md index 7f66dd1..c898303 100644 --- a/docs/functions/ConvertFrom-Base64ToByteArray.md +++ b/docs/functions/ConvertFrom-Base64ToByteArray.md @@ -1,7 +1,7 @@ --- external help file: Convert-help.xml Module Name: Convert -online version: https://msdn.microsoft.com/en-us/library/system.convert.frombase64string%28v=vs.110%29.aspx +online version: https://austoonz.github.io/Convert/functions/ConvertFrom-Base64ToByteArray/ schema: 2.0.0 --- @@ -13,7 +13,7 @@ Converts a Base 64 Encoded String to a Byte Array ## SYNTAX ``` -ConvertFrom-Base64ToByteArray [-String] [-ProgressAction ] [] +ConvertFrom-Base64ToByteArray [-String] [-ProgressAction ] [] ``` ## DESCRIPTION @@ -24,13 +24,17 @@ Converts a Base 64 Encoded String to a Byte Array ### EXAMPLE 1 ``` ConvertFrom-Base64ToByteArray -String 'dGVzdA==' -116 -101 -115 -116 ``` -Converts the base64 string to its byte array representation. +### EXAMPLE 2 +``` +'SGVsbG8=' | ConvertFrom-Base64ToByteArray +``` + +### EXAMPLE 3 +``` +'SGVsbG8=', 'V29ybGQ=' | ConvertFrom-Base64ToByteArray +``` ## PARAMETERS @@ -38,14 +42,14 @@ Converts the base64 string to its byte array representation. The Base 64 Encoded String to be converted ```yaml -Type: String +Type: String[] Parameter Sets: (All) Aliases: Base64String Required: True Position: 1 Default value: None -Accept pipeline input: False +Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False ``` @@ -71,9 +75,10 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## OUTPUTS +### [Byte[]] ## NOTES ## RELATED LINKS -[https://msdn.microsoft.com/en-us/library/system.convert.frombase64string%28v=vs.110%29.aspx](https://msdn.microsoft.com/en-us/library/system.convert.frombase64string%28v=vs.110%29.aspx) +[https://austoonz.github.io/Convert/functions/ConvertFrom-Base64ToByteArray/](https://austoonz.github.io/Convert/functions/ConvertFrom-Base64ToByteArray/) diff --git a/docs/functions/ConvertFrom-ByteArrayToMemoryStream.md b/docs/functions/ConvertFrom-ByteArrayToMemoryStream.md index 7314732..5b3c801 100644 --- a/docs/functions/ConvertFrom-ByteArrayToMemoryStream.md +++ b/docs/functions/ConvertFrom-ByteArrayToMemoryStream.md @@ -1,7 +1,7 @@ --- external help file: Convert-help.xml Module Name: Convert -online version: https://msdn.microsoft.com/en-us/library/system.io.memorystream(v=vs.110).aspx +online version: https://austoonz.github.io/Convert/functions/ConvertFrom-ByteArrayToMemoryStream/ schema: 2.0.0 --- @@ -27,7 +27,11 @@ Converts a Byte Array to a MemoryStream ConvertFrom-ByteArrayToMemoryStream -ByteArray ([Byte[]] (,0xFF * 100)) ``` -This command uses the ConvertFrom-ByteArrayToMemoryStream cmdlet to convert a Byte Array into a Memory Stream. +### EXAMPLE 2 +``` +$bytes = [Byte[]]@(72, 101, 108, 108, 111) +,$bytes | ConvertFrom-ByteArrayToMemoryStream +``` ## PARAMETERS @@ -42,7 +46,7 @@ Aliases: Bytes Required: True Position: 1 Default value: None -Accept pipeline input: False +Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False ``` @@ -68,11 +72,10 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## OUTPUTS +### [System.IO.MemoryStream[]] ## NOTES -Additional information: -https://msdn.microsoft.com/en-us/library/63z365ty(v=vs.110).aspx ## RELATED LINKS -[https://msdn.microsoft.com/en-us/library/system.io.memorystream(v=vs.110).aspx](https://msdn.microsoft.com/en-us/library/system.io.memorystream(v=vs.110).aspx) +[https://austoonz.github.io/Convert/functions/ConvertFrom-ByteArrayToMemoryStream/](https://austoonz.github.io/Convert/functions/ConvertFrom-ByteArrayToMemoryStream/) diff --git a/docs/functions/ConvertFrom-ByteArrayToString.md b/docs/functions/ConvertFrom-ByteArrayToString.md new file mode 100644 index 0000000..d6f8f5e --- /dev/null +++ b/docs/functions/ConvertFrom-ByteArrayToString.md @@ -0,0 +1,111 @@ +--- +external help file: Convert-help.xml +Module Name: Convert +online version: https://austoonz.github.io/Convert/functions/ConvertFrom-ByteArrayToString/ +schema: 2.0.0 +--- + +# ConvertFrom-ByteArrayToString + +## SYNOPSIS +Converts a byte array to a string using the specified encoding. + +## SYNTAX + +``` +ConvertFrom-ByteArrayToString [-ByteArray] [[-Encoding] ] [-ProgressAction ] + [] +``` + +## DESCRIPTION +Converts a byte array to a string using the specified encoding. +This is the inverse operation of ConvertFrom-StringToByteArray. + +## EXAMPLES + +### EXAMPLE 1 +``` +$bytes = [byte[]]@(72, 101, 108, 108, 111) +ConvertFrom-ByteArrayToString -ByteArray $bytes +``` + +Hello + +### EXAMPLE 2 +``` +$bytes = ConvertFrom-StringToByteArray -String 'Hello, World!' +ConvertFrom-ByteArrayToString -ByteArray $bytes +``` + +Hello, World! + +### EXAMPLE 3 +``` +$bytes1, $bytes2 | ConvertFrom-ByteArrayToString -Encoding 'UTF8' +``` + +Converts multiple byte arrays from the pipeline to strings. + +## PARAMETERS + +### -ByteArray +The array of bytes to convert. + +```yaml +Type: Byte[] +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Encoding +The encoding to use for conversion. +Defaults to UTF8. +Valid options are ASCII, BigEndianUnicode, Default, Unicode, UTF32, and UTF8. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: UTF8 +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### [String] +## NOTES + +## RELATED LINKS + +[https://austoonz.github.io/Convert/functions/ConvertFrom-ByteArrayToString/](https://austoonz.github.io/Convert/functions/ConvertFrom-ByteArrayToString/) + diff --git a/docs/functions/ConvertFrom-MemoryStreamToSecureString.md b/docs/functions/ConvertFrom-MemoryStreamToSecureString.md index 619a937..12cf47c 100644 --- a/docs/functions/ConvertFrom-MemoryStreamToSecureString.md +++ b/docs/functions/ConvertFrom-MemoryStreamToSecureString.md @@ -14,14 +14,14 @@ Converts a Memory Stream to a Secure String ### MemoryStream (Default) ``` -ConvertFrom-MemoryStreamToSecureString -MemoryStream [-Encoding ] [-ProgressAction ] - [] +ConvertFrom-MemoryStreamToSecureString -MemoryStream [-Encoding ] + [-ProgressAction ] [] ``` ### Stream ``` -ConvertFrom-MemoryStreamToSecureString -Stream [-Encoding ] [-ProgressAction ] - [] +ConvertFrom-MemoryStreamToSecureString -Stream [-Encoding ] + [-ProgressAction ] [] ``` ## DESCRIPTION diff --git a/docs/functions/ConvertFrom-MemoryStreamToString.md b/docs/functions/ConvertFrom-MemoryStreamToString.md index 864be4c..5c81bd5 100644 --- a/docs/functions/ConvertFrom-MemoryStreamToString.md +++ b/docs/functions/ConvertFrom-MemoryStreamToString.md @@ -12,13 +12,6 @@ Converts MemoryStream to a string. ## SYNTAX -### MemoryStream -``` -ConvertFrom-MemoryStreamToString -MemoryStream [-Encoding ] [-ProgressAction ] - [] -``` - -### Stream ``` ConvertFrom-MemoryStreamToString -Stream [-Encoding ] [-ProgressAction ] [] @@ -97,33 +90,19 @@ Another string ## PARAMETERS -### -MemoryStream -A System.IO.MemoryStream object for conversion. - -```yaml -Type: MemoryStream[] -Parameter Sets: MemoryStream -Aliases: - -Required: True -Position: Named -Default value: None -Accept pipeline input: True (ByPropertyName, ByValue) -Accept wildcard characters: False -``` - ### -Stream A System.IO.Stream object for conversion. +Accepts any stream type including MemoryStream, FileStream, etc. ```yaml Type: Stream[] -Parameter Sets: Stream -Aliases: +Parameter Sets: (All) +Aliases: MemoryStream Required: True Position: Named Default value: None -Accept pipeline input: True (ByPropertyName) +Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False ``` diff --git a/docs/functions/ConvertFrom-StringToByteArray.md b/docs/functions/ConvertFrom-StringToByteArray.md index 6a45119..1303898 100644 --- a/docs/functions/ConvertFrom-StringToByteArray.md +++ b/docs/functions/ConvertFrom-StringToByteArray.md @@ -19,6 +19,7 @@ ConvertFrom-StringToByteArray [-String] [[-Encoding] ] [-Prog ## DESCRIPTION Converts a string to a byte array object. +This is the inverse operation of ConvertFrom-ByteArrayToString. ## EXAMPLES diff --git a/docs/functions/ConvertTo-String.md b/docs/functions/ConvertTo-String.md index b628d78..66e1c12 100644 --- a/docs/functions/ConvertTo-String.md +++ b/docs/functions/ConvertTo-String.md @@ -18,11 +18,6 @@ ConvertTo-String -Base64EncodedString [-Encoding ] [-Decompre [-ProgressAction ] [] ``` -### MemoryStream -``` -ConvertTo-String -MemoryStream [-ProgressAction ] [] -``` - ### Stream ``` ConvertTo-String -Stream [-ProgressAction ] [] @@ -120,33 +115,19 @@ Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False ``` -### -MemoryStream -A MemoryStream object for conversion. - -```yaml -Type: MemoryStream[] -Parameter Sets: MemoryStream -Aliases: - -Required: True -Position: Named -Default value: None -Accept pipeline input: True (ByPropertyName, ByValue) -Accept wildcard characters: False -``` - ### -Stream A System.IO.Stream object for conversion. +Accepts any stream type including MemoryStream, FileStream, etc. ```yaml Type: Stream[] Parameter Sets: Stream -Aliases: +Aliases: MemoryStream Required: True Position: Named Default value: None -Accept pipeline input: True (ByPropertyName) +Accept pipeline input: True (ByPropertyName, ByValue) Accept wildcard characters: False ``` diff --git a/lib/src/encoding.rs b/lib/src/encoding.rs index adf9d9e..c7be5b4 100644 --- a/lib/src/encoding.rs +++ b/lib/src/encoding.rs @@ -76,6 +76,13 @@ pub unsafe extern "C" fn string_to_bytes( } }; + // Check for deprecated UTF7 encoding (both UTF7 and UTF-7 variants) + if encoding_str.eq_ignore_ascii_case("UTF7") || encoding_str.eq_ignore_ascii_case("UTF-7") { + crate::error::set_error("UTF7 encoding is deprecated and not supported".to_string()); + set_output_length_zero(out_length); + return std::ptr::null_mut(); + } + // Convert string to bytes using shared encoding logic let bytes = match crate::base64::convert_string_to_bytes(input_str, encoding_str) { Ok(b) => b, @@ -114,6 +121,99 @@ fn set_output_length_zero(out_length: *mut usize) { } } +/// Convert a byte array to a string using the specified encoding +/// +/// Supports UTF-8, ASCII, Unicode (UTF-16LE), UTF-32, BigEndianUnicode (UTF-16BE), +/// and Default (UTF-8) encodings. The encoding name is case-insensitive and supports +/// both hyphenated (UTF-8) and non-hyphenated (UTF8) variants. +/// +/// # Arguments +/// * `bytes` - Pointer to byte array to convert +/// * `length` - Length of the byte array +/// * `encoding` - Null-terminated C string specifying the encoding +/// +/// # Returns +/// Pointer to allocated null-terminated C string, or null on error. The caller must +/// free the returned pointer using `free_string`. +/// +/// # Safety +/// This function is unsafe because it dereferences raw pointers. +/// The caller must ensure that: +/// - `bytes` is a valid pointer to a byte array of at least `length` bytes, or null if length is 0 +/// - `encoding` is a valid null-terminated C string or null +/// - The returned pointer must be freed using `free_string` +/// +/// # Error Handling +/// Returns null pointer and sets error message via `set_error` if: +/// - Encoding pointer is null +/// - Encoding contains invalid UTF-8 +/// - Encoding name is not supported +/// - Byte sequence is invalid for the specified encoding +#[unsafe(no_mangle)] +pub unsafe extern "C" fn bytes_to_string( + bytes: *const u8, + length: usize, + encoding: *const c_char, +) -> *mut c_char { + // Validate encoding pointer first (consistent with string_to_bytes) + if encoding.is_null() { + crate::error::set_error("Encoding pointer is null".to_string()); + return std::ptr::null_mut(); + } + + // SAFETY: encoding pointer is validated as non-null above + let encoding_str = match unsafe { CStr::from_ptr(encoding).to_str() } { + Ok(s) => s, + Err(_) => { + crate::error::set_error("Invalid UTF-8 in encoding string".to_string()); + return std::ptr::null_mut(); + } + }; + + // Handle empty byte array case + if length == 0 { + crate::error::clear_error(); + let empty = std::ffi::CString::new("").unwrap(); + return empty.into_raw(); + } + + // Validate bytes pointer (only needed when length > 0) + if bytes.is_null() { + crate::error::set_error("Bytes pointer is null".to_string()); + return std::ptr::null_mut(); + } + + // SAFETY: bytes pointer is validated as non-null and length is provided by caller + let byte_slice = unsafe { std::slice::from_raw_parts(bytes, length) }; + + // Check for deprecated UTF7 encoding (both UTF7 and UTF-7 variants) + if encoding_str.eq_ignore_ascii_case("UTF7") || encoding_str.eq_ignore_ascii_case("UTF-7") { + crate::error::set_error("UTF7 encoding is deprecated and not supported".to_string()); + return std::ptr::null_mut(); + } + + // Convert bytes to string using shared encoding logic + let result_string = match crate::base64::convert_bytes_to_string(byte_slice, encoding_str) { + Ok(s) => s, + Err(e) => { + crate::error::set_error(e); + return std::ptr::null_mut(); + } + }; + + // Convert Rust string to C string + match std::ffi::CString::new(result_string) { + Ok(c_string) => { + crate::error::clear_error(); + c_string.into_raw() + } + Err(_) => { + crate::error::set_error("Result string contains null byte".to_string()); + std::ptr::null_mut() + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -225,6 +325,28 @@ mod tests { } } + #[test] + fn test_string_to_bytes_utf7_deprecated() { + // Test: UTF7 encoding should return null (deprecated) + let input = CString::new("Hello").unwrap(); + let encoding = CString::new("UTF7").unwrap(); + let mut out_length: usize = 0; + + let result = unsafe { + string_to_bytes( + input.as_ptr(), + encoding.as_ptr(), + &mut out_length as *mut usize, + ) + }; + + assert!( + result.is_null(), + "UTF7 encoding should return null (deprecated)" + ); + assert_eq!(out_length, 0, "Output length should be 0 for UTF7"); + } + #[test] fn test_string_to_bytes_invalid_encoding() { // Test: invalid encoding name should return null @@ -662,4 +784,391 @@ mod tests { handle.join().unwrap(); } } + + // ========== Tests for bytes_to_string ========== + + #[test] + fn test_bytes_to_string_happy_path_utf8() { + // Test: convert [72, 101, 108, 108, 111] with UTF8 encoding to "Hello" + let bytes: [u8; 5] = [72, 101, 108, 108, 111]; + let encoding = CString::new("UTF8").unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + + assert!(!result.is_null(), "Result should not be null"); + + let result_str = unsafe { CStr::from_ptr(result).to_str().unwrap() }; + assert_eq!(result_str, "Hello", "Should decode to 'Hello'"); + + unsafe { crate::memory::free_string(result) }; + } + + #[test] + fn test_bytes_to_string_empty_bytes() { + // Test: empty byte array should return empty string + let bytes: [u8; 0] = []; + let encoding = CString::new("UTF8").unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), 0, encoding.as_ptr()) }; + + assert!( + !result.is_null(), + "Result should not be null for empty bytes" + ); + + let result_str = unsafe { CStr::from_ptr(result).to_str().unwrap() }; + assert_eq!(result_str, "", "Should return empty string"); + + unsafe { crate::memory::free_string(result) }; + } + + #[test] + fn test_bytes_to_string_null_bytes_with_length() { + // Test: null bytes pointer with non-zero length should return null + let encoding = CString::new("UTF8").unwrap(); + + let result = unsafe { bytes_to_string(std::ptr::null(), 5, encoding.as_ptr()) }; + + assert!( + result.is_null(), + "Null bytes with length > 0 should return null" + ); + } + + #[test] + fn test_bytes_to_string_null_bytes_with_zero_length() { + // Test: null bytes pointer with zero length should succeed (edge case) + let encoding = CString::new("UTF8").unwrap(); + + let result = unsafe { bytes_to_string(std::ptr::null(), 0, encoding.as_ptr()) }; + + assert!(!result.is_null(), "Null bytes with length 0 should succeed"); + + let result_str = unsafe { CStr::from_ptr(result).to_str().unwrap() }; + assert_eq!(result_str, "", "Should return empty string"); + + unsafe { crate::memory::free_string(result) }; + } + + #[test] + fn test_bytes_to_string_null_encoding() { + // Test: null encoding pointer should return null + let bytes: [u8; 5] = [72, 101, 108, 108, 111]; + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), std::ptr::null()) }; + + assert!(result.is_null(), "Null encoding should return null"); + } + + #[test] + fn test_bytes_to_string_all_encodings() { + // Test: all supported encodings should work + let encodings_and_bytes: Vec<(&str, Vec)> = vec![ + ("UTF8", vec![72, 101, 108, 108, 111]), // "Hello" in UTF-8 + ("ASCII", vec![72, 101, 108, 108, 111]), // "Hello" in ASCII + ("Unicode", vec![72, 0, 101, 0, 108, 0, 108, 0, 111, 0]), // "Hello" in UTF-16LE + ( + "BigEndianUnicode", + vec![0, 72, 0, 101, 0, 108, 0, 108, 0, 111], + ), // "Hello" in UTF-16BE + ( + "UTF32", + vec![ + 72, 0, 0, 0, 101, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 111, 0, 0, 0, + ], + ), // "Hello" in UTF-32LE + ("Default", vec![72, 101, 108, 108, 111]), // "Hello" in Default (UTF-8) + ]; + + for (enc, bytes) in encodings_and_bytes { + let encoding = CString::new(enc).unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + + assert!( + !result.is_null(), + "Result should not be null for encoding: {}", + enc + ); + + let result_str = unsafe { CStr::from_ptr(result).to_str().unwrap() }; + assert_eq!( + result_str, "Hello", + "Should decode to 'Hello' for encoding: {}", + enc + ); + + unsafe { crate::memory::free_string(result) }; + } + } + + #[test] + fn test_bytes_to_string_utf7_deprecated() { + // Test: UTF7 encoding should return null (deprecated) + let bytes: [u8; 5] = [72, 101, 108, 108, 111]; + let encoding = CString::new("UTF7").unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + + assert!( + result.is_null(), + "UTF7 encoding should return null (deprecated)" + ); + } + + #[test] + fn test_bytes_to_string_invalid_encoding() { + // Test: invalid encoding name should return null + let bytes: [u8; 5] = [72, 101, 108, 108, 111]; + let encoding = CString::new("INVALID_ENCODING").unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + + assert!(result.is_null(), "Invalid encoding should return null"); + } + + #[test] + fn test_bytes_to_string_invalid_utf8_bytes() { + // Test: invalid UTF-8 byte sequence should return null + let bytes: [u8; 2] = [0xFF, 0xFE]; // Invalid UTF-8 sequence + let encoding = CString::new("UTF8").unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + + assert!(result.is_null(), "Invalid UTF-8 bytes should return null"); + } + + #[test] + fn test_bytes_to_string_result_contains_null_byte() { + // Test: UTF-32 bytes that decode to a string containing a null character + // U+0000 (null) in UTF-32LE = [0x00, 0x00, 0x00, 0x00] + let bytes: [u8; 8] = [0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; // "A" + null + let encoding = CString::new("UTF32").unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + + assert!( + result.is_null(), + "Result containing null byte should return null" + ); + } + + #[test] + fn test_bytes_to_string_invalid_utf16_length() { + // Test: odd-length byte array for UTF-16 should return null + let bytes: [u8; 3] = [72, 0, 101]; // Odd length, invalid for UTF-16 + let encoding = CString::new("Unicode").unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + + assert!( + result.is_null(), + "Odd-length UTF-16 bytes should return null" + ); + } + + #[test] + fn test_bytes_to_string_invalid_utf32_length() { + // Test: non-multiple-of-4 byte array for UTF-32 should return null + let bytes: [u8; 5] = [72, 0, 0, 0, 101]; // Not multiple of 4 + let encoding = CString::new("UTF32").unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + + assert!( + result.is_null(), + "Non-multiple-of-4 UTF-32 bytes should return null" + ); + } + + #[test] + fn test_bytes_to_string_ascii_rejects_non_ascii() { + // Test: ASCII encoding should reject bytes > 127 + let bytes: [u8; 3] = [72, 200, 111]; // 200 is not valid ASCII + let encoding = CString::new("ASCII").unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + + assert!(result.is_null(), "ASCII should reject non-ASCII bytes"); + } + + #[test] + fn test_bytes_to_string_unicode_emoji() { + // Test: UTF-8 bytes for emoji should decode correctly + // 🌍 = U+1F30D = F0 9F 8C 8D in UTF-8 + let bytes: [u8; 4] = [0xF0, 0x9F, 0x8C, 0x8D]; + let encoding = CString::new("UTF8").unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + + assert!(!result.is_null(), "Result should not be null for emoji"); + + let result_str = unsafe { CStr::from_ptr(result).to_str().unwrap() }; + assert_eq!(result_str, "🌍", "Should decode to earth emoji"); + + unsafe { crate::memory::free_string(result) }; + } + + #[test] + fn test_bytes_to_string_large_input() { + // Test: 1MB of bytes should decode successfully + let large_bytes: Vec = vec![65u8; 1024 * 1024]; // 1MB of 'A' + let encoding = CString::new("UTF8").unwrap(); + + let result = + unsafe { bytes_to_string(large_bytes.as_ptr(), large_bytes.len(), encoding.as_ptr()) }; + + assert!( + !result.is_null(), + "Result should not be null for large input" + ); + + let result_str = unsafe { CStr::from_ptr(result).to_str().unwrap() }; + assert_eq!( + result_str.len(), + 1024 * 1024, + "Should have 1MB of characters" + ); + + unsafe { crate::memory::free_string(result) }; + } + + #[test] + fn test_bytes_to_string_round_trip_utf8() { + // Test: string -> bytes -> string round-trip + let original = CString::new("Hello, World! 🌍").unwrap(); + let encoding = CString::new("UTF8").unwrap(); + let mut out_length: usize = 0; + + // String to bytes + let bytes_ptr = unsafe { + string_to_bytes( + original.as_ptr(), + encoding.as_ptr(), + &mut out_length as *mut usize, + ) + }; + assert!(!bytes_ptr.is_null(), "string_to_bytes should succeed"); + + // Bytes to string + let result = unsafe { bytes_to_string(bytes_ptr, out_length, encoding.as_ptr()) }; + assert!(!result.is_null(), "bytes_to_string should succeed"); + + let result_str = unsafe { CStr::from_ptr(result).to_str().unwrap() }; + assert_eq!( + result_str, "Hello, World! 🌍", + "Round-trip should preserve string" + ); + + unsafe { + crate::memory::free_bytes(bytes_ptr); + crate::memory::free_string(result); + }; + } + + #[test] + fn test_bytes_to_string_round_trip_all_encodings() { + // Test: round-trip for all encodings + let encodings = vec![ + "UTF8", + "ASCII", + "Unicode", + "BigEndianUnicode", + "UTF32", + "Default", + ]; + + for enc in encodings { + let original = CString::new("Test").unwrap(); // ASCII-safe for all encodings + let encoding = CString::new(enc).unwrap(); + let mut out_length: usize = 0; + + // String to bytes + let bytes_ptr = unsafe { + string_to_bytes( + original.as_ptr(), + encoding.as_ptr(), + &mut out_length as *mut usize, + ) + }; + assert!( + !bytes_ptr.is_null(), + "string_to_bytes should succeed for {}", + enc + ); + + // Bytes to string + let result = unsafe { bytes_to_string(bytes_ptr, out_length, encoding.as_ptr()) }; + assert!( + !result.is_null(), + "bytes_to_string should succeed for {}", + enc + ); + + let result_str = unsafe { CStr::from_ptr(result).to_str().unwrap() }; + assert_eq!( + result_str, "Test", + "Round-trip should preserve string for {}", + enc + ); + + unsafe { + crate::memory::free_bytes(bytes_ptr); + crate::memory::free_string(result); + }; + } + } + + #[test] + fn test_bytes_to_string_case_insensitive_encoding() { + // Test: encoding names should be case-insensitive + let bytes: [u8; 4] = [84, 101, 115, 116]; // "Test" in UTF-8 + let encoding_variants = vec!["utf8", "UTF8", "Utf8", "ascii", "ASCII"]; + + for enc in encoding_variants { + let encoding = CString::new(enc).unwrap(); + + let result = unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + + assert!( + !result.is_null(), + "Encoding '{}' should be recognized (case-insensitive)", + enc + ); + + unsafe { crate::memory::free_string(result) }; + } + } + + #[test] + fn test_bytes_to_string_concurrent_operations() { + use std::thread; + + // Test: multiple threads using bytes_to_string concurrently + let handles: Vec<_> = (0..10) + .map(|i| { + thread::spawn(move || { + let bytes: [u8; 5] = [72, 101, 108, 108, 111]; // "Hello" + let encoding = CString::new("UTF8").unwrap(); + + let result = + unsafe { bytes_to_string(bytes.as_ptr(), bytes.len(), encoding.as_ptr()) }; + assert!(!result.is_null(), "Decoding should succeed in thread {}", i); + + let result_str = unsafe { CStr::from_ptr(result).to_str().unwrap() }; + assert_eq!( + result_str, "Hello", + "Should decode to 'Hello' in thread {}", + i + ); + + unsafe { crate::memory::free_string(result) }; + }) + }) + .collect(); + + for handle in handles { + handle.join().unwrap(); + } + } } diff --git a/mkdocs.yml b/mkdocs.yml index a8fed94..f0ee825 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,7 @@ nav: - ConvertFrom-Base64ToString: functions/ConvertFrom-Base64ToString.md - ConvertFrom-ByteArrayToBase64: functions/ConvertFrom-ByteArrayToBase64.md - ConvertFrom-ByteArrayToMemoryStream: functions/ConvertFrom-ByteArrayToMemoryStream.md + - ConvertFrom-ByteArrayToString: functions/ConvertFrom-ByteArrayToString.md - ConvertFrom-Clixml: functions/ConvertFrom-Clixml.md - ConvertFrom-CompressedByteArrayToString: functions/ConvertFrom-CompressedByteArrayToString.md - ConvertFrom-EscapedUrl: functions/ConvertFrom-EscapedUrl.md diff --git a/src/Convert/Convert.psd1 b/src/Convert/Convert.psd1 index a413ca1..067f869 100644 --- a/src/Convert/Convert.psd1 +++ b/src/Convert/Convert.psd1 @@ -12,7 +12,7 @@ RootModule = 'Convert.psm1' # Version number of this module. - ModuleVersion = '2.0.3' + ModuleVersion = '2.0.4' # Supported PSEditions CompatiblePSEditions = @( @@ -79,6 +79,7 @@ 'ConvertFrom-Base64ToString' 'ConvertFrom-ByteArrayToBase64' 'ConvertFrom-ByteArrayToMemoryStream' + 'ConvertFrom-ByteArrayToString' 'ConvertFrom-CompressedByteArrayToString' 'ConvertFrom-EscapedUrl' 'ConvertFrom-HashTable' diff --git a/src/Convert/Private/RustInterop.ps1 b/src/Convert/Private/RustInterop.ps1 index 98b3368..846d9ec 100644 --- a/src/Convert/Private/RustInterop.ps1 +++ b/src/Convert/Private/RustInterop.ps1 @@ -128,6 +128,12 @@ public static class ConvertCoreInterop { [MarshalAs(UnmanagedType.LPUTF8Str)] string encoding, out UIntPtr length); + [DllImport("$escapedPath", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr bytes_to_string( + IntPtr bytes, + UIntPtr length, + [MarshalAs(UnmanagedType.LPUTF8Str)] string encoding); + // Hash operations [DllImport("$escapedPath", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr compute_hash( diff --git a/src/Convert/Public/ConvertFrom-Base64.ps1 b/src/Convert/Public/ConvertFrom-Base64.ps1 index ae25b3f..06fafe6 100644 --- a/src/Convert/Public/ConvertFrom-Base64.ps1 +++ b/src/Convert/Public/ConvertFrom-Base64.ps1 @@ -87,21 +87,56 @@ function ConvertFrom-Base64 { begin { $userErrorActionPreference = $ErrorActionPreference + $nullPtr = [IntPtr]::Zero } process { foreach ($b64 in $Base64) { try { - $bytes = [System.Convert]::FromBase64String($b64) - - if ($ToString) { - if ($Decompress) { + if ($ToString -and -not $Decompress) { + # Direct base64 to string conversion via Rust + $ptr = $nullPtr + try { + $ptr = [ConvertCoreInterop]::base64_to_string($b64, $Encoding) + + if ($ptr -eq $nullPtr) { + $errorMsg = GetRustError -DefaultMessage "Base64 to string conversion failed for encoding '$Encoding'" + throw $errorMsg + } + + ConvertPtrToString -Ptr $ptr + } finally { + if ($ptr -ne $nullPtr) { + [ConvertCoreInterop]::free_string($ptr) + } + } + } else { + # Get bytes first (for raw output or decompression) + $bytesPtr = $nullPtr + try { + $length = [UIntPtr]::Zero + $bytesPtr = [ConvertCoreInterop]::base64_to_bytes($b64, [ref]$length) + + if ($bytesPtr -eq $nullPtr) { + $errorMsg = GetRustError -DefaultMessage "Base64 decoding failed" + throw $errorMsg + } + + $bytes = New-Object byte[] $length.ToUInt64() + [System.Runtime.InteropServices.Marshal]::Copy($bytesPtr, $bytes, 0, $bytes.Length) + } finally { + if ($bytesPtr -ne $nullPtr) { + [ConvertCoreInterop]::free_bytes($bytesPtr) + } + } + + if ($ToString) { + # Decompress path ConvertFrom-CompressedByteArrayToString -ByteArray $bytes -Encoding $Encoding } else { - [System.Text.Encoding]::$Encoding.GetString($bytes) + # Return raw bytes + $bytes } - } else { - $bytes } } catch { Write-Error -ErrorRecord $_ -ErrorAction $userErrorActionPreference diff --git a/src/Convert/Public/ConvertFrom-Base64ToByteArray.ps1 b/src/Convert/Public/ConvertFrom-Base64ToByteArray.ps1 index d2e5dae..836031a 100644 --- a/src/Convert/Public/ConvertFrom-Base64ToByteArray.ps1 +++ b/src/Convert/Public/ConvertFrom-Base64ToByteArray.ps1 @@ -3,7 +3,7 @@ Converts a Base 64 Encoded String to a Byte Array .DESCRIPTION - Converts a Base 64 Encoded String to a Byte Array + Converts a Base 64 Encoded String to a Byte Array. .PARAMETER String The Base 64 Encoded String to be converted @@ -24,7 +24,7 @@ https://austoonz.github.io/Convert/functions/ConvertFrom-Base64ToByteArray/ #> function ConvertFrom-Base64ToByteArray { - [CmdletBinding()] + [CmdletBinding(HelpUri = 'https://austoonz.github.io/Convert/functions/ConvertFrom-Base64ToByteArray/')] [Alias('ConvertFrom-Base64StringToByteArray')] param ( @@ -40,14 +40,32 @@ function ConvertFrom-Base64ToByteArray { begin { $userErrorActionPreference = $ErrorActionPreference + $nullPtr = [IntPtr]::Zero } process { foreach ($s in $String) { + $ptr = $nullPtr try { - [System.Convert]::FromBase64String($s) + $length = [UIntPtr]::Zero + $ptr = [ConvertCoreInterop]::base64_to_bytes($s, [ref]$length) + + if ($ptr -eq $nullPtr) { + $errorMsg = GetRustError -DefaultMessage "Base64 to byte array conversion failed" + throw $errorMsg + } + + $byteArray = New-Object byte[] $length.ToUInt64() + [System.Runtime.InteropServices.Marshal]::Copy($ptr, $byteArray, 0, $byteArray.Length) + + # Output the byte array (use comma to prevent PowerShell from unrolling) + ,$byteArray } catch { Write-Error -ErrorRecord $_ -ErrorAction $userErrorActionPreference + } finally { + if ($ptr -ne $nullPtr) { + [ConvertCoreInterop]::free_bytes($ptr) + } } } } diff --git a/src/Convert/Public/ConvertFrom-ByteArrayToString.ps1 b/src/Convert/Public/ConvertFrom-ByteArrayToString.ps1 new file mode 100644 index 0000000..5265cde --- /dev/null +++ b/src/Convert/Public/ConvertFrom-ByteArrayToString.ps1 @@ -0,0 +1,95 @@ +<# + .SYNOPSIS + Converts a byte array to a string using the specified encoding. + + .DESCRIPTION + Converts a byte array to a string using the specified encoding. + This is the inverse operation of ConvertFrom-StringToByteArray. + + .PARAMETER ByteArray + The array of bytes to convert. + + .PARAMETER Encoding + The encoding to use for conversion. + Defaults to UTF8. + Valid options are ASCII, BigEndianUnicode, Default, Unicode, UTF32, and UTF8. + + .EXAMPLE + $bytes = [byte[]]@(72, 101, 108, 108, 111) + ConvertFrom-ByteArrayToString -ByteArray $bytes + + Hello + + .EXAMPLE + $bytes = ConvertFrom-StringToByteArray -String 'Hello, World!' + ConvertFrom-ByteArrayToString -ByteArray $bytes + + Hello, World! + + .EXAMPLE + $bytes1, $bytes2 | ConvertFrom-ByteArrayToString -Encoding 'UTF8' + + Converts multiple byte arrays from the pipeline to strings. + + .OUTPUTS + [String] + + .LINK + https://austoonz.github.io/Convert/functions/ConvertFrom-ByteArrayToString/ +#> +function ConvertFrom-ByteArrayToString { + [CmdletBinding(HelpUri = 'https://austoonz.github.io/Convert/functions/ConvertFrom-ByteArrayToString/')] + param + ( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [Byte[]] + $ByteArray, + + [ValidateSet('ASCII', 'BigEndianUnicode', 'Default', 'Unicode', 'UTF32', 'UTF8')] + [String] + $Encoding = 'UTF8' + ) + + begin { + $userErrorActionPreference = $ErrorActionPreference + $nullPtr = [IntPtr]::Zero + } + + process { + try { + $ptr = $nullPtr + + try { + # Pin the byte array in memory to prevent garbage collection during FFI call + $pinnedArray = [System.Runtime.InteropServices.GCHandle]::Alloc($ByteArray, [System.Runtime.InteropServices.GCHandleType]::Pinned) + try { + $byteArrayPtr = $pinnedArray.AddrOfPinnedObject() + $length = [UIntPtr]::new($ByteArray.Length) + + $ptr = [ConvertCoreInterop]::bytes_to_string($byteArrayPtr, $length, $Encoding) + + if ($ptr -eq $nullPtr) { + $errorMsg = GetRustError -DefaultMessage "Byte array to string conversion failed for encoding '$Encoding'" + throw $errorMsg + } + + ConvertPtrToString -Ptr $ptr + } finally { + if ($pinnedArray.IsAllocated) { + $pinnedArray.Free() + } + } + } finally { + if ($ptr -ne $nullPtr) { + [ConvertCoreInterop]::free_string($ptr) + } + } + } catch { + Write-Error -ErrorRecord $_ -ErrorAction $userErrorActionPreference + } + } +} diff --git a/src/Convert/Public/ConvertFrom-StringToByteArray.ps1 b/src/Convert/Public/ConvertFrom-StringToByteArray.ps1 index 41f0a91..df45cf1 100644 --- a/src/Convert/Public/ConvertFrom-StringToByteArray.ps1 +++ b/src/Convert/Public/ConvertFrom-StringToByteArray.ps1 @@ -4,6 +4,7 @@ .DESCRIPTION Converts a string to a byte array object. + This is the inverse operation of ConvertFrom-ByteArrayToString. .PARAMETER String A string object for conversion. diff --git a/src/Tests/Build/Build.Tests.ps1 b/src/Tests/Build/Build.Tests.ps1 index 2151604..c79c3e1 100644 --- a/src/Tests/Build/Build.Tests.ps1 +++ b/src/Tests/Build/Build.Tests.ps1 @@ -8,7 +8,7 @@ Describe -Name 'Module Manifest' -Fixture { Context -Name 'Exported Functions' -Fixture { It -Name 'Exports the correct number of functions' -Test { $assertion = Get-Command -Module $script:ModuleName -CommandType Function - $assertion | Should -HaveCount 30 + $assertion | Should -HaveCount 31 } It -Name '<_>' -TestCases @( diff --git a/src/Tests/Unit/ConvertFrom-Base64.Tests.ps1 b/src/Tests/Unit/ConvertFrom-Base64.Tests.ps1 index bdadaa5..d4310e1 100644 --- a/src/Tests/Unit/ConvertFrom-Base64.Tests.ps1 +++ b/src/Tests/Unit/ConvertFrom-Base64.Tests.ps1 @@ -79,12 +79,7 @@ Describe -Name $function -Fixture { } $assertion = ConvertFrom-Base64 @splat -ErrorAction Continue 2>&1 - $ExpectedList = @( - 'Invalid length for a Base-64 char array or string.', - 'The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.' - ) - - $assertion.Exception.InnerException.Message | Should -BeIn $ExpectedList + $assertion[0].Exception.Message | Should -BeLike 'Failed to decode Base64:*' } } @@ -156,12 +151,7 @@ Describe -Name $function -Fixture { } $assertion = ConvertFrom-Base64 @splat -ErrorAction Continue 2>&1 - $ExpectedList = @( - 'Invalid length for a Base-64 char array or string.', - 'The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.' - ) - - $assertion.Exception.InnerException.Message | Should -BeIn $ExpectedList + $assertion[0].Exception.Message | Should -BeLike 'Failed to decode Base64:*' } } diff --git a/src/Tests/Unit/ConvertFrom-Base64ToMemoryStream.Tests.ps1 b/src/Tests/Unit/ConvertFrom-Base64ToMemoryStream.Tests.ps1 index 09f28e4..1ec501a 100644 --- a/src/Tests/Unit/ConvertFrom-Base64ToMemoryStream.Tests.ps1 +++ b/src/Tests/Unit/ConvertFrom-Base64ToMemoryStream.Tests.ps1 @@ -32,14 +32,7 @@ Describe -Name $function -Fixture { It -Name 'Supports EAP Continue' -Test { $assertion = ConvertFrom-Base64ToMemoryStream -String ([int]1) -ErrorAction Continue 2>&1 - $exception = @( - # PowerShell - 'The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.' - - # Windows PowerShell - 'Invalid length for a Base-64 char array or string.' - ) - $assertion[0].Exception.InnerException.Message | Should -BeIn $exception + $assertion[0].Exception.Message | Should -BeLike 'Failed to decode Base64:*' } } } diff --git a/src/Tests/Unit/ConvertFrom-ByteArrayToString.Tests.ps1 b/src/Tests/Unit/ConvertFrom-ByteArrayToString.Tests.ps1 new file mode 100644 index 0000000..6b14444 --- /dev/null +++ b/src/Tests/Unit/ConvertFrom-ByteArrayToString.Tests.ps1 @@ -0,0 +1,242 @@ +$function = $MyInvocation.MyCommand.Name.Split('.')[0] + +Describe -Name $function -Fixture { + BeforeEach { + $String = 'ThisIsMyString' + + # Use the variables so IDE does not complain + $null = $String + } + + Context -Name '' -ForEach @( + @{ + Encoding = 'ASCII' + ByteArray = [byte[]]@(84, 104, 105, 115, 73, 115, 77, 121, 83, 116, 114, 105, 110, 103) + } + @{ + Encoding = 'BigEndianUnicode' + ByteArray = [byte[]]@(0, 84, 0, 104, 0, 105, 0, 115, 0, 73, 0, 115, 0, 77, 0, 121, 0, 83, 0, 116, 0, 114, 0, 105, 0, 110, 0, 103) + } + @{ + Encoding = 'Default' + ByteArray = [byte[]]@(84, 104, 105, 115, 73, 115, 77, 121, 83, 116, 114, 105, 110, 103) + } + @{ + Encoding = 'Unicode' + ByteArray = [byte[]]@(84, 0, 104, 0, 105, 0, 115, 0, 73, 0, 115, 0, 77, 0, 121, 0, 83, 0, 116, 0, 114, 0, 105, 0, 110, 0, 103, 0) + } + @{ + Encoding = 'UTF32' + ByteArray = [byte[]]@(84, 0, 0, 0, 104, 0, 0, 0, 105, 0, 0, 0, 115, 0, 0, 0, 73, 0, 0, 0, 115, 0, 0, 0, 77, 0, 0, 0, 121, 0, 0, 0, 83, 0, 0, 0, 116, 0, 0, 0, 114, 0, 0, 0, 105, 0, 0, 0, 110, 0, 0, 0, 103, 0, 0, 0) + } + @{ + Encoding = 'UTF8' + ByteArray = [byte[]]@(84, 104, 105, 115, 73, 115, 77, 121, 83, 116, 114, 105, 110, 103) + } + ) -Fixture { + It -Name 'Converts a Encoded byte array to a string' -Test { + $splat = @{ + ByteArray = $ByteArray + Encoding = $Encoding + } + $assertion = ConvertFrom-ByteArrayToString @splat + $assertion | Should -BeExactly $String + } + + It -Name 'Supports the Pipeline' -Test { + $assertion = ,$ByteArray | ConvertFrom-ByteArrayToString -Encoding $Encoding + $assertion | Should -BeExactly $String + } + + It -Name 'Outputs an array of strings for multiple byte arrays' -Test { + $assertion = @($ByteArray, $ByteArray) | ConvertFrom-ByteArrayToString -Encoding $Encoding + $assertion.Count | Should -BeExactly 2 + $assertion[0] | Should -BeExactly $String + $assertion[1] | Should -BeExactly $String + } + } + + Context -Name 'Round-Trip Validation' -Fixture { + It -Name 'Round-trips correctly with encoding' -ForEach @( + @{Encoding = 'UTF8'} + @{Encoding = 'ASCII'} + @{Encoding = 'Unicode'} + @{Encoding = 'BigEndianUnicode'} + @{Encoding = 'UTF32'} + @{Encoding = 'Default'} + ) -Test { + $original = 'RoundTripTest' + $bytes = ConvertFrom-StringToByteArray -String $original -Encoding $Encoding + $result = ConvertFrom-ByteArrayToString -ByteArray $bytes -Encoding $Encoding + $result | Should -BeExactly $original + } + + It -Name 'Round-trips Unicode characters (emoji) with UTF8' -Test { + $original = 'Hello 🌍' + $bytes = ConvertFrom-StringToByteArray -String $original -Encoding 'UTF8' + $result = ConvertFrom-ByteArrayToString -ByteArray $bytes -Encoding 'UTF8' + $result | Should -BeExactly $original + } + + It -Name 'Round-trips from Base64 → ByteArray → String' -Test { + $original = 'Hello, World!' + $base64 = ConvertFrom-StringToBase64 -String $original -Encoding 'UTF8' + $bytes = ConvertFrom-Base64ToByteArray -String $base64 + $result = ConvertFrom-ByteArrayToString -ByteArray $bytes -Encoding 'UTF8' + $result | Should -BeExactly $original + } + } + + Context -Name 'Edge Cases' -Fixture { + It -Name 'Throws on empty byte array' -Test { + $emptyBytes = [byte[]]@() + { ConvertFrom-ByteArrayToString -ByteArray $emptyBytes -Encoding 'UTF8' } | Should -Throw + } + + It -Name 'Handles large byte array (1MB)' -Test { + $largeBytes = [byte[]]::new(1024 * 1024) + for ($i = 0; $i -lt $largeBytes.Length; $i++) { + $largeBytes[$i] = 65 # 'A' in ASCII/UTF8 + } + $result = ConvertFrom-ByteArrayToString -ByteArray $largeBytes -Encoding 'UTF8' + $result | Should -Not -BeNullOrEmpty + $result.Length | Should -Be (1024 * 1024) + } + + It -Name 'Handles special characters (tabs, newlines)' -Test { + # "Hello`t`n`r" in UTF-8 bytes + $specialBytes = [byte[]]@(72, 101, 108, 108, 111, 9, 10, 13) + $result = ConvertFrom-ByteArrayToString -ByteArray $specialBytes -Encoding 'UTF8' + $result | Should -BeExactly "Hello`t`n`r" + } + + It -Name 'Handles Unicode characters (emoji)' -Test { + # 🌍 = F0 9F 8C 8D in UTF-8 + $emojiBytes = [byte[]]@(0xF0, 0x9F, 0x8C, 0x8D) + $result = ConvertFrom-ByteArrayToString -ByteArray $emojiBytes -Encoding 'UTF8' + $result | Should -BeExactly '🌍' + } + + It -Name 'Handles whitespace-only content' -Test { + $whitespaceBytes = [byte[]]@(32, 32, 32) # Three spaces + $result = ConvertFrom-ByteArrayToString -ByteArray $whitespaceBytes -Encoding 'UTF8' + $result | Should -BeExactly ' ' + } + } + + Context -Name 'Error Handling' -Fixture { + It -Name 'Respects ErrorAction parameter - Stop' -Test { + $bytes = [byte[]]@(72, 101, 108, 108, 111) + $result = ConvertFrom-ByteArrayToString -ByteArray $bytes -Encoding 'UTF8' -ErrorAction Stop + $result | Should -Not -BeNullOrEmpty + } + + It -Name 'Respects ErrorAction parameter - SilentlyContinue' -Test { + $bytes = [byte[]]@(72, 101, 108, 108, 111) + $result = ConvertFrom-ByteArrayToString -ByteArray $bytes -Encoding 'UTF8' -ErrorAction SilentlyContinue + $result | Should -Not -BeNullOrEmpty + } + + It -Name 'Handles null input gracefully' -Test { + # PowerShell parameter validation should catch this + { ConvertFrom-ByteArrayToString -ByteArray $null -Encoding 'UTF8' } | Should -Throw + } + + It -Name 'Throws on invalid UTF-8 byte sequence' -Test { + # Invalid UTF-8 sequence + $invalidBytes = [byte[]]@(0xFF, 0xFE) + { ConvertFrom-ByteArrayToString -ByteArray $invalidBytes -Encoding 'UTF8' -ErrorAction Stop } | Should -Throw + } + + It -Name 'Throws on invalid UTF-16 byte length (odd)' -Test { + # Odd number of bytes is invalid for UTF-16 + $invalidBytes = [byte[]]@(72, 0, 101) + { ConvertFrom-ByteArrayToString -ByteArray $invalidBytes -Encoding 'Unicode' -ErrorAction Stop } | Should -Throw + } + + It -Name 'Throws on non-ASCII bytes with ASCII encoding' -Test { + # Byte > 127 is invalid for ASCII + $invalidBytes = [byte[]]@(72, 200, 111) + { ConvertFrom-ByteArrayToString -ByteArray $invalidBytes -Encoding 'ASCII' -ErrorAction Stop } | Should -Throw + } + } + + Context -Name 'Performance' -Fixture { + It -Name 'Processes large batch efficiently (100+ items in <5 seconds)' -Test { + $items = 1..100 | ForEach-Object { + [byte[]]@(84, 101, 115, 116, 83, 116, 114, 105, 110, 103) # "TestString" + } + + $measure = Measure-Command { + $null = $items | ForEach-Object { ConvertFrom-ByteArrayToString -ByteArray $_ -Encoding 'UTF8' } + } + + $measure.TotalSeconds | Should -BeLessThan 5 + } + + It -Name 'Handles very large byte array (1MB+) in <2 seconds' -Test { + $largeBytes = [byte[]]::new(1024 * 1024) + for ($i = 0; $i -lt $largeBytes.Length; $i++) { + $largeBytes[$i] = 65 # 'A' + } + + $measure = Measure-Command { + $null = ConvertFrom-ByteArrayToString -ByteArray $largeBytes -Encoding 'UTF8' + } + + $measure.TotalSeconds | Should -BeLessThan 2 + } + } + + Context -Name 'Memory Management' -Fixture { + It -Name 'Processes repeated calls without memory leaks (1000 iterations)' -Test { + [System.GC]::Collect() + [System.GC]::WaitForPendingFinalizers() + [System.GC]::Collect() + + $process = Get-Process -Id $PID + $memoryBefore = $process.WorkingSet64 + + $testBytes = [byte[]]@(84, 101, 115, 116, 83, 116, 114, 105, 110, 103) + 1..1000 | ForEach-Object { + $null = ConvertFrom-ByteArrayToString -ByteArray $testBytes -Encoding 'UTF8' + } + + [System.GC]::Collect() + [System.GC]::WaitForPendingFinalizers() + [System.GC]::Collect() + + $process.Refresh() + $memoryAfter = $process.WorkingSet64 + + $memoryGrowthMB = [Math]::Round(($memoryAfter - $memoryBefore) / 1MB, 2) + + $memoryGrowthMB | Should -BeLessThan 30 + } + } + + Context -Name 'Data Integrity' -Fixture { + It -Name 'Produces consistent output across multiple calls' -Test { + $testBytes = [byte[]]@(67, 111, 110, 115, 105, 115, 116, 101, 110, 99, 121) # "Consistency" + $result1 = ConvertFrom-ByteArrayToString -ByteArray $testBytes -Encoding 'UTF8' + $result2 = ConvertFrom-ByteArrayToString -ByteArray $testBytes -Encoding 'UTF8' + + $result1 | Should -BeExactly $result2 + } + + It -Name 'Returns String type' -Test { + $testBytes = [byte[]]@(84, 101, 115, 116) # "Test" + $result = ConvertFrom-ByteArrayToString -ByteArray $testBytes -Encoding 'UTF8' + $result -is [string] | Should -Be $true + } + + It -Name 'Outputs array of strings when given multiple byte arrays' -Test { + $bytes1 = [byte[]]@(72, 101, 108, 108, 111) # "Hello" + $bytes2 = [byte[]]@(87, 111, 114, 108, 100) # "World" + $result = @($bytes1, $bytes2) | ConvertFrom-ByteArrayToString -Encoding 'UTF8' + $result.Count | Should -Be 2 + $result[0] | Should -BeExactly 'Hello' + $result[1] | Should -BeExactly 'World' + } + } +} diff --git a/src/Tests/Unit/Global.Tests.ps1 b/src/Tests/Unit/Global.Tests.ps1 index 019ef8a..ecee9b0 100644 --- a/src/Tests/Unit/Global.Tests.ps1 +++ b/src/Tests/Unit/Global.Tests.ps1 @@ -39,7 +39,7 @@ Describe -Name 'Module Manifest' -Fixture { Context -Name 'Exported Functions' -Fixture { It -Name 'Exports the correct number of functions' -Test { $assertion = Get-Command -Module $script:ModuleName -CommandType Function - $assertion | Should -HaveCount 30 + $assertion | Should -HaveCount 31 } It -Name '<_>' -TestCases @(