33// See LICENSE.txt for details or visit http://www.opensource.org/licenses/ms-pl.html for MS-PL and http://opensource.org/licenses/Apache-2.0 for Apache 2.0.
44// https://github.com/JoshClose/CsvHelper
55using CsvHelper . Configuration ;
6+ using System . Globalization ;
67using System . Text ;
78
89namespace CsvHelper . TypeConversion ;
@@ -12,6 +13,7 @@ namespace CsvHelper.TypeConversion;
1213/// </summary>
1314public class ByteArrayConverter : DefaultTypeConverter
1415{
16+ private readonly char [ ] HexDigits = "0123456789ABCDEF" . ToCharArray ( ) ;
1517 private readonly ByteArrayConverterOptions options ;
1618 private readonly string HexStringPrefix ;
1719 private readonly byte ByteLength ;
@@ -37,13 +39,29 @@ public ByteArrayConverter(ByteArrayConverterOptions options = ByteArrayConverter
3739 /// <param name="row">The <see cref="IWriterRow"/> for the current record.</param>
3840 /// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being written.</param>
3941 /// <returns>The string representation of the object.</returns>
40- public override string ? ConvertToString ( object ? value , IWriterRow row , MemberMapData memberMapData )
42+ public override ReadOnlySpan < char > ConvertToString ( object ? value , IWriterRow row , MemberMapData memberMapData )
4143 {
4244 if ( value is byte [ ] byteArray )
4345 {
44- return ( options & ByteArrayConverterOptions . Base64 ) == ByteArrayConverterOptions . Base64
45- ? Convert . ToBase64String ( byteArray )
46- : ByteArrayToHexString ( byteArray ) ;
46+ if ( ( options & ByteArrayConverterOptions . Base64 ) == ByteArrayConverterOptions . Base64 )
47+ {
48+ #if NET8_0_OR_GREATER
49+ var length = GetBase64Length ( byteArray ) ;
50+ EnsureBufferSize ( length ) ;
51+ if ( Convert . TryToBase64Chars ( byteArray . AsSpan ( ) , Buffer . AsSpan ( ) , out int charsWritten ) )
52+ {
53+ return Buffer . AsSpan ( 0 , charsWritten ) ;
54+ }
55+ else
56+ {
57+ throw new InvalidOperationException ( "Failed to convert byte array to Base64 string." ) ;
58+ }
59+ #else
60+ return Convert . ToBase64String ( byteArray ) . AsSpan ( ) ;
61+ #endif
62+ }
63+
64+ return BytesToHex ( byteArray ) ;
4765 }
4866
4967 return base . ConvertToString ( value , row , memberMapData ) ;
@@ -56,56 +74,110 @@ public ByteArrayConverter(ByteArrayConverterOptions options = ByteArrayConverter
5674 /// <param name="row">The <see cref="IReaderRow"/> for the current record.</param>
5775 /// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being created.</param>
5876 /// <returns>The object created from the string.</returns>
59- public override object ? ConvertFromString ( string ? text , IReaderRow row , MemberMapData memberMapData )
77+ public override object ? ConvertFromString ( ReadOnlySpan < char > text , IReaderRow row , MemberMapData memberMapData )
6078 {
61- if ( text != null )
79+ if ( ( options & ByteArrayConverterOptions . Base64 ) == ByteArrayConverterOptions . Base64 )
6280 {
63- return ( options & ByteArrayConverterOptions . Base64 ) == ByteArrayConverterOptions . Base64
64- ? Convert . FromBase64String ( text )
65- : HexStringToByteArray ( text ) ;
81+ #if NET8_0_OR_GREATER
82+ var length = ( text . Length * 3 ) / 4 ;
83+ var buffer = new byte [ length ] ;
84+ var bufferSpan = new Span < byte > ( buffer ) ;
85+ if ( Convert . TryFromBase64Chars ( text , bufferSpan , out int bytesWritten ) )
86+ {
87+ return bufferSpan . Slice ( 0 , bytesWritten ) . ToArray ( ) ;
88+ }
89+ else
90+ {
91+ throw new FormatException ( "Invalid Base64 string." ) ;
92+ }
93+ #else
94+ return Convert . FromBase64String ( text . ToString ( ) ) ;
95+ #endif
6696 }
6797
68- return base . ConvertFromString ( text , row , memberMapData ) ;
98+ return HexToBytes ( text ) ;
6999 }
70100
71- private string ByteArrayToHexString ( byte [ ] byteArray )
101+ private ReadOnlySpan < char > BytesToHex ( byte [ ] bytes )
72102 {
73- var hexString = new StringBuilder ( ) ;
74-
75- if ( ( options & ByteArrayConverterOptions . HexInclude0x ) == ByteArrayConverterOptions . HexInclude0x )
103+ if ( bytes . Length == 0 )
76104 {
77- hexString . Append ( "0x" ) ;
105+ return ReadOnlySpan < char > . Empty ;
78106 }
79107
80- if ( byteArray . Length >= 1 )
108+ var length = bytes . Length * 2 ;
109+ if ( ( options & ByteArrayConverterOptions . HexInclude0x ) == ByteArrayConverterOptions . HexInclude0x )
81110 {
82- hexString . Append ( byteArray [ 0 ] . ToString ( "X2" ) ) ;
111+ length += 2 ;
83112 }
84113
85- for ( var i = 1 ; i < byteArray . Length ; i ++ )
114+ var result = new char [ length ] ;
115+ result [ 0 ] = '0' ;
116+ result [ 1 ] = 'x' ;
117+
118+ for ( int i = 0 ; i < bytes . Length ; i ++ )
86119 {
87- hexString . Append ( HexStringPrefix + byteArray [ i ] . ToString ( "X2" ) ) ;
120+ byte b = bytes [ i ] ;
121+ result [ i * 2 ] = HexDigits [ b >> 4 ] ; // High nibble
122+ result [ i * 2 + 1 ] = HexDigits [ b & 0xF ] ; // Low nibble
88123 }
89124
90- return hexString . ToString ( ) ;
125+ return result ;
91126 }
92127
93- private byte [ ] HexStringToByteArray ( string hex )
128+ private byte [ ] HexToBytes ( ReadOnlySpan < char > hex )
94129 {
95- var has0x = hex . StartsWith ( "0x" ) ;
130+ hex = hex . Trim ( ) ;
96131
97- var length = has0x
98- ? ( hex . Length - 1 ) / ByteLength
99- : hex . Length + 1 / ByteLength ;
100- var byteArray = new byte [ length ] ;
101- var has0xOffset = has0x ? 1 : 0 ;
132+ Span < char > prefix = [ '0' , 'x' ] ;
133+ if ( hex . StartsWith ( prefix , StringComparison . OrdinalIgnoreCase ) )
134+ {
135+ hex = hex . Slice ( 2 ) ;
136+ }
102137
103- for ( var stringIndex = has0xOffset * 2 ; stringIndex < hex . Length ; stringIndex += ByteLength )
138+ if ( hex . IsEmpty )
104139 {
105- byteArray [ ( stringIndex - has0xOffset ) / ByteLength ] = Convert . ToByte ( hex . Substring ( stringIndex , 2 ) , 16 ) ;
140+ return Array . Empty < byte > ( ) ;
106141 }
107142
108- return byteArray ;
143+ if ( hex . Length % 2 != 0 )
144+ {
145+ throw new ArgumentException ( "Hex string must have an even number of characters." , nameof ( hex ) ) ;
146+ }
147+
148+ var result = new byte [ hex . Length / 2 ] ;
149+
150+ for ( int i = 0 ; i < hex . Length ; i += 2 )
151+ {
152+ var hexPair = hex . Slice ( i , 2 ) ;
153+
154+ try
155+ {
156+ result [ i / 2 ] = byte . Parse (
157+ #if NET8_0_OR_GREATER
158+ hexPair
159+ #else
160+ hexPair . ToString ( )
161+ #endif
162+ , NumberStyles . HexNumber ) ;
163+ }
164+ catch ( FormatException )
165+ {
166+ throw new FormatException ( $ "Invalid hex characters at position { i } : '{ hexPair . ToString ( ) } '.") ;
167+ }
168+ }
169+
170+ return result ;
171+ }
172+
173+ private int GetBase64Length ( byte [ ] bytes )
174+ {
175+ if ( bytes == null || bytes . Length == 0 )
176+ {
177+ return 0 ;
178+ }
179+
180+ return ( ( bytes . Length + 2 ) / 3 ) * 4 ;
109181 }
110182
111183 private void ValidateOptions ( )
@@ -121,4 +193,6 @@ private void ValidateOptions()
121193 }
122194 }
123195 }
196+
197+ private
124198}
0 commit comments