99 "errors"
1010 "strconv"
1111 "unicode/utf16"
12+ "unicode/utf8"
1213 "unsafe"
1314
1415 "github.com/microsoft/go-sqlcmd/internal/localizer"
@@ -17,6 +18,15 @@ import (
1718 "golang.org/x/text/transform"
1819)
1920
21+ const (
22+ // MB_ERR_INVALID_CHARS causes MultiByteToWideChar to fail if it encounters
23+ // an invalid character in the source string (including incomplete sequences)
24+ mbErrInvalidChars = 0x00000008
25+ // Maximum bytes that might form a single character in any Windows codepage
26+ // (most DBCS codepages use 2 bytes, but we use 4 for safety)
27+ maxMultibyteCharLen = 4
28+ )
29+
2030var (
2131 kernel32 = windows .NewLazySystemDLL ("kernel32.dll" )
2232 procMultiByteToWideChar = kernel32 .NewProc ("MultiByteToWideChar" )
@@ -36,43 +46,113 @@ func (e *windowsCodePageEncoding) NewEncoder() *encoding.Encoder {
3646 return & encoding.Encoder {Transformer : & windowsEncoder {codepage : e .codepage }}
3747}
3848
39- // windowsDecoder converts from a Windows codepage to UTF-8
49+ // windowsDecoder converts from a Windows codepage to UTF-8.
50+ // It buffers incomplete multibyte sequences between Transform calls.
4051type windowsDecoder struct {
4152 codepage uint32
53+ buf [maxMultibyteCharLen ]byte // buffer for incomplete sequences
54+ bufLen int // number of bytes in buffer
4255}
4356
44- func (d * windowsDecoder ) Reset () {}
57+ func (d * windowsDecoder ) Reset () {
58+ d .bufLen = 0
59+ }
4560
4661func (d * windowsDecoder ) Transform (dst , src []byte , atEOF bool ) (nDst , nSrc int , err error ) {
47- if len (src ) == 0 {
62+ // Prepend any buffered bytes from previous call
63+ var input []byte
64+ if d .bufLen > 0 {
65+ input = make ([]byte , d .bufLen + len (src ))
66+ copy (input , d .buf [:d .bufLen ])
67+ copy (input [d .bufLen :], src )
68+ } else {
69+ input = src
70+ }
71+
72+ if len (input ) == 0 {
4873 return 0 , 0 , nil
4974 }
5075
51- // First call to get required buffer size for wide chars
76+ // Try to convert with MB_ERR_INVALID_CHARS to detect incomplete sequences
5277 n , _ , errno := procMultiByteToWideChar .Call (
5378 uintptr (d .codepage ),
54- 0 ,
55- uintptr (unsafe .Pointer (& src [0 ])),
56- uintptr (len (src )),
79+ mbErrInvalidChars ,
80+ uintptr (unsafe .Pointer (& input [0 ])),
81+ uintptr (len (input )),
5782 0 ,
5883 0 ,
5984 )
60- if n == 0 {
85+
86+ // If conversion failed, it might be due to incomplete trailing sequence
87+ if n == 0 && errno == windows .ERROR_NO_UNICODE_TRANSLATION {
88+ if atEOF {
89+ // At EOF with incomplete sequence - this is an error
90+ d .bufLen = 0
91+ return 0 , len (src ), errors .New ("incomplete multibyte sequence at end of input" )
92+ }
93+
94+ // Not at EOF - try removing bytes from the end until conversion succeeds
95+ // This finds the incomplete trailing sequence
96+ for trimLen := 1 ; trimLen <= len (input ) && trimLen <= maxMultibyteCharLen ; trimLen ++ {
97+ tryLen := len (input ) - trimLen
98+ if tryLen <= 0 {
99+ // Need more input - buffer what we have
100+ if len (input ) <= maxMultibyteCharLen {
101+ copy (d .buf [:], input )
102+ d .bufLen = len (input )
103+ return 0 , len (src ), transform .ErrShortSrc
104+ }
105+ break
106+ }
107+
108+ n , _ , errno = procMultiByteToWideChar .Call (
109+ uintptr (d .codepage ),
110+ mbErrInvalidChars ,
111+ uintptr (unsafe .Pointer (& input [0 ])),
112+ uintptr (tryLen ),
113+ 0 ,
114+ 0 ,
115+ )
116+ if n > 0 || errno != windows .ERROR_NO_UNICODE_TRANSLATION {
117+ // Found a valid prefix - buffer the trailing bytes
118+ trailingBytes := input [tryLen :]
119+ copy (d .buf [:], trailingBytes )
120+ d .bufLen = len (trailingBytes )
121+ input = input [:tryLen ]
122+ break
123+ }
124+ }
125+
126+ // If still failing, buffer everything and wait for more
127+ if n == 0 {
128+ if len (input ) <= maxMultibyteCharLen {
129+ copy (d .buf [:], input )
130+ d .bufLen = len (input )
131+ return 0 , len (src ), transform .ErrShortSrc
132+ }
133+ // Input is larger than max char length but still invalid - real error
134+ d .bufLen = 0
135+ return 0 , len (src ), errors .New ("invalid multibyte sequence" )
136+ }
137+ } else if n == 0 {
61138 if errno != windows .ERROR_SUCCESS {
139+ d .bufLen = 0
62140 return 0 , 0 , errno
63141 }
142+ d .bufLen = 0
64143 return 0 , 0 , errors .New ("MultiByteToWideChar failed" )
144+ } else {
145+ // Success - clear buffer since we'll consume all input
146+ d .bufLen = 0
65147 }
66148
67- // Allocate wide char buffer
149+ // Allocate wide char buffer and do the actual conversion
68150 wideChars := make ([]uint16 , n )
69-
70- // Convert to wide chars
71151 n , _ , errno = procMultiByteToWideChar .Call (
72152 uintptr (d .codepage ),
73- 0 ,
74- uintptr (unsafe .Pointer (& src [0 ])),
75- uintptr (len (src )),
153+ 0 , // Don't use MB_ERR_INVALID_CHARS here - we already validated
154+ uintptr (unsafe .Pointer (& input [0 ])),
155+ uintptr (len (input )),
76156 uintptr (unsafe .Pointer (& wideChars [0 ])),
77157 uintptr (len (wideChars )),
78158 )
@@ -92,23 +172,71 @@ func (d *windowsDecoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int,
92172 }
93173
94174 copy (dst , utf8Bytes )
95- return len (utf8Bytes ), len (src ), nil
175+ return len (utf8Bytes ), len (src ), err
96176}
97177
98- // windowsEncoder converts from UTF-8 to a Windows codepage
178+ // windowsEncoder converts from UTF-8 to a Windows codepage.
179+ // It buffers incomplete UTF-8 sequences between Transform calls.
99180type windowsEncoder struct {
100181 codepage uint32
182+ buf [utf8 .UTFMax ]byte // buffer for incomplete UTF-8 sequences
183+ bufLen int // number of bytes in buffer
101184}
102185
103- func (e * windowsEncoder ) Reset () {}
186+ func (e * windowsEncoder ) Reset () {
187+ e .bufLen = 0
188+ }
104189
105190func (e * windowsEncoder ) Transform (dst , src []byte , atEOF bool ) (nDst , nSrc int , err error ) {
106- if len (src ) == 0 {
191+ // Prepend any buffered bytes from previous call
192+ var input []byte
193+ if e .bufLen > 0 {
194+ input = make ([]byte , e .bufLen + len (src ))
195+ copy (input , e .buf [:e .bufLen ])
196+ copy (input [e .bufLen :], src )
197+ } else {
198+ input = src
199+ }
200+
201+ if len (input ) == 0 {
107202 return 0 , 0 , nil
108203 }
109204
205+ // Find the last complete UTF-8 sequence
206+ validLen := len (input )
207+ for validLen > 0 && ! utf8 .Valid (input [:validLen ]) {
208+ validLen --
209+ }
210+
211+ // Check for incomplete trailing sequence
212+ if validLen < len (input ) {
213+ trailingBytes := input [validLen :]
214+ if atEOF {
215+ // At EOF with incomplete UTF-8 - this is an error
216+ e .bufLen = 0
217+ return 0 , len (src ), errors .New ("incomplete UTF-8 sequence at end of input" )
218+ }
219+ // Buffer the incomplete trailing bytes for next call
220+ if len (trailingBytes ) <= utf8 .UTFMax {
221+ copy (e .buf [:], trailingBytes )
222+ e .bufLen = len (trailingBytes )
223+ } else {
224+ // Shouldn't happen with valid partial UTF-8, but handle it
225+ e .bufLen = 0
226+ return 0 , len (src ), errors .New ("invalid UTF-8 sequence" )
227+ }
228+ input = input [:validLen ]
229+ } else {
230+ e .bufLen = 0
231+ }
232+
233+ if len (input ) == 0 {
234+ // Only incomplete sequence - need more input
235+ return 0 , len (src ), transform .ErrShortSrc
236+ }
237+
110238 // Convert UTF-8 to UTF-16
111- runes := []rune (string (src ))
239+ runes := []rune (string (input ))
112240 wideChars := utf16 .Encode (runes )
113241
114242 if len (wideChars ) == 0 {
@@ -155,7 +283,7 @@ func (e *windowsEncoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int,
155283 return 0 , 0 , errors .New ("WideCharToMultiByte failed" )
156284 }
157285
158- return int (n ), len (src ), nil
286+ return int (n ), len (src ), err
159287}
160288
161289// isCodePageValid checks if a codepage is valid/installed on Windows
0 commit comments