changed README.md
 
@@ -1,18 +1,10 @@
1
1
# Tds - MSSQL Driver for Elixir
2
2
3
3
[![Hex.pm](https://img.shields.io/hexpm/v/tds.svg)](https://hex.pm/packages/tds)
4
- [![Coverage Status](https://coveralls.io/repos/github/livehelpnow/tds/badge.svg?branch=support-1.1)](https://coveralls.io/github/livehelpnow/tds?branch=master)
5
- ![Elixir TDS CI](https://github.com/livehelpnow/tds/workflows/Elixir%20TDS%20CI/badge.svg)
4
+ ![Elixir TDS CI](https://github.com/elixir-ecto/tds/workflows/Elixir%20TDS%20CI/badge.svg)
6
5
7
6
MSSQL / TDS Database driver for Elixir.
8
7
9
- ### NOTE:
10
- Since TDS version 2.0, `tds_ecto` package is deprecated, this version supports `ecto_sql` since version 3.3.4.
11
-
12
- Please check out the issues for a more complete overview. This branch should not be considered stable or ready for production yet.
13
-
14
- For stable versions always use [hex.pm](https://hex.pm/packages/tds) as source for your mix.exs!!!
15
-
16
8
## Usage
17
9
18
10
 
@@ -98,6 +90,35 @@ This will skip calling `sp_prepare` and query will be executed using `sp_execute
98
90
Please note that only one execution mode can be set at a time, and SQL Server will probably
99
91
use single execution plan (since it is NOT estimated by checking data density!).
100
92
93
+ ## SSL / TLS support
94
+
95
+ tds `>= 2.3.0` supports encrypted connections to the SQL Server.
96
+
97
+ The following encryption behaviours are currently supported:
98
+
99
+ - `:required`: Requires the server to use TLS
100
+ - `:on`: Same as required
101
+ - `:not_supported`: Indicates to the server that encryption is not supported. If server requires encryption, the connection will not be established.
102
+
103
+ Currently not supported:
104
+
105
+ - `:off`: This setting allows the server to upgrade the connection (if server encryption is `:on` or `:required`) and only encrypts the LOGIN packet when the server has encryption set to `:off`.
106
+ - `:client_cert`: This will make the server check the client cerfiticate.
107
+
108
+ Setting `ssl: true` or `ssl: false` is also allowed. In that case `true` is mapped to `:required` and `false` to `:not_supported`.
109
+
110
+ ```elixir
111
+ config :your_app, :tds_conn,
112
+ hostname: "localhost",
113
+ username: "test_user",
114
+ password: "test_password",
115
+ database: "test_db",
116
+ ssl: :required,
117
+ port: 1433,
118
+ execution_mode: :executesql
119
+
120
+ ```
121
+
101
122
## Connecting to SQL Server Instances
102
123
103
124
Tds supports SQL Server instances by passing `instance: "instancename"` to the connection options.
 
@@ -171,7 +192,7 @@ To convert a big-endian UUID string to a mixed-endian binary, use
171
192
Clone and compile Tds with:
172
193
173
194
```bash
174
- git clone https://github.com/livehelpnow/tds.git
195
+ git clone https://github.com/elixir-ecto/tds.git
175
196
cd tds
176
197
mix deps.get
177
198
```
changed hex_metadata.config
 
@@ -6,15 +6,15 @@
6
6
{<<"files">>,
7
7
[<<"lib">>,<<"lib/tds.ex">>,<<"lib/tds">>,<<"lib/tds/protocol.ex">>,
8
8
<<"lib/tds/types">>,<<"lib/tds/types/uuid.ex">>,<<"lib/tds/types.ex">>,
9
- <<"lib/tds/tokens.ex">>,<<"lib/tds/perf.ex">>,<<"lib/tds/latin1.ex">>,
10
- <<"lib/tds/error.ex">>,<<"lib/tds/protocol">>,
11
- <<"lib/tds/protocol/collation.ex">>,<<"lib/tds/protocol/grammar.ex">>,
12
- <<"lib/tds/protocol/token.ex">>,<<"lib/tds/protocol/header.ex">>,
13
- <<"lib/tds/query.ex">>,<<"lib/tds/versions.ex">>,<<"lib/tds/parameter.ex">>,
14
- <<"lib/tds/result.ex">>,<<"lib/tds/errors.csv">>,<<"lib/tds/utils.ex">>,
15
- <<"lib/tds/token_descriptors">>,<<"lib/tds/token_descriptors/basic.ex">>,
16
- <<"lib/tds/binary_utils.ex">>,<<"lib/tds/messages.ex">>,<<"mix.exs">>,
17
- <<"README.md">>]}.
9
+ <<"lib/tds/tokens.ex">>,<<"lib/tds/latin1.ex">>,<<"lib/tds/error.ex">>,
10
+ <<"lib/tds/protocol">>,<<"lib/tds/protocol/collation.ex">>,
11
+ <<"lib/tds/protocol/grammar.ex">>,<<"lib/tds/protocol/prelogin.ex">>,
12
+ <<"lib/tds/protocol/token.ex">>,<<"lib/tds/protocol/login7.ex">>,
13
+ <<"lib/tds/protocol/header.ex">>,<<"lib/tds/query.ex">>,
14
+ <<"lib/tds/versions.ex">>,<<"lib/tds/parameter.ex">>,<<"lib/tds/ucs2.ex">>,
15
+ <<"lib/tds/result.ex">>,<<"lib/tds/tls.ex">>,<<"lib/tds/errors.csv">>,
16
+ <<"lib/tds/utils.ex">>,<<"lib/tds/binary_utils.ex">>,
17
+ <<"lib/tds/messages.ex">>,<<"mix.exs">>,<<"README.md">>]}.
18
18
{<<"licenses">>,[<<"Apache 2.0">>]}.
19
19
{<<"links">>,[{<<"Github">>,<<"https://github.com/livehelpnow/tds">>}]}.
20
20
{<<"name">>,<<"tds">>}.
 
@@ -34,4 +34,4 @@
34
34
{<<"optional">>,false},
35
35
{<<"repository">>,<<"hexpm">>},
36
36
{<<"requirement">>,<<"~> 2.0">>}]]}.
37
- {<<"version">>,<<"2.2.0">>}.
37
+ {<<"version">>,<<"2.3.0-rc.0">>}.
changed lib/tds/binary_utils.ex
 
@@ -21,6 +21,11 @@ defmodule Tds.BinaryUtils do
21
21
"""
22
22
defmacro ushort(), do: quote(do: little - unsigned - 16)
23
23
24
+ @doc """
25
+ An unsigned 6-byte (48-bit) value. The range is 0 to (2^48)-1
26
+ """
27
+ defmacro sixbyte(), do: quote(do: unsigned - 48)
28
+
24
29
@doc """
25
30
A signed 4-byte (32-bit) value. The range is -(2^31) to (2^31)-1.
26
31
"""
 
@@ -162,7 +167,7 @@ defmodule Tds.BinaryUtils do
162
167
@doc """
163
168
A 64-bit signed integer
164
169
"""
165
- defmacro uint64(), do: quote(do: unsigned - 64)
170
+ defmacro uint64(), do: quote(do: unsigned - 64)
166
171
167
172
@doc """
168
173
A 64-bit signed float
changed lib/tds/error.ex
 
@@ -20,6 +20,9 @@ defmodule Tds.Error do
20
20
** (Tds.Error) Line 10 (8): some error
21
21
"""
22
22
23
+ @type error_details :: %{line_number: integer(), number: integer(), msg_text: String.t()}
24
+ @type t :: %__MODULE__{message: String.t(), mssql: error_details}
25
+
23
26
defexception [:message, :mssql]
24
27
25
28
def exception(message) when is_binary(message) or is_atom(message) do
changed lib/tds/latin1.ex
 
@@ -1,7 +1,7 @@
1
1
defmodule Tds.Latin1 do
2
2
@moduledoc false
3
3
4
- def encode(str, "utf-16le") do
4
+ def encode(str, "utf-16le") when is_binary(str) do
5
5
case :unicode.characters_to_binary(str, :unicode, {:utf16, :little}) do
6
6
utf16 when is_bitstring(utf16) ->
7
7
utf16
 
@@ -12,7 +12,7 @@ defmodule Tds.Latin1 do
12
12
end
13
13
end
14
14
15
- def encode(str, "windows-1252") do
15
+ def encode(str, "windows-1252") when is_binary(str) do
16
16
case :unicode.characters_to_binary(str, :unicode, :latin1) do
17
17
utf16 when is_bitstring(utf16) ->
18
18
utf16
 
@@ -23,11 +23,11 @@ defmodule Tds.Latin1 do
23
23
end
24
24
end
25
25
26
- def decode(binary, "utf-16le") do
27
- :unicode.characters_to_binary(binary, {:utf16, :little}, :utf8)
26
+ def decode(binary, "utf-16le") when is_binary(binary) do
27
+ :unicode.characters_to_binary(binary, {:utf16, :little})
28
28
end
29
29
30
- def decode(binary, _) do
30
+ def decode(binary, _) when is_binary(binary) do
31
31
binary
32
32
end
33
33
end
changed lib/tds/messages.ex
 
@@ -1,10 +1,11 @@
1
1
defmodule Tds.Messages do
2
2
import Record, only: [defrecord: 2]
3
- import Tds.Utils
4
3
import Tds.Tokens, only: [decode_tokens: 1]
5
4
6
5
alias Tds.Parameter
6
+ alias Tds.Protocol.{Login7, Prelogin}
7
7
alias Tds.Types
8
+ alias Tds.UCS2
8
9
9
10
require Bitwise
10
11
require Logger
 
@@ -19,6 +20,7 @@ defmodule Tds.Messages do
19
20
defrecord :msg_attn, []
20
21
21
22
# responses
23
+ defrecord :msg_preloginack, [:response]
22
24
defrecord :msg_loginack, [:redirect]
23
25
defrecord :msg_prepared, [:params]
24
26
defrecord :msg_sql_result, [:columns, :rows, :row_count]
 
@@ -45,8 +47,8 @@ defmodule Tds.Messages do
45
47
46
48
## Packet Size
47
49
@tds_pack_data_size 4088
48
- @tds_pack_header_size 8
49
- @tds_pack_size @tds_pack_header_size + @tds_pack_data_size
50
+ # @tds_pack_header_size 8
51
+ # @tds_pack_size @tds_pack_header_size + @tds_pack_data_size
50
52
51
53
## Packet Types
52
54
# @tds_pack_sqlbatch 1
 
@@ -60,6 +62,10 @@ defmodule Tds.Messages do
60
62
# @tds_pack_prelogin 18
61
63
62
64
## Parsers
65
+ def parse(:prelogin, packet_data, s) do
66
+ response = Prelogin.decode(packet_data, s)
67
+ {msg_preloginack(response: response), s}
68
+ end
63
69
64
70
def parse(:login, packet_data, s) do
65
71
packet_data
 
@@ -160,8 +166,7 @@ defmodule Tds.Messages do
160
166
c = %{c | rows: [row | c.rows], num_rows: c.num_rows + 1}
161
167
{m, c, s}
162
168
163
- {token, %{status: status, rows: num_rows}},
164
- {msg_result(set: set) = m, c, s}
169
+ {token, %{status: status, rows: num_rows}}, {msg_result(set: set) = m, c, s}
165
170
when token in [:done, :doneinproc, :doneproc] ->
166
171
cond do
167
172
status.count? and is_nil(c) ->
 
@@ -236,159 +241,14 @@ defmodule Tds.Messages do
236
241
encode(msg, env)
237
242
end
238
243
239
- defp encode(msg_prelogin(params: _params), _env) do
240
- version_data = <<11, 0, 12, 56, 0, 0>>
241
- version_length = byte_size(version_data)
242
- version_offset = 0x06
243
- version = <<0x00, version_offset::size(16), version_length::size(16)>>
244
- terminator = <<0xFF>>
245
- prelogin_data = version_data
246
- data = version <> terminator <> prelogin_data
247
- encode_packets(0x12, data)
248
- # encode_header(0x12, data) <> data
244
+ defp encode(msg_prelogin(params: opts), _env) do
245
+ Prelogin.encode(opts)
249
246
end
250
247
251
- defp encode(msg_login(params: params), _env) do
252
- {:ok, hostname} = :inet.gethostname()
253
- hostname = String.Chars.to_string(hostname)
254
- app_name = Node.self() |> Atom.to_string()
255
-
256
- tds_version = <<0x04, 0x00, 0x00, 0x74>>
257
- message_size = <<@tds_pack_size::little-size(4)-unit(8)>>
258
- client_prog_ver = <<0x04, 0x00, 0x00, 0x07>>
259
- client_pid = <<0x00, 0x10, 0x00, 0x00>>
260
- connection_id = <<0x00::size(32)>>
261
- option_flags_1 = <<0x00>>
262
- option_flags_2 = <<0x00>>
263
- type_flags = <<0x00>>
264
- option_flags_3 = <<0x00>>
265
- client_time_zone = <<0xE0, 0x01, 0x00, 0x00>>
266
- client_lcid = <<0x09, 0x04, 0x00, 0x00>>
267
-
268
- login_a =
269
- tds_version <>
270
- message_size <>
271
- client_prog_ver <>
272
- client_pid <>
273
- connection_id <>
274
- option_flags_1 <>
275
- option_flags_2 <>
276
- type_flags <> option_flags_3 <> client_time_zone <> client_lcid
277
-
278
- offset_start = byte_size(login_a) + 4
279
- username = params[:username]
280
- password = params[:password]
281
- servername = params[:hostname]
282
-
283
- username_ucs = to_little_ucs2(username)
284
- password_ucs = to_little_ucs2(password)
285
- servername_ucs = to_little_ucs2(servername)
286
- app_name_ucs = to_little_ucs2(app_name)
287
- hostname_ucs = to_little_ucs2(hostname)
288
-
289
- password_ucs_xor = encode_tdspassword(password_ucs)
290
- # Before submitting a password from the client to the server,
291
- # for every byte in the password buffer starting with the position pointed
292
- # to by IbPassword, the client SHOULD first swap the four high bits with the
293
- # four low bits and then do a bit-XOR with 0xA5 (10100101).
294
-
295
- clt_int_name = "ODBC"
296
- clt_int_name_ucs = to_little_ucs2(clt_int_name)
297
- database = params[:database] || ""
298
- database_ucs = to_little_ucs2(database)
299
-
300
- login_data =
301
- hostname_ucs <>
302
- username_ucs <>
303
- password_ucs_xor <>
304
- app_name_ucs <>
305
- servername_ucs <>
306
- clt_int_name_ucs <>
307
- database_ucs
308
-
309
- curr_offset = offset_start + 58
310
- ibHostName = <<curr_offset::little-size(16)>>
311
- cchHostName = <<String.length(hostname)::little-size(16)>>
312
- curr_offset = curr_offset + byte_size(hostname_ucs)
313
-
314
- ibUserName = <<curr_offset::little-size(16)>>
315
- cchUserName = <<String.length(username)::little-size(16)>>
316
- curr_offset = curr_offset + byte_size(username_ucs)
317
-
318
- ibPassword = <<curr_offset::little-size(16)>>
319
- cchPassword = <<String.length(password)::little-size(16)>>
320
- curr_offset = curr_offset + byte_size(password_ucs)
321
-
322
- ibAppName = <<curr_offset::little-size(16)>>
323
- cchAppName = <<String.length(app_name)::little-size(16)>>
324
- curr_offset = curr_offset + byte_size(app_name_ucs)
325
-
326
- ibServerName = <<curr_offset::little-size(16)>>
327
- cchServerName = <<String.length(servername)::little-size(16)>>
328
- curr_offset = curr_offset + byte_size(servername_ucs)
329
-
330
- ibUnused = <<0::size(16)>>
331
- cbUnused = <<0::size(16)>>
332
-
333
- ibCltIntName = <<curr_offset::little-size(16)>>
334
- cchCltIntName = <<4::little-size(16)>>
335
- curr_offset = curr_offset + 4 * 2
336
-
337
- ibLanguage = <<0::size(16)>>
338
- cchLanguage = <<0::size(16)>>
339
-
340
- ibDatabase = <<curr_offset::little-size(16)>>
341
-
342
- cchDatabase =
343
- if database == "" do
344
- <<0xAC>>
345
- else
346
- <<String.length(database)::little-size(16)>>
347
- end
348
-
349
- clientID = <<0::size(48)>>
350
-
351
- ibSSPI = <<0::size(16)>>
352
- cbSSPI = <<0::size(16)>>
353
-
354
- ibAtchDBFile = <<0::size(16)>>
355
- cchAtchDBFile = <<0::size(16)>>
356
-
357
- ibChangePassword = <<0::size(16)>>
358
- cchChangePassword = <<0::size(16)>>
359
-
360
- cbSSPILong = <<0::size(32)>>
361
-
362
- offset =
363
- ibHostName <>
364
- cchHostName <>
365
- ibUserName <>
366
- cchUserName <>
367
- ibPassword <>
368
- cchPassword <>
369
- ibAppName <>
370
- cchAppName <>
371
- ibServerName <>
372
- cchServerName <>
373
- ibUnused <>
374
- cbUnused <>
375
- ibCltIntName <>
376
- cchCltIntName <>
377
- ibLanguage <>
378
- cchLanguage <>
379
- ibDatabase <>
380
- cchDatabase <>
381
- clientID <>
382
- ibSSPI <>
383
- cbSSPI <>
384
- ibAtchDBFile <>
385
- cchAtchDBFile <> ibChangePassword <> cchChangePassword <> cbSSPILong
386
-
387
- login7 = login_a <> offset <> login_data
388
-
389
- login7_len = byte_size(login7) + 4
390
- data = <<login7_len::little-size(32)>> <> login7
391
- encode_packets(0x10, data)
248
+ defp encode(msg_login(params: opts), _env) do
249
+ opts
250
+ |> Login7.new()
251
+ |> Login7.encode()
392
252
end
393
253
394
254
defp encode(msg_attn(), _s) do
 
@@ -397,7 +257,7 @@ defmodule Tds.Messages do
397
257
398
258
defp encode(msg_sql(query: q), %{trans: trans}) do
399
259
# convert query to unicodestream
400
- q_ucs = to_little_ucs2(q)
260
+ q_ucs = UCS2.from_string(q)
401
261
402
262
# Transaction Descriptor header
403
263
header_type = <<2::little-size(2)-unit(8)>>
 
@@ -406,8 +266,7 @@ defmodule Tds.Messages do
406
266
transaction_descriptor = trans <> <<0::size(padding)-unit(8)>>
407
267
outstanding_request_count = <<1::little-size(4)-unit(8)>>
408
268
409
- td_header =
410
- header_type <> transaction_descriptor <> outstanding_request_count
269
+ td_header = header_type <> transaction_descriptor <> outstanding_request_count
411
270
412
271
td_header_len = byte_size(td_header) + 4
413
272
td_header = <<td_header_len::little-size(4)-unit(8)>> <> td_header
 
@@ -427,8 +286,7 @@ defmodule Tds.Messages do
427
286
transaction_descriptor = trans <> <<0::size(padding)-unit(8)>>
428
287
outstanding_request_count = <<1::little-size(4)-unit(8)>>
429
288
430
- td_header =
431
- header_type <> transaction_descriptor <> outstanding_request_count
289
+ td_header = header_type <> transaction_descriptor <> outstanding_request_count
432
290
433
291
td_header_len = byte_size(td_header) + 4
434
292
td_header = <<td_header_len::little-size(4)-unit(8)>> <> td_header
 
@@ -442,7 +300,9 @@ defmodule Tds.Messages do
442
300
encode_packets(0x03, data)
443
301
end
444
302
445
- defp encode(msg_transmgr(command: "TM_BEGIN_XACT", isolation_level: isolation_level), %{trans: trans}) do
303
+ defp encode(msg_transmgr(command: "TM_BEGIN_XACT", isolation_level: isolation_level), %{
304
+ trans: trans
305
+ }) do
446
306
isolation = encode_isolation_level(isolation_level)
447
307
encode_trans(5, trans, <<isolation::size(1)-unit(8), 0x0::size(1)-unit(8)>>)
448
308
end
 
@@ -452,9 +312,10 @@ defmodule Tds.Messages do
452
312
end
453
313
454
314
defp encode(msg_transmgr(command: "TM_ROLLBACK_XACT", name: name), %{trans: trans}) do
455
- payload = unless name > 0,
456
- do: <<0x00::size(2)-unit(8)>>,
457
- else: <<2::unsigned-8, name::little-size(2)-unit(8), 0x0::size(1)-unit(8)>>
315
+ payload =
316
+ unless name > 0,
317
+ do: <<0x00::size(2)-unit(8)>>,
318
+ else: <<2::unsigned-8, name::little-size(2)-unit(8), 0x0::size(1)-unit(8)>>
458
319
459
320
encode_trans(8, trans, payload)
460
321
end
 
@@ -482,8 +343,7 @@ defmodule Tds.Messages do
482
343
transaction_descriptor = trans <> <<0::size(padding)-unit(8)>>
483
344
outstanding_request_count = <<1::little-size(4)-unit(8)>>
484
345
485
- td_header =
486
- header_type <> transaction_descriptor <> outstanding_request_count
346
+ td_header = header_type <> transaction_descriptor <> outstanding_request_count
487
347
488
348
td_header_len = byte_size(td_header) + 4
489
349
td_header = <<td_header_len::little-size(4)-unit(8)>> <> td_header
 
@@ -492,8 +352,7 @@ defmodule Tds.Messages do
492
352
total_length = byte_size(headers) + 4
493
353
all_headers = <<total_length::little-size(32)>> <> headers
494
354
495
- data =
496
- all_headers <> <<request_type::little-size(2)-unit(8), request_payload::binary>>
355
+ data = all_headers <> <<request_type::little-size(2)-unit(8), request_payload::binary>>
497
356
498
357
encode_packets(0x0E, data)
499
358
end
 
@@ -519,7 +378,7 @@ defmodule Tds.Messages do
519
378
# for that parameter. Otherwise RPC will fail and we must use ProceName
520
379
# instead. But we want to avoid execution overhead with named approach
521
380
# hence ommiting @handle from parameter name
522
- %{p| name: ""}
381
+ %{p | name: ""}
523
382
524
383
p ->
525
384
# other paramters should be named
 
@@ -549,7 +408,7 @@ defmodule Tds.Messages do
549
408
end
550
409
551
410
defp encode_rpc_param(%Tds.Parameter{name: name} = param) do
552
- p_name = to_little_ucs2(name)
411
+ p_name = UCS2.from_string(name)
553
412
p_flags = param |> Parameter.option_flags()
554
413
{type_code, type_data, type_attr} = Types.encode_data_type(param)
555
414
 
@@ -585,12 +444,4 @@ defmodule Tds.Messages do
585
444
header = encode_header(type, data, id, 1)
586
445
[header <> data]
587
446
end
588
-
589
- defp encode_tdspassword(list) do
590
- for <<b::4, a::4 <- list>> do
591
- <<c>> = <<a::size(4), b::size(4)>>
592
- Bitwise.bxor(c, 0xA5)
593
- end
594
- |> Enum.map_join(&<<&1>>)
595
- end
596
447
end
changed lib/tds/parameter.ex
 
@@ -57,6 +57,7 @@ defmodule Tds.Parameter do
57
57
58
58
def do_name([param | tail], name, acc) do
59
59
name = name + 1
60
+
60
61
param =
61
62
case param do
62
63
%Tds.Parameter{name: nil} -> fix_data_type(%{param | name: "@#{name}"})
 
@@ -140,9 +141,7 @@ defmodule Tds.Parameter do
140
141
%{param | type: :datetime}
141
142
end
142
143
143
- def fix_data_type(
144
- %Tds.Parameter{value: %NaiveDateTime{microsecond: {_, s}}} = param
145
- ) do
144
+ def fix_data_type(%Tds.Parameter{value: %NaiveDateTime{microsecond: {_, s}}} = param) do
146
145
type = if s > 3, do: :datetime2, else: :datetime
147
146
%{param | type: type}
148
147
end
 
@@ -158,9 +157,7 @@ defmodule Tds.Parameter do
158
157
%{param | type: :datetimeoffset}
159
158
end
160
159
161
- def fix_data_type(
162
- %Tds.Parameter{value: {{_y, _m, _d}, _time, _offset}} = param
163
- ) do
160
+ def fix_data_type(%Tds.Parameter{value: {{_y, _m, _d}, _time, _offset}} = param) do
164
161
%{param | type: :datetimeoffset}
165
162
end
removed lib/tds/perf.ex
 
@@ -1,41 +0,0 @@
1
- defmodule Tds.Perf do
2
- @moduledoc false
3
-
4
- @millisecond 1000
5
- @second @millisecond * 1000
6
- @minute @second * 60
7
- @hour @minute * 60
8
-
9
-
10
- def to_string(misec) do
11
- cond do
12
- 0.9 < misec / @hour ->
13
- "#{Float.round(misec / @hour, 2)}h"
14
- 0.9 < misec / @minute ->
15
- "#{Float.round(misec / @minute, 2)}m"
16
- 0.9 < misec / @second ->
17
- "#{Float.round(misec / @second, 2)}s"
18
- 0.9 < misec / @millisecond ->
19
- "#{Float.round(misec / @millisecond, 2)}ms"
20
- :else ->
21
- "#{misec}μsec"
22
- end
23
- end
24
-
25
- @kbyte 1024
26
- @mbyte @kbyte * 1024
27
- @gbyte @mbyte * 1024
28
-
29
- def to_size(len) do
30
- cond do
31
- 0.1 < len / @gbyte ->
32
- "#{Float.round(len / @gbyte, 3)}GiB"
33
- 0.1 < len / @mbyte ->
34
- "#{Float.round(len / @mbyte, 3)}MiB"
35
- 0.1 < len / @kbyte ->
36
- "#{Float.round(len / @kbyte, 3)}KiB"
37
- :else ->
38
- "#{len}bytes"
39
- end
40
- end
41
- end
changed lib/tds/protocol.ex
 
@@ -2,18 +2,12 @@ defmodule Tds.Protocol do
2
2
@moduledoc """
3
3
Implements DBConnection behaviour for TDS protocol
4
4
"""
5
- import Tds.BinaryUtils
6
- import Tds.Messages
7
- import Tds.Utils
8
-
9
- alias Tds.Parameter
10
- alias Tds.Query
11
-
5
+ alias Tds.{Parameter, Query}
6
+ import Tds.{BinaryUtils, Messages, Utils}
12
7
require Logger
8
+ use DBConnection
13
9
14
- @behaviour DBConnection
15
-
16
- @timeout 5000
10
+ @timeout 5_000
17
11
@sock_opts [packet: :raw, mode: :binary, active: false]
18
12
@trans_levels [
19
13
:read_uncommitted,
 
@@ -67,16 +61,11 @@ defmodule Tds.Protocol do
67
61
packetsize: 4096
68
62
}
69
63
70
- @impl DBConnection
71
- @spec connect(opts :: Keyword.t()) ::
72
- {:ok, state :: t()} | {:error, Exception.t()}
64
+ @spec connect(opts :: Keyword.t()) :: {:ok, state :: t()} | {:error, Exception.t()}
73
65
def connect(opts) do
74
66
opts =
75
67
opts
76
- |> Keyword.put_new(
77
- :username,
78
- System.get_env("MSSQLUSER") || System.get_env("USER")
79
- )
68
+ |> Keyword.put_new(:username, System.get_env("MSSQLUSER") || System.get_env("USER"))
80
69
|> Keyword.put_new(:password, System.get_env("MSSQLPASSWORD"))
81
70
|> Keyword.put_new(:instance, System.get_env("MSSQLINSTANCE"))
82
71
|> Keyword.put_new(:hostname, System.get_env("MSSQLHOST") || "localhost")
 
@@ -96,9 +85,7 @@ defmodule Tds.Protocol do
96
85
end
97
86
end
98
87
99
- @impl DBConnection
100
- @spec disconnect(err :: Exception.t() | String.t(), state :: t()) ::
101
- :ok
88
+ @spec disconnect(err :: Exception.t() | String.t(), state :: t()) :: :ok
102
89
def disconnect(_err, %{sock: {mod, sock}} = s) do
103
90
# If socket is active we flush any socket messages so the next
104
91
# socket does not get the messages.
 
@@ -106,7 +93,6 @@ defmodule Tds.Protocol do
106
93
mod.close(sock)
107
94
end
108
95
109
- @impl DBConnection
110
96
@spec ping(t) :: {:ok, t} | {:disconnect, Exception.t(), t}
111
97
def ping(state) do
112
98
case send_query(~s{SELECT 'pong' as [msg]}, state) do
 
@@ -131,19 +117,17 @@ defmodule Tds.Protocol do
131
117
end
132
118
end
133
119
134
- @impl DBConnection
135
120
@spec checkout(state :: t) ::
136
- {:ok, new_state :: any}
137
- | {:disconnect, Exception.t(), new_state :: t}
121
+ {:ok, new_state :: any} | {:disconnect, Exception.t(), new_state :: t}
138
122
def checkout(%{transaction: :started} = s) do
139
123
err = %Tds.Error{message: "Unexpected transaction status `:started`"}
140
124
{:disconnect, err, s}
141
125
end
142
126
143
- def checkout(%{sock: {mod, sock}} = s) do
127
+ def checkout(%{sock: {mod, _sock}} = s) do
144
128
sock_mod = inspect(mod)
145
129
146
- case :inet.setopts(sock, active: false) do
130
+ case setopts(s.sock, active: false) do
147
131
:ok ->
148
132
{:ok, s}
149
133
 
@@ -153,19 +137,17 @@ defmodule Tds.Protocol do
153
137
end
154
138
end
155
139
156
- @impl DBConnection
157
140
@spec checkin(state :: t) ::
158
- {:ok, new_state :: t}
159
- | {:disconnect, Exception.t(), new_state :: t}
141
+ {:ok, new_state :: t} | {:disconnect, Exception.t(), new_state :: t}
160
142
def checkin(%{transaction: :started} = s) do
161
143
err = %Tds.Error{message: "Unexpected transaction status `:started`"}
162
144
{:disconnect, err, s}
163
145
end
164
146
165
- def checkin(%{sock: {mod, sock}} = s) do
147
+ def checkin(%{sock: {mod, _sock}} = s) do
166
148
sock_mod = inspect(mod)
167
149
168
- case :inet.setopts(sock, active: :once) do
150
+ case setopts(s.sock, active: :once) do
169
151
:ok ->
170
152
{:ok, s}
171
153
 
@@ -175,7 +157,6 @@ defmodule Tds.Protocol do
175
157
end
176
158
end
177
159
178
- @impl DBConnection
179
160
@spec handle_execute(Tds.Query.t(), DBConnection.params(), Keyword.t(), t) ::
180
161
{:ok, Tds.Query.t(), Tds.Result.t(), new_state :: t}
181
162
| {:error | :disconnect, Exception.t(), new_state :: t}
 
@@ -210,7 +191,6 @@ defmodule Tds.Protocol do
210
191
end
211
192
end
212
193
213
- @impl DBConnection
214
194
@spec handle_prepare(Tds.Query.t(), Keyword.t(), t) ::
215
195
{:ok, Tds.Query.t(), new_state :: t()}
216
196
| {:error | :disconnect, Exception.t(), new_state :: t}
 
@@ -235,7 +215,6 @@ defmodule Tds.Protocol do
235
215
end
236
216
end
237
217
238
- @impl DBConnection
239
218
@spec handle_close(Tds.Query.t(), nil | keyword | map, t()) ::
240
219
{:ok, Tds.Result.t(), new_state :: t()}
241
220
| {:error | :disconnect, Exception.t(), new_state :: t()}
 
@@ -244,7 +223,6 @@ defmodule Tds.Protocol do
244
223
send_close(query, params, s)
245
224
end
246
225
247
- @impl DBConnection
248
226
@spec handle_begin(Keyword.t(), t) ::
249
227
{:ok, Tds.Result.t(), new_state :: t}
250
228
| {DBConnection.status(), new_state :: t}
 
@@ -268,7 +246,6 @@ defmodule Tds.Protocol do
268
246
end
269
247
end
270
248
271
- @impl DBConnection
272
249
@spec handle_commit(Keyword.t(), t) ::
273
250
{:ok, Tds.Result.t(), new_state :: t}
274
251
| {DBConnection.status(), new_state :: t}
 
@@ -286,7 +263,6 @@ defmodule Tds.Protocol do
286
263
end
287
264
end
288
265
289
- @impl DBConnection
290
266
@spec handle_rollback(Keyword.t(), t) ::
291
267
{:ok, Tds.Result.t(), new_state :: t}
292
268
| {:idle, new_state :: t}
 
@@ -312,7 +288,6 @@ defmodule Tds.Protocol do
312
288
end
313
289
end
314
290
315
- @impl DBConnection
316
291
@spec handle_status(Keyword.t(), t) ::
317
292
{:idle | :transaction | :error, t}
318
293
| {:disconnect, Exception.t(), t}
 
@@ -325,7 +300,6 @@ defmodule Tds.Protocol do
325
300
end
326
301
end
327
302
328
- @impl DBConnection
329
303
@spec handle_fetch(
330
304
Query.t(),
331
305
cursor :: any(),
 
@@ -338,7 +312,6 @@ defmodule Tds.Protocol do
338
312
{:error, Tds.Error.exception("Cursor is not supported by TDS"), state}
339
313
end
340
314
341
- @impl DBConnection
342
315
@spec handle_deallocate(
343
316
query :: Query.t(),
344
317
cursor :: any,
 
@@ -348,22 +321,14 @@ defmodule Tds.Protocol do
348
321
{:ok, Tds.Result.t(), new_state :: t()}
349
322
| {:error | :disconnect, Exception.t(), new_state :: t()}
350
323
def handle_deallocate(_query, _cursor, _opts, state) do
351
- {:error, Tds.Error.exception("Cursor operations are not supported in TDS"),
352
- state}
324
+ {:error, Tds.Error.exception("Cursor operations are not supported in TDS"), state}
353
325
end
354
326
355
- @impl DBConnection
356
- @spec handle_declare(
357
- Query.t(),
358
- params :: any,
359
- opts :: Keyword.t(),
360
- state :: t
361
- ) ::
327
+ @spec handle_declare(Query.t(), params :: any, opts :: Keyword.t(), state :: t) ::
362
328
{:ok, Query.t(), cursor :: any, new_state :: t}
363
329
| {:error | :disconnect, Exception.t(), new_state :: t}
364
330
def handle_declare(_query, _params, _opts, state) do
365
- {:error, Tds.Error.exception("Cursor operations are not supported in TDS"),
366
- state}
331
+ {:error, Tds.Error.exception("Cursor operations are not supported in TDS"), state}
367
332
end
368
333
369
334
# CONNECTION
 
@@ -379,43 +344,39 @@ defmodule Tds.Protocol do
379
344
parse_udp(msg, %{s | opts: opts, usock: sock})
380
345
381
346
{:error, error} ->
382
- error(%Tds.Error{message: "udp connect: #{error}"}, s)
347
+ {:error, %Tds.Error{message: "udp connect: #{error}"}}
383
348
end
384
349
end
385
350
386
351
defp connect(opts, s) do
387
352
host = Keyword.fetch!(opts, :hostname)
388
353
host = if is_binary(host), do: String.to_charlist(host), else: host
354
+
389
355
port = s.itcp || opts[:port] || System.get_env("MSSQLPORT") || 1433
390
356
{port, _} = if is_binary(port), do: Integer.parse(port), else: {port, nil}
357
+
391
358
timeout = opts[:timeout] || @timeout
359
+
392
360
sock_opts = @sock_opts ++ (opts[:socket_options] || [])
393
361
394
362
s = %{s | opts: opts}
395
363
396
- case :gen_tcp.connect(host, port, sock_opts, timeout) do
397
- {:ok, sock} ->
398
- {:ok, [sndbuf: sndbuf, recbuf: recbuf, buffer: buffer]} =
399
- :inet.getopts(sock, [:sndbuf, :recbuf, :buffer])
400
-
401
- buffer =
402
- buffer
403
- |> max(sndbuf)
404
- |> max(recbuf)
405
-
406
- :ok = :inet.setopts(sock, buffer: buffer)
407
-
408
- case login(%{s | sock: {:gen_tcp, sock}}) do
409
- {:error, error, _state} ->
410
- :gen_tcp.close(sock)
411
- {:error, error}
412
-
413
- r ->
414
- r
415
- end
364
+ # Initalize TCP connection with the SQL Server
365
+ with {:ok, sock} <- :gen_tcp.connect(host, port, sock_opts, timeout),
366
+ {:ok, buffers} <- :inet.getopts(sock, [:sndbuf, :recbuf, :buffer]),
367
+ :ok <- :inet.setopts(sock, buffer: max_buf_size(buffers)) do
368
+ # Send Prelogin message to SQL Server
369
+ case send_prelogin(%{s | sock: {:gen_tcp, sock}}) do
370
+ {:error, error, _state} ->
371
+ :gen_tcp.close(sock)
372
+ {:error, error}
416
373
374
+ other ->
375
+ other
376
+ end
377
+ else
417
378
{:error, error} ->
418
- error(%Tds.Error{message: "tcp connect: #{error}"}, s)
379
+ {:error, %Tds.Error{message: "tcp connect: #{error}"}}
419
380
end
420
381
end
421
382
 
@@ -451,10 +412,7 @@ defmodule Tds.Protocol do
451
412
452
413
case server do
453
414
nil ->
454
- error(%Tds.Error{message: "Instance #{opts[:instance]} not found"}, %{
455
- s
456
- | usock: nil
457
- })
415
+ {:error, %Tds.Error{message: "Instance #{opts[:instance]} not found"}}
458
416
459
417
serv ->
460
418
{port, _} = Integer.parse(serv[:tcp])
 
@@ -462,6 +420,25 @@ defmodule Tds.Protocol do
462
420
end
463
421
end
464
422
423
+ defp ssl_connect(%{sock: {:gen_tcp, sock}, opts: opts} = s) do
424
+ {:ok, _} = Application.ensure_all_started(:ssl)
425
+
426
+ case Tds.Tls.connect(sock, opts[:ssl_opts] || []) do
427
+ {:ok, ssl_sock} ->
428
+ state = %{s | sock: {:ssl, ssl_sock}}
429
+ {:ok, state}
430
+
431
+ {:error, reason} ->
432
+ error =
433
+ Tds.Error.exception(
434
+ "Unable to establish secure connection to server due #{inspect(reason)}"
435
+ )
436
+
437
+ :gen_tcp.close(sock)
438
+ {:error, error, s}
439
+ end
440
+ end
441
+
465
442
def handle_info({:udp_error, _, :econnreset}, s) do
466
443
msg =
467
444
"Tds encountered an error while connecting to the Sql Server " <>
 
@@ -474,10 +451,7 @@ defmodule Tds.Protocol do
474
451
{:tcp, _, _data},
475
452
%{sock: {mod, sock}, opts: opts, state: :prelogin} = s
476
453
) do
477
- case mod do
478
- :gen_tcp -> :inet.setopts(sock, active: false)
479
- :ssl -> :ssl.setopts(sock, active: false)
480
- end
454
+ setopts(s.sock, active: false)
481
455
482
456
login(%{s | opts: opts, sock: {mod, sock}})
483
457
end
 
@@ -541,25 +515,19 @@ defmodule Tds.Protocol do
541
515
542
516
# PROTOCOL
543
517
544
- def prelogin(%{opts: opts} = s) do
518
+ def send_prelogin(%{opts: opts} = s) do
545
519
msg = msg_prelogin(params: opts)
546
520
547
- case msg_send(msg, s) do
548
- {:ok, s} ->
549
- {:noreply, %{s | state: :prelogin}}
550
-
551
- {:error, reason, s} ->
552
- error(%Tds.Error{message: "tcp send: #{reason}"}, s)
553
-
554
- any ->
555
- any
521
+ case msg_send(msg, %{s | state: :prelogin}) do
522
+ {:ok, s} -> login(s)
523
+ any -> any
556
524
end
557
525
end
558
526
559
527
def login(%{opts: opts} = s) do
560
528
msg = msg_login(params: opts)
561
529
562
- case login_send(msg, s) do
530
+ case login_send(msg, %{s | state: :login}) do
563
531
{:ok, s} ->
564
532
{:ok, %{s | state: :ready}}
565
533
 
@@ -763,14 +731,19 @@ defmodule Tds.Protocol do
763
731
end
764
732
end
765
733
734
+ def message(:prelogin, msg_preloginack(response: response), _) do
735
+ case response do
736
+ {:login, s} -> {:ok, s}
737
+ {:encrypt, s} -> ssl_connect(s)
738
+ other -> other
739
+ end
740
+ end
741
+
766
742
def message(
767
743
:login,
768
744
msg_loginack(redirect: %{hostname: host, port: port}),
769
- %{opts: opts} = s
745
+ %{opts: opts}
770
746
) do
771
- # we got an ENVCHANGE:redirection token, we need to disconnect and start over with new server
772
- disconnect("redirected", s)
773
-
774
747
new_opts =
775
748
opts
776
749
|> Keyword.put(:hostname, host)
 
@@ -837,8 +810,9 @@ defmodule Tds.Protocol do
837
810
end
838
811
839
812
# Send Command To Sql Server
840
- defp login_send(msg, %{sock: {mod, sock}, env: env} = s) do
813
+ defp login_send(msg, %{sock: {mod, sock}, env: env, opts: opts} = s) do
841
814
paks = encode_msg(msg, env)
815
+ s = %{s | opts: clean_opts(opts)}
842
816
843
817
Enum.each(paks, fn pak ->
844
818
mod.send(sock, pak)
 
@@ -846,7 +820,7 @@ defmodule Tds.Protocol do
846
820
847
821
case msg_recv(s) do
848
822
{:disconnect, ex, s} ->
849
- {:error, ex, s}
823
+ {:disconnect, ex, s}
850
824
851
825
buffer ->
852
826
buffer
 
@@ -857,51 +831,33 @@ defmodule Tds.Protocol do
857
831
858
832
defp msg_send(
859
833
msg,
860
- %{sock: {mod, sock}, env: env, state: state, opts: opts} = s
834
+ %{sock: {mod, port}, env: env, opts: opts} = s
861
835
) do
862
- :inet.setopts(sock, active: false)
836
+ setopts(s.sock, active: false)
863
837
864
838
opts
865
839
|> Keyword.get(:use_elixir_calendar_types, false)
866
840
|> use_elixir_calendar_types()
867
841
868
- {t_send, _} =
869
- :timer.tc(fn ->
870
- msg
871
- |> encode_msg(env)
872
- |> Enum.each(&mod.send(sock, &1))
873
- end)
874
-
875
- {t_recv, {t_decode, result}} =
876
- :timer.tc(fn ->
877
- case msg_recv(s) do
878
- {:disconnect, _ex, _s} = res ->
879
- {0, res}
880
-
881
- buffer ->
882
- :timer.tc(fn ->
883
- buffer
884
- |> IO.iodata_to_binary()
885
- |> decode(s)
886
- end)
842
+ send_result =
843
+ msg
844
+ |> encode_msg(env)
845
+ |> Enum.reduce_while(:ok, fn chunk, _ ->
846
+ case mod.send(port, chunk) do
847
+ {:error, reason} -> {:halt, {:error, reason}}
848
+ :ok -> {:cont, :ok}
887
849
end
888
850
end)
889
851
890
- stm = Map.get(s, :query)
891
-
892
- if Keyword.get(s.opts, :trace, false) == true do
893
- Logger.debug(fn ->
894
- "[trace] [Tds.Protocod.msg_send/2] " <>
895
- "state=#{inspect(state)} " <>
896
- "send=#{Tds.Perf.to_string(t_send)} " <>
897
- "receive=#{Tds.Perf.to_string(t_recv - t_decode)} " <>
898
- "decode=#{Tds.Perf.to_string(t_decode)}" <>
899
- "\n" <>
900
- "#{inspect(stm)}"
901
- end)
852
+ with :ok <- send_result,
853
+ buffer when is_list(buffer) <- msg_recv(s) do
854
+ buffer
855
+ |> IO.iodata_to_binary()
856
+ |> decode(s)
857
+ else
858
+ {:disconnect, _ex, _s} = res -> {0, res}
859
+ other -> other
902
860
end
903
-
904
- result
905
861
end
906
862
907
863
defp msg_recv(%{sock: {mod, pid}} = s) do
 
@@ -1073,7 +1029,7 @@ defmodule Tds.Protocol do
1073
1029
raise(
1074
1030
ArgumentError,
1075
1031
"set_deadlock_priority: #{inspect(val)} is an invalid value, " <>
1076
- "valid values are #{inspect([:low, :high, :normal | -10..10])}"
1032
+ "valid values are #{inspect([:low, :high, :normal] ++ [-10..10])}"
1077
1033
)
1078
1034
end
1079
1035
end
 
@@ -1175,4 +1131,17 @@ defmodule Tds.Protocol do
1175
1131
)
1176
1132
end
1177
1133
end
1134
+
1135
+ defp setopts({mod, sock}, options) do
1136
+ case mod do
1137
+ :gen_tcp -> :inet.setopts(sock, options)
1138
+ :ssl -> :ssl.setopts(sock, options)
1139
+ end
1140
+ end
1141
+
1142
+ defp max_buf_size(buffers) when is_list(buffers) do
1143
+ buffers
1144
+ |> Keyword.values()
1145
+ |> Enum.max()
1146
+ end
1178
1147
end
changed lib/tds/protocol/grammar.ex
 
@@ -135,5 +135,4 @@ defmodule Tds.Protocol.Grammar do
135
135
defmacro unicodechar(n \\ 1), do: quote(do: size(unquote(n)) - unit(16))
136
136
137
137
defmacro bigbinary(n), do: quote(do: binary - size(unquote(n)) - unit(8))
138
-
139
138
end
changed lib/tds/protocol/header.ex
 
@@ -29,9 +29,8 @@ defmodule Tds.Protocol.Header do
29
29
| :sspi
30
30
| :pre_login
31
31
32
-
33
32
@typedoc """
34
- Header flag that should tell if pakcage data that header preceding is end of
33
+ Header flag that should tell if package data that header preceding is end of
35
34
TDS message or not.
36
35
37
36
* `:eom` - End of message (EOM). The packet is the last packet in the whole
 
@@ -39,7 +38,7 @@ defmodule Tds.Protocol.Header do
39
38
* `normal` - Normal message, means that there is more packages comming after
40
39
the one that header is preceding.
41
40
"""
42
- @type msg_sned_status :: :normal | :eom
41
+ @type msg_send_status :: :normal | :eom
43
42
@typedoc """
44
43
(Client to SQL server only) Reset this connection before processing event.
45
44
Only set for event types Batch, RPC, or Transaction Manager request
 
@@ -55,7 +54,7 @@ defmodule Tds.Protocol.Header do
55
54
Tuple that holds decoded package header status flags.
56
55
"""
57
56
@type pkg_header_status ::
58
- {msg_sned_status, pkg_process, conn_reset}
57
+ {msg_send_status, pkg_process, conn_reset}
59
58
60
59
@typedoc """
61
60
Decoded TDS package header
 
@@ -81,14 +80,9 @@ defmodule Tds.Protocol.Header do
81
80
]
82
81
83
82
@spec decode(<<_::64>>) :: t | {:error, any}
84
- def decode(<<
85
- type::int8,
86
- status::int8,
87
- length::int16,
88
- spid::int16,
89
- package::int8,
90
- window::int8
91
- >>) do
83
+ def decode(
84
+ <<type::int8, status::int8, length::int16, spid::int16, package::int8, window::int8>>
85
+ ) do
92
86
with {^type, pkg_header_type, has_data} <- decode_type(type) do
93
87
{:ok,
94
88
struct!(__MODULE__,
added lib/tds/protocol/login7.ex
 
@@ -0,0 +1,212 @@
1
+ defmodule Tds.Protocol.Login7 do
2
+ @moduledoc """
3
+ Login7 message definition
4
+
5
+ See: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/773a62b6-ee89-4c02-9e5e-344882630aac
6
+ """
7
+ alias Tds.UCS2
8
+ import Tds.BinaryUtils
9
+
10
+ @packet_header 0x10
11
+ ## Packet Size
12
+ @tds_pack_header_size 8
13
+ @tds_pack_data_size 4088
14
+ @tds_pack_size @tds_pack_header_size + @tds_pack_data_size
15
+ @max_supported_tds_version <<0x04, 0x00, 0x00, 0x74>>
16
+ @default_client_version <<0x04, 0x00, 0x00, 0x07>>
17
+ @client_pid <<0x00, 0x10, 0x00, 0x00>>
18
+ # SQL_DFLT
19
+ @sql_type <<0x00>>
20
+ @options <<0x00>>
21
+ @clt_int_name "ODBC"
22
+ @default_app_name "Elixir TDS"
23
+ # EN-US
24
+ @language_code_id <<0x09, 0x04, 0x00, 0x00>>
25
+
26
+ defstruct [
27
+ # Highest TDS version used by the client
28
+ :tds_version,
29
+ # The packet size being requested by the client
30
+ :packet_size,
31
+ # The version of the interface library (for example, ODBC or OLEDB) being used by the client.
32
+ :client_version,
33
+ # The process ID of the client application.
34
+ :client_pid,
35
+ # The connection ID of the primary Server. Used when connecting to an "Always Up" backup server.
36
+ :connection_id,
37
+ # Options (currently not used)
38
+ :option_flags_1,
39
+ # More options (also not used)
40
+ :option_flags_2,
41
+ # The SQL type sent to the client
42
+ :type_flags,
43
+ # More options (also not used)
44
+ :option_flags_3,
45
+ # This field is not used and can be set to zero.
46
+ :client_time_zone,
47
+ # The language code identifier (LCID) value for the client collation.
48
+ # If ClientLCID is specified, the specified collation is set as the session collation.
49
+ :client_language_code_id,
50
+ # Client username
51
+ :username,
52
+ # Client password
53
+ :password,
54
+ # Server name
55
+ :servername,
56
+ # Application name
57
+ :app_name,
58
+ # Hostname of the SQL server
59
+ :hostname,
60
+ # Database to use (defaults to user database)
61
+ :database
62
+ ]
63
+
64
+ def new(opts) do
65
+ # gethostname/0 always succeeds
66
+ {:ok, hostname} = :inet.gethostname()
67
+
68
+ %__MODULE__{
69
+ tds_version: @max_supported_tds_version,
70
+ packet_size: <<@tds_pack_size::little-size(4)-unit(8)>>,
71
+ hostname: to_string(hostname),
72
+ app_name: Keyword.get(opts, :app_name, @default_app_name),
73
+ client_version: @default_client_version,
74
+ client_pid: pid!(),
75
+ connection_id: <<0x00::size(32)>>,
76
+ option_flags_1: @options,
77
+ option_flags_2: @options,
78
+ type_flags: @sql_type,
79
+ option_flags_3: @options,
80
+ client_time_zone: <<0x0, 0x0, 0x0, 0x0>>,
81
+ client_language_code_id: @language_code_id,
82
+ username: opts[:username],
83
+ password: opts[:password],
84
+ servername: opts[:hostname],
85
+ database: Keyword.get(opts, :database, "")
86
+ }
87
+ end
88
+
89
+ def encode(%__MODULE__{} = login) do
90
+ # Fixed login configuration
91
+ fixed_login = fixed_login(login)
92
+ {variable_login, offsets} = encode_variable_login(login, byte_size(fixed_login) + 62)
93
+
94
+ login7 = fixed_login <> offsets <> variable_login
95
+ login7_len = byte_size(login7) + 4
96
+ data = <<login7_len::little-size(32)>> <> login7
97
+
98
+ Tds.Messages.encode_packets(@packet_header, data)
99
+ end
100
+
101
+ defp fixed_login(login) do
102
+ login.tds_version <>
103
+ login.packet_size <>
104
+ login.client_version <>
105
+ login.client_pid <>
106
+ login.connection_id <>
107
+ login.option_flags_1 <>
108
+ login.option_flags_2 <>
109
+ login.type_flags <>
110
+ login.option_flags_3 <>
111
+ login.client_time_zone <>
112
+ login.client_language_code_id
113
+ end
114
+
115
+ defp encode_variable_login(login, start_offset) do
116
+ current_offset = start_offset
117
+
118
+ # Hostname
119
+ offsets = <<current_offset::ushort, String.length(login.hostname)::ushort>>
120
+ hostname = UCS2.from_string(login.hostname)
121
+ variable_login = hostname
122
+ current_offset = current_offset + byte_size(hostname)
123
+
124
+ # Username
125
+ offsets = offsets <> <<current_offset::ushort, String.length(login.username)::ushort>>
126
+ username = UCS2.from_string(login.username)
127
+ variable_login = variable_login <> username
128
+ current_offset = current_offset + byte_size(username)
129
+
130
+ # Password
131
+ offsets = offsets <> <<current_offset::ushort, String.length(login.password)::ushort>>
132
+ password = UCS2.from_string(login.password)
133
+ variable_login = variable_login <> encode_tds_password(password)
134
+ current_offset = current_offset + byte_size(password)
135
+
136
+ # App Name
137
+ offsets = offsets <> <<current_offset::ushort, String.length(login.app_name)::ushort>>
138
+ app_name = UCS2.from_string(login.app_name)
139
+ variable_login = variable_login <> app_name
140
+ current_offset = current_offset + byte_size(app_name)
141
+
142
+ # Servername
143
+ offsets = offsets <> <<current_offset::ushort, String.length(login.servername)::ushort>>
144
+ servername = UCS2.from_string(login.servername)
145
+ variable_login = variable_login <> servername
146
+ current_offset = current_offset + byte_size(servername)
147
+
148
+ # Unused
149
+ offsets = offsets <> <<0::ushort, 0::ushort>>
150
+
151
+ # Client Int Name
152
+ variable_login = variable_login <> UCS2.from_string(@clt_int_name)
153
+ offsets = offsets <> <<current_offset::ushort, 4::ushort>>
154
+ current_offset = current_offset + 8
155
+
156
+ # Language
157
+ offsets = offsets <> <<0::ushort, 0::ushort>>
158
+
159
+ # Database
160
+ variable_login = variable_login <> UCS2.from_string(login.database)
161
+
162
+ database =
163
+ if login.database == "" do
164
+ 0xAC
165
+ else
166
+ String.length(login.database)
167
+ end
168
+
169
+ offsets = offsets <> <<current_offset::ushort, database::ushort>>
170
+
171
+ # Client ID
172
+ offsets = offsets <> <<0::sixbyte>>
173
+
174
+ # SSPI
175
+ offsets = offsets <> <<0::ushort, 0::ushort>>
176
+
177
+ # Attach DB File
178
+ offsets = offsets <> <<0::ushort, 0::ushort>>
179
+
180
+ # Change password?
181
+ offsets = offsets <> <<0::ushort, 0::ushort>>
182
+
183
+ # SSPI Long
184
+ offsets = offsets <> <<0::dword>>
185
+
186
+ {variable_login, offsets}
187
+ end
188
+
189
+ defp encode_tds_password(list) do
190
+ for <<b::4, a::4 <- list>> do
191
+ <<c>> = <<a::size(4), b::size(4)>>
192
+ Bitwise.bxor(c, 0xA5)
193
+ end
194
+ |> Enum.map_join(&<<&1>>)
195
+ end
196
+
197
+ # Return the current pid
198
+ # If that fails return a "default" pid
199
+ defp pid! do
200
+ value =
201
+ self()
202
+ |> :erlang.pid_to_list()
203
+ |> to_string()
204
+ |> String.split(".")
205
+ |> Enum.at(1)
206
+ |> String.to_integer()
207
+
208
+ <<value::dword>>
209
+ rescue
210
+ _ -> @client_pid
211
+ end
212
+ end
added lib/tds/protocol/prelogin.ex
 
@@ -0,0 +1,332 @@
1
+ defmodule Tds.Protocol.Prelogin do
2
+ @moduledoc """
3
+ Prelogin message definition
4
+
5
+ See: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/60f56408-0188-4cd5-8b90-25c6f2423868
6
+ """
7
+ import Tds.Protocol.Grammar
8
+ require Logger
9
+
10
+ @type state :: Tds.Protocol.t()
11
+ @type packet_data :: iodata()
12
+
13
+ @type response ::
14
+ {:ok, state()}
15
+ | {:error, Exception.t() | atom(), state()}
16
+
17
+ defstruct version: nil,
18
+ encryption: <<0x00>>,
19
+ instance: true,
20
+ thread_id: nil,
21
+ mars: false,
22
+ trace_id: nil,
23
+ fed_auth_required: false,
24
+ nonce_opt: nil
25
+
26
+ @type t :: %__MODULE__{
27
+ version: tuple(),
28
+ encryption: <<_::8>>,
29
+ instance: boolean(),
30
+ mars: boolean()
31
+ }
32
+
33
+ @packet_header 0x12
34
+
35
+ # PL Options Tokens
36
+ @version_token 0x00
37
+ @encryption_token 0x01
38
+ @instopt_token 0x02
39
+ @thread_id_token 0x03
40
+ @mars_token 0x04
41
+ # @trace_id_token 0x05
42
+ @fed_auth_required_token 0x06
43
+ @nonce_opt_token 0x07
44
+ @termintator_token 0xFF
45
+
46
+ # Encryption flags
47
+ @encryption_off 0x00
48
+ @encryption_on 0x01
49
+ @encryption_not_supported 0x02
50
+ @encryption_required 0x03
51
+
52
+ @version Mix.Project.config()[:version]
53
+ |> String.split(".")
54
+ |> Enum.map(&(Integer.parse(&1, 10) |> elem(0)))
55
+
56
+ @spec encode(maybe_improper_list()) :: [binary(), ...]
57
+ def encode(opts) do
58
+ stream = [
59
+ {@version_token, get_version()},
60
+ encode_encryption(opts),
61
+ # when instance id check is sent, encryption is not negotiated
62
+ # encode_instance(opts),
63
+ encode_thread_id(opts),
64
+ encode_mars(opts),
65
+ encode_fed_auth_required(opts)
66
+ ]
67
+
68
+ start_offset = 5 * Enum.count(stream) + 1
69
+
70
+ {iodata, _} =
71
+ stream
72
+ |> Enum.reduce({[[], @termintator_token, []], start_offset}, fn
73
+ {token, option_data}, {[options, term, data], offset} ->
74
+ data_length = byte_size(option_data)
75
+
76
+ options = [
77
+ options,
78
+ <<token, offset::ushort(), data_length::ushort()>>
79
+ ]
80
+
81
+ data = [data, option_data]
82
+ {[options, term, data], offset + data_length}
83
+ end)
84
+
85
+ data = IO.iodata_to_binary(iodata)
86
+ Tds.Messages.encode_packets(@packet_header, data)
87
+ end
88
+
89
+ defp get_version do
90
+ @version
91
+ |> case do
92
+ [major, minor, build] ->
93
+ <<build::little-ushort, minor, major, 0x00, 0x00>>
94
+
95
+ [major, minor] ->
96
+ <<0x00, 0x00, minor, major, 0x00, 0x00>>
97
+
98
+ _ ->
99
+ # probably PRE-release
100
+ <<0x01, 0x00, 0, 1, 0x00, 0x00>>
101
+ end
102
+ end
103
+
104
+ # TODO: Add support for client certificates
105
+ defp encode_encryption(opts) do
106
+ data =
107
+ case ssl?(opts) do
108
+ :on ->
109
+ <<@encryption_on::byte>>
110
+
111
+ :not_supported ->
112
+ <<@encryption_not_supported::byte>>
113
+
114
+ :required ->
115
+ <<@encryption_required::byte>>
116
+
117
+ :off ->
118
+ # TODO: Support ssl: :off
119
+ # This requires that the LOGIN7 message is send encrypted, but
120
+ # the other packages are send unencrypted over the wire.
121
+ raise ArgumentError, ~s("ssl: :off" is currently not supported)
122
+
123
+ # <<@encryption_off::byte>>
124
+
125
+ value ->
126
+ raise ArgumentError, "invalid value for :ssl: #{inspect(value)}"
127
+ end
128
+
129
+ {@encryption_token, data}
130
+ end
131
+
132
+ # defp encode_instance(opts) do
133
+ # # not working for some reason
134
+ # instance = Keyword.get(opts, :instance)
135
+
136
+ # if is_nil(instance) do
137
+ # {@instopt_token, <<0x00>>}
138
+ # else
139
+ # {@instopt_token, instance <> <<0x00>>}
140
+ # end
141
+ # end
142
+
143
+ defp encode_thread_id(_opts) do
144
+ pid_serial =
145
+ self()
146
+ |> inspect()
147
+ |> String.split(".")
148
+ |> Enum.at(1)
149
+ |> Integer.parse()
150
+ |> elem(0)
151
+
152
+ {@thread_id_token, <<pid_serial::ulong()>>}
153
+ end
154
+
155
+ defp encode_mars(_opts) do
156
+ {@mars_token, <<0x00>>}
157
+ end
158
+
159
+ defp encode_fed_auth_required(_opts) do
160
+ {@fed_auth_required_token, <<0x01>>}
161
+ end
162
+
163
+ # DECODE
164
+ @spec decode(iodata(), state()) ::
165
+ {:encrypt, state()}
166
+ | {:login, state()}
167
+ | {:disconnect, Tds.Error.t(), state()}
168
+ def decode(packet_data, %{opts: opts} = s) do
169
+ {:ok, %{encryption: encryption, instance: instance}} =
170
+ packet_data
171
+ |> IO.iodata_to_binary()
172
+ |> decode_tokens([], s)
173
+
174
+ case {ssl?(opts), encryption, instance} do
175
+ {_, _, false} ->
176
+ msg = "Connection terminated, connected instance is not '#{instance}'!"
177
+ disconnect(msg, s)
178
+
179
+ # Encryption is off. Allowed server response is :off or :not_supported
180
+ {:off, enc, _} when enc in [<<@encryption_off>>, <<@encryption_not_supported>>] ->
181
+ {:login, s}
182
+
183
+ # TODO: Encryption is off but server has encryption on. Should upgrade.
184
+ {:off, <<@encryption_required>>, _} ->
185
+ disconnect("Server does not allow the requested encryption level.", s)
186
+
187
+ # Encryption is not supported. The server needs to respond with :not_supported
188
+ {:not_supported, <<@encryption_not_supported>>, _} ->
189
+ {:login, s}
190
+
191
+ # Encryption is on. The server needs to respond with :on
192
+ {:on, <<@encryption_on>>, _} ->
193
+ {:encrypt, s}
194
+
195
+ # Encryption is required. The server needs to respond with :on
196
+ {:required, <<@encryption_on>>, _} ->
197
+ {:encrypt, s}
198
+
199
+ {_, _, _} ->
200
+ disconnect("Server does not allow the requested encryption level.", s)
201
+ end
202
+ end
203
+
204
+ defp decode_tokens(
205
+ <<@version_token, offset::ushort, length::ushort, tail::binary>>,
206
+ tokens,
207
+ s
208
+ ) do
209
+ tokens = [{:version, offset, length} | tokens]
210
+ decode_tokens(tail, tokens, s)
211
+ end
212
+
213
+ defp decode_tokens(
214
+ <<@encryption_token, offset::ushort, length::ushort, tail::binary>>,
215
+ tokens,
216
+ s
217
+ ) do
218
+ tokens = [{:encryption, offset, length} | tokens]
219
+ decode_tokens(tail, tokens, s)
220
+ end
221
+
222
+ defp decode_tokens(
223
+ <<@instopt_token, offset::ushort, length::ushort, tail::binary>>,
224
+ tokens,
225
+ s
226
+ ) do
227
+ tokens = [{:encryption, offset, length} | tokens]
228
+ decode_tokens(tail, tokens, s)
229
+ end
230
+
231
+ defp decode_tokens(
232
+ <<@thread_id_token, offset::ushort, length::ushort, tail::binary>>,
233
+ tokens,
234
+ s
235
+ ) do
236
+ tokens = [{:thread_id, offset, length} | tokens]
237
+ decode_tokens(tail, tokens, s)
238
+ end
239
+
240
+ defp decode_tokens(
241
+ <<@mars_token, offset::ushort, length::ushort, tail::binary>>,
242
+ tokens,
243
+ s
244
+ ) do
245
+ tokens = [{:mars, offset, length} | tokens]
246
+ decode_tokens(tail, tokens, s)
247
+ end
248
+
249
+ defp decode_tokens(
250
+ <<@fed_auth_required_token, offset::ushort, length::ushort, tail::binary>>,
251
+ tokens,
252
+ s
253
+ ) do
254
+ tokens = [{:fed_auth_required, offset, length} | tokens]
255
+ decode_tokens(tail, tokens, s)
256
+ end
257
+
258
+ defp decode_tokens(
259
+ <<@nonce_opt_token, offset::ushort, length::ushort, tail::binary>>,
260
+ tokens,
261
+ s
262
+ ) do
263
+ tokens = [{:nonce_opt, offset, length} | tokens]
264
+ decode_tokens(tail, tokens, s)
265
+ end
266
+
267
+ defp decode_tokens(
268
+ <<@termintator_token, tail::binary>>,
269
+ tokens,
270
+ _s
271
+ ) do
272
+ {:ok, decode_data(Enum.reverse(tokens), tail, %__MODULE__{})}
273
+ end
274
+
275
+ defp decode_data([], _, result), do: result
276
+
277
+ defp decode_data([{key, _, length} | tokens], bin, m) do
278
+ <<data::binary-size(length), tail::binary>> = bin
279
+
280
+ case key do
281
+ :version ->
282
+ <<major, minor, patch, trivial, subbuild, _>> = data
283
+
284
+ decode_data(
285
+ tokens,
286
+ tail,
287
+ %{m | version: {major, minor, patch, trivial, subbuild}}
288
+ )
289
+
290
+ :encryption ->
291
+ decode_data(
292
+ tokens,
293
+ tail,
294
+ %{m | encryption: data}
295
+ )
296
+
297
+ :instance ->
298
+ decode_data(
299
+ tokens,
300
+ tail,
301
+ %{m | instance: data == <<0x00>>}
302
+ )
303
+
304
+ # :thread_id ->
305
+ # :mars ->
306
+ # :fed_auth_required ->
307
+ # :nonce_opt ->
308
+ _ ->
309
+ decode_data(tokens, tail, m)
310
+ end
311
+ end
312
+
313
+ defp disconnect(message, s) do
314
+ {:disconnect, Tds.Error.exception(message), s}
315
+ end
316
+
317
+ defp ssl?(opts) do
318
+ case opts[:ssl] do
319
+ nil ->
320
+ :not_supported
321
+
322
+ true ->
323
+ :required
324
+
325
+ false ->
326
+ :not_supported
327
+
328
+ other ->
329
+ other
330
+ end
331
+ end
332
+ end
changed lib/tds/query.ex
 
@@ -1,10 +1,12 @@
1
1
defmodule Tds.Query do
2
- @moduledoc false
2
+ @moduledoc """
3
+ TDS query encoding and decoding
4
+ """
3
5
4
6
@type t :: %__MODULE__{
5
- statement: String.t,
6
- handle: term
7
- }
7
+ statement: String.t(),
8
+ handle: term
9
+ }
8
10
9
11
defstruct [:statement, :handle]
10
12
end
added lib/tds/tls.ex
 
@@ -0,0 +1,219 @@
1
+ defmodule Tds.Tls do
2
+ @moduledoc false
3
+ use GenServer
4
+
5
+ require Logger
6
+
7
+ import Kernel, except: [send: 2]
8
+ import Tds.BinaryUtils
9
+
10
+ @default_ssl_opts [active: false, cb_info: {Tds.Tls, :tcp, :tcp_closed, :tcp_error}]
11
+
12
+ defstruct [:socket, :ssl_opts, :owner_pid, :handshake?, :buffer]
13
+
14
+ def connect(socket, ssl_opts) do
15
+ ssl_opts = ssl_opts ++ @default_ssl_opts
16
+ :inet.setopts(socket, active: false)
17
+
18
+ with {:ok, pid} <- GenServer.start_link(__MODULE__, {socket, ssl_opts}, []),
19
+ :ok <- :gen_tcp.controlling_process(socket, pid) do
20
+ connection_result = :ssl.connect(socket, ssl_opts, :infinity)
21
+
22
+ # Check if ssl connection was established successfully
23
+ if elem(connection_result, 0) == :ok do
24
+ GenServer.cast(pid, :handshake_complete)
25
+ end
26
+
27
+ connection_result
28
+ else
29
+ error -> error
30
+ end
31
+ end
32
+
33
+ def controlling_process(socket, tls_conn_pid) do
34
+ socket
35
+ |> assert_connected!()
36
+ |> GenServer.call({:controlling_process, tls_conn_pid})
37
+ end
38
+
39
+ def send(socket, payload) do
40
+ socket
41
+ |> assert_connected!()
42
+ |> GenServer.call({:send, payload})
43
+ end
44
+
45
+ def recv(socket, length, timeout \\ :infinity) do
46
+ socket
47
+ |> assert_connected!()
48
+ |> GenServer.call({:recv, length, timeout}, timeout)
49
+ end
50
+
51
+ defdelegate getopts(port, options), to: :inet
52
+
53
+ # defdelegate setopts(socket, options), to: :inet
54
+ def setopts(socket, options) do
55
+ socket
56
+ |> assert_connected!()
57
+ |> GenServer.call({:setopts, options})
58
+ end
59
+
60
+ defdelegate peername(socket), to: :inet
61
+
62
+ :exports
63
+ |> :gen_tcp.module_info()
64
+ |> Enum.reject(fn {fun, _} ->
65
+ fun in [:send, :recv, :module_info, :controlling_process]
66
+ end)
67
+ |> Enum.each(fn
68
+ {name, 0} ->
69
+ defdelegate unquote(name)(), to: :gen_tcp
70
+
71
+ {name, 1} ->
72
+ defdelegate unquote(name)(arg1), to: :gen_tcp
73
+
74
+ {name, 2} ->
75
+ defdelegate unquote(name)(arg1, arg2), to: :gen_tcp
76
+
77
+ {name, 3} ->
78
+ defdelegate unquote(name)(arg1, arg2, arg3), to: :gen_tcp
79
+
80
+ {name, 4} ->
81
+ defdelegate unquote(name)(arg1, arg2, arg3, arg4), to: :gen_tcp
82
+ end)
83
+
84
+ # Asserts that the port / socket is still open and returns its `pid`
85
+ defp assert_connected!(socket) do
86
+ {:connected, pid} = Port.info(socket, :connected)
87
+ pid
88
+ end
89
+
90
+ # SERVER
91
+ def init({socket, ssl_opts}) do
92
+ {:ok, %__MODULE__{socket: socket, ssl_opts: ssl_opts, handshake?: true}}
93
+ end
94
+
95
+ def handle_call({:controlling_process, tls_conn_pid}, _from, s) do
96
+ {:reply, :ok, %{s | owner_pid: tls_conn_pid}}
97
+ end
98
+
99
+ def handle_call({:setopts, options}, _from, %{socket: socket, handshake?: hs} = s) do
100
+ tds_header_size = if hs == true, do: 8, else: 0
101
+
102
+ opts =
103
+ options
104
+ |> Enum.map(fn
105
+ {:active, val} when is_number(val) -> {:active, val + tds_header_size}
106
+ val -> val
107
+ end)
108
+
109
+ {:reply, :inet.setopts(socket, opts), s}
110
+ end
111
+
112
+ def handle_call({:send, data}, _from, %{socket: socket, handshake?: true} = s) do
113
+ size = IO.iodata_length(data) + 8
114
+
115
+ header = <<0x12, 0x01, size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>>
116
+
117
+ resp = :gen_tcp.send(socket, [header, data])
118
+ {:reply, resp, s}
119
+ end
120
+
121
+ def handle_call({:send, data}, _from, %{socket: socket, handshake?: false} = s) do
122
+ resp = :gen_tcp.send(socket, data)
123
+ {:reply, resp, s}
124
+ end
125
+
126
+ # def handle_call({:recv, length, timeout}, _from, %{socket: socket, handshake?: true} = s) do
127
+ # res = case :gen_tcp.recv(socket, length, timeout) do
128
+ # {:ok, data}
129
+ # end
130
+ # {:reply, res, s}
131
+ # end
132
+
133
+ def handle_call({:recv, length, timeout}, _from, %{socket: socket} = s) do
134
+ res = :gen_tcp.recv(socket, length, timeout)
135
+ {:reply, res, s}
136
+ end
137
+
138
+ def handle_cast(:handshake_complete, s), do: {:noreply, %{s | handshake?: false}}
139
+
140
+ def handle_info({:tcp, _, _} = msg, %{owner_pid: pid, handshake?: false, buffer: nil} = s) do
141
+ Kernel.send(pid, msg)
142
+ {:noreply, s}
143
+ end
144
+
145
+ def handle_info(
146
+ {:tcp, port, <<0x12, 0, size::unsigned-16, _::32, tail::binary>>},
147
+ %{socket: socket, owner_pid: pid, buffer: nil, handshake?: true} = s
148
+ ) do
149
+ expecting = size - 8
150
+
151
+ case tail do
152
+ <<ssl_payload::binary(expecting), next_packet::binary>> ->
153
+ Kernel.send(pid, {:tcp, socket, ssl_payload})
154
+ handle_info({:tcp, port, next_packet}, %{s | buffer: nil})
155
+
156
+ next_slice ->
157
+ state = %{s | buffer: {next_slice, expecting}}
158
+ {:noreply, state}
159
+ end
160
+ end
161
+
162
+ def handle_info(
163
+ {:tcp, port, <<0x12, 1, size::unsigned-16, _::32, tail::binary>>},
164
+ %{socket: socket, owner_pid: pid, buffer: nil, handshake?: true} = s
165
+ ) do
166
+ expecting = size - 8
167
+
168
+ case tail do
169
+ <<ssl_payload::binary(expecting), next_packet::binary>> ->
170
+ Kernel.send(pid, {:tcp, socket, ssl_payload})
171
+ handle_info({:tcp, port, next_packet}, %{s | buffer: nil})
172
+
173
+ next_slice ->
174
+ state = %{s | buffer: {next_slice, expecting}}
175
+ {:noreply, state}
176
+ end
177
+ end
178
+
179
+ def handle_info(
180
+ {:tcp, port, bin},
181
+ %{socket: socket, owner_pid: pid, buffer: {slice, expecting}, handshake?: true} = s
182
+ ) do
183
+ case IO.iodata_to_binary([slice, bin]) do
184
+ <<ssl_payload::binary(expecting), next_packet::binary>> ->
185
+ Kernel.send(pid, {:tcp, socket, ssl_payload})
186
+ handle_info({:tcp, port, next_packet}, %{s | buffer: nil})
187
+
188
+ next_slice ->
189
+ state = %{s | buffer: {next_slice, expecting}}
190
+ {:noreply, state}
191
+ end
192
+ end
193
+
194
+ def handle_info({:tcp, _, _} = msg, %{owner_pid: pid, handshake?: true, buffer: nil} = s) do
195
+ Kernel.send(pid, msg)
196
+ {:noreply, s}
197
+ end
198
+
199
+ def handle_info(
200
+ {:tcp_passive, _port} = msg,
201
+ %{owner_pid: pid, handshake?: false, buffer: nil} = s
202
+ ) do
203
+ Kernel.send(pid, msg)
204
+ {:noreply, s}
205
+ end
206
+
207
+ def handle_info({tag, _} = msg, %{owner_pid: pid} = s) when tag in [:tcp_closed, :ssl_closed] do
208
+ # todo
209
+ send(pid, msg)
210
+ {:stop, tag, s}
211
+ end
212
+
213
+ def handle_info({tag, _, _} = msg, %{owner_pid: pid} = s)
214
+ when tag in [:tcp_error, :ssl_error] do
215
+ # todo
216
+ send(pid, msg)
217
+ {:stop, tag, s}
218
+ end
219
+ end
removed lib/tds/token_descriptors/basic.ex
 
@@ -1,3 +0,0 @@
1
- defmodule Tds.TokenDescriptors.Basic do
2
-
3
- end
changed lib/tds/tokens.ex
 
@@ -1,11 +1,11 @@
1
1
defmodule Tds.Tokens do
2
2
import Tds.BinaryUtils
3
- import Tds.Utils
4
3
import Bitwise
5
4
6
5
require Logger
7
6
8
7
alias Tds.Types
8
+ alias Tds.UCS2
9
9
10
10
def retval_typ_size(38) do
11
11
# 0x26 - SYBINTN - 1
 
@@ -86,7 +86,7 @@ defmodule Tds.Tokens do
86
86
data::binary
87
87
>> = bin
88
88
89
- name = ucs2_to_utf(name)
89
+ name = UCS2.to_string(name)
90
90
{type_info, tail} = Tds.Types.decode_info(data)
91
91
{value, tail} = Tds.Types.decode_data(type_info, tail)
92
92
param = %Tds.Parameter{name: name, value: value, direction: :output}
 
@@ -138,9 +138,9 @@ defmodule Tds.Tokens do
138
138
number: number,
139
139
state: state,
140
140
class: class,
141
- msg_text: ucs2_to_utf(:binary.copy(msg)),
142
- server_name: ucs2_to_utf(:binary.copy(server_name)),
143
- proc_name: ucs2_to_utf(:binary.copy(proc_name)),
141
+ msg_text: UCS2.to_string(:binary.copy(msg)),
142
+ server_name: UCS2.to_string(:binary.copy(server_name)),
143
+ proc_name: UCS2.to_string(:binary.copy(proc_name)),
144
144
line_number: line_number
145
145
}
146
146
 
@@ -170,9 +170,9 @@ defmodule Tds.Tokens do
170
170
number: number,
171
171
state: state,
172
172
class: class,
173
- msg_text: ucs2_to_utf(msg),
174
- server_name: ucs2_to_utf(server_name),
175
- proc_name: ucs2_to_utf(proc_name),
173
+ msg_text: UCS2.to_string(msg),
174
+ server_name: UCS2.to_string(server_name),
175
+ proc_name: UCS2.to_string(proc_name),
176
176
line_number: line_number
177
177
}
178
178
 
@@ -227,8 +227,8 @@ defmodule Tds.Tokens do
227
227
rest::binary
228
228
>> = tail
229
229
230
- new_database = ucs2_to_utf(new_value)
231
- old_database = ucs2_to_utf(old_value)
230
+ new_database = UCS2.to_string(new_value)
231
+ old_database = UCS2.to_string(old_value)
232
232
{{:database, new_database, old_database}, rest}
233
233
234
234
0x02 ->
 
@@ -240,8 +240,8 @@ defmodule Tds.Tokens do
240
240
rest::binary
241
241
>> = tail
242
242
243
- new_language = ucs2_to_utf(new_value)
244
- old_language = ucs2_to_utf(old_value)
243
+ new_language = UCS2.to_string(new_value)
244
+ old_language = UCS2.to_string(old_value)
245
245
{{:language, new_language, old_language}, rest}
246
246
247
247
0x03 ->
 
@@ -253,8 +253,8 @@ defmodule Tds.Tokens do
253
253
rest::binary
254
254
>> = tail
255
255
256
- new_charset = ucs2_to_utf(new_value)
257
- old_charset = ucs2_to_utf(old_value)
256
+ new_charset = UCS2.to_string(new_value)
257
+ old_charset = UCS2.to_string(old_value)
258
258
{{:charset, new_charset, old_charset}, rest}
259
259
260
260
0x04 ->
 
@@ -268,7 +268,7 @@ defmodule Tds.Tokens do
268
268
269
269
new_packetsize =
270
270
new_value
271
- |> ucs2_to_utf()
271
+ |> UCS2.to_string()
272
272
|> Integer.parse()
273
273
|> case do
274
274
:error -> 4096
 
@@ -278,7 +278,7 @@ defmodule Tds.Tokens do
278
278
279
279
old_packetsize =
280
280
old_value
281
- |> ucs2_to_utf()
281
+ |> UCS2.to_string()
282
282
|> Integer.parse()
283
283
|> case do
284
284
:error -> 4096
 
@@ -386,7 +386,7 @@ defmodule Tds.Tokens do
386
386
rest::binary
387
387
>> = tail
388
388
389
- {{:userinfo, ucs2_to_utf(value), nil}, rest}
389
+ {{:userinfo, UCS2.to_string(value), nil}, rest}
390
390
391
391
0x14 ->
392
392
<<
 
@@ -402,7 +402,7 @@ defmodule Tds.Tokens do
402
402
>> = tail
403
403
404
404
routing = %{
405
- hostname: ucs2_to_utf(alt_host),
405
+ hostname: UCS2.to_string(alt_host),
406
406
port: port
407
407
}
408
408
 
@@ -414,8 +414,7 @@ defmodule Tds.Tokens do
414
414
415
415
## DONE
416
416
defp decode_done(
417
- <<status::little-unsigned-size(2)-unit(8),
418
- cur_cmd::little-unsigned-size(2)-unit(8),
417
+ <<status::little-unsigned-size(2)-unit(8), cur_cmd::little-unsigned-size(2)-unit(8),
419
418
row_count::little-size(8)-unit(8), tail::binary>>,
420
419
collmetadata
421
420
) do
 
@@ -469,7 +468,7 @@ defmodule Tds.Tokens do
469
468
token = %{
470
469
t_sql_only: interface == 1,
471
470
tds_version: tds_version,
472
- program: ucs2_to_utf(prog_name),
471
+ program: UCS2.to_string(prog_name),
473
472
version: "#{major_ver}.#{minor_ver}.#{build_hi}.#{build_low}"
474
473
}
475
474
 
@@ -493,8 +492,7 @@ defmodule Tds.Tokens do
493
492
end
494
493
495
494
defp bitmap_list(
496
- <<b8::1, b7::1, b6::1, b5::1, b4::1, b3::1, b2::1, b1::1,
497
- tail::binary>>,
495
+ <<b8::1, b7::1, b6::1, b5::1, b4::1, b3::1, b2::1, b1::1, tail::binary>>,
498
496
n
499
497
) do
500
498
{bits, tail} = bitmap_list(tail, n - 1)
 
@@ -523,10 +521,8 @@ defmodule Tds.Tokens do
523
521
{info, tail}
524
522
end
525
523
526
- defp decode_column_name(
527
- <<length::int8, name::binary-size(length)-unit(16), tail::binary>>
528
- ) do
529
- name = ucs2_to_utf(name)
524
+ defp decode_column_name(<<length::int8, name::binary-size(length)-unit(16), tail::binary>>) do
525
+ name = UCS2.to_string(name)
530
526
{name, tail}
531
527
end
changed lib/tds/types.ex
 
@@ -4,13 +4,18 @@ defmodule Tds.Types do
4
4
use Bitwise
5
5
6
6
alias Tds.Parameter
7
+ alias Tds.UCS2
7
8
8
9
@year_1900_days :calendar.date_to_gregorian_days({1900, 1, 1})
9
10
@secs_in_min 60
10
11
@secs_in_hour 60 * @secs_in_min
11
12
@max_time_scale 7
12
13
14
+ # Zero Length Data Types
13
15
@tds_data_type_null 0x1F
16
+
17
+ # Fixed Length Data Types
18
+ # See: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/859eb3d2-80d3-40f6-a637-414552c9c552
14
19
@tds_data_type_tinyint 0x30
15
20
@tds_data_type_bit 0x32
16
21
@tds_data_type_smallint 0x34
 
@@ -23,21 +28,24 @@ defmodule Tds.Types do
23
28
@tds_data_type_smallmoney 0x7A
24
29
@tds_data_type_bigint 0x7F
25
30
26
- @fixed_data_types [
27
- @tds_data_type_null,
28
- @tds_data_type_tinyint,
29
- @tds_data_type_bit,
30
- @tds_data_type_smallint,
31
- @tds_data_type_int,
32
- @tds_data_type_smalldatetime,
33
- @tds_data_type_real,
34
- @tds_data_type_money,
35
- @tds_data_type_datetime,
36
- @tds_data_type_float,
37
- @tds_data_type_smallmoney,
38
- @tds_data_type_bigint
39
- ]
31
+ # Fixed Data Types with their length
32
+ @fixed_data_types %{
33
+ @tds_data_type_null => 0,
34
+ @tds_data_type_tinyint => 1,
35
+ @tds_data_type_bit => 1,
36
+ @tds_data_type_smallint => 2,
37
+ @tds_data_type_int => 4,
38
+ @tds_data_type_smalldatetime => 4,
39
+ @tds_data_type_real => 4,
40
+ @tds_data_type_money => 8,
41
+ @tds_data_type_datetime => 8,
42
+ @tds_data_type_float => 8,
43
+ @tds_data_type_smallmoney => 4,
44
+ @tds_data_type_bigint => 8
45
+ }
40
46
47
+ # Variable-Length Data Types
48
+ # See: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/ce3183a6-9d89-47e8-a02f-de5a1a1303de
41
49
@tds_data_type_uniqueidentifier 0x24
42
50
@tds_data_type_intn 0x26
43
51
# legacy
 
@@ -160,42 +168,11 @@ defmodule Tds.Types do
160
168
end
161
169
162
170
def decode_info(<<data_type_code::unsigned-8, tail::binary>>)
163
- when data_type_code in @fixed_data_types do
164
- length =
165
- cond do
166
- data_type_code == @tds_data_type_null ->
167
- 0
168
-
169
- data_type_code in [
170
- @tds_data_type_tinyint,
171
- @tds_data_type_bit
172
- ] ->
173
- 1
174
-
175
- data_type_code == @tds_data_type_smallint ->
176
- 2
177
-
178
- data_type_code in [
179
- @tds_data_type_int,
180
- @tds_data_type_smalldatetime,
181
- @tds_data_type_real,
182
- @tds_data_type_smallmoney
183
- ] ->
184
- 4
185
-
186
- data_type_code in [
187
- @tds_data_type_datetime,
188
- @tds_data_type_float,
189
- @tds_data_type_money,
190
- @tds_data_type_bigint
191
- ] ->
192
- 8
193
- end
194
-
171
+ when is_map_key(@fixed_data_types, data_type_code) do
195
172
{%{
196
173
data_type: :fixed,
197
174
data_type_code: data_type_code,
198
- length: length,
175
+ length: @fixed_data_types[data_type_code],
199
176
data_type_name: to_atom(data_type_code)
200
177
}, tail}
201
178
end
 
@@ -372,8 +349,7 @@ defmodule Tds.Types do
372
349
1..numparts,
373
350
rest,
374
351
fn _,
375
- <<tsize::little-unsigned-16,
376
- _table_name::binary-size(tsize)-unit(16),
352
+ <<tsize::little-unsigned-16, _table_name::binary-size(tsize)-unit(16),
377
353
next_rest::binary>> ->
378
354
next_rest
379
355
end
 
@@ -696,9 +672,7 @@ defmodule Tds.Types do
696
672
# UUID
697
673
def decode_uuid(<<_::128>> = bin), do: bin
698
674
699
- def encode_uuid(
700
- <<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = string
701
- ) do
675
+ def encode_uuid(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = string) do
702
676
raise ArgumentError,
703
677
"trying to load string UUID as Tds.Types.UUID: #{inspect(string)}. " <>
704
678
"Maybe you wanted to declare :uuid as your database field?"
 
@@ -729,11 +703,11 @@ defmodule Tds.Types do
729
703
end
730
704
731
705
def decode_nchar(_data_info, <<data::binary>>) do
732
- ucs2_to_utf(data)
706
+ UCS2.to_string(data)
733
707
end
734
708
735
709
def decode_xml(_data_info, <<data::binary>>) do
736
- ucs2_to_utf(data)
710
+ UCS2.to_string(data)
737
711
end
738
712
739
713
def decode_udt(%{}, <<data::binary>>) do
 
@@ -821,7 +795,7 @@ defmodule Tds.Types do
821
795
822
796
length =
823
797
if value != nil do
824
- value = value |> to_little_ucs2
798
+ value = value |> UCS2.from_string()
825
799
value_size = byte_size(value)
826
800
827
801
if value_size == 0 or value_size > 8000 do
 
@@ -938,7 +912,7 @@ defmodule Tds.Types do
938
912
939
913
def encode_float_type(%Parameter{value: value} = param)
940
914
when is_float(value) do
941
- encode_float_type(%{param | value: to_decimal(value)})
915
+ encode_float_type(%{param | value: Decimal.from_float(value)})
942
916
end
943
917
944
918
def encode_float_type(%Parameter{value: %Decimal{} = value}) do
 
@@ -989,9 +963,7 @@ defmodule Tds.Types do
989
963
@doc """
990
964
Creates the Parameter Descriptor for the selected type
991
965
"""
992
- def encode_param_descriptor(
993
- %Parameter{name: name, value: value, type: type} = param
994
- )
966
+ def encode_param_descriptor(%Parameter{name: name, value: value, type: type} = param)
995
967
when type != nil do
996
968
desc =
997
969
case type do
 
@@ -1152,9 +1124,7 @@ defmodule Tds.Types do
1152
1124
end
1153
1125
1154
1126
# Decimal.new/0 is undefined -- modifying params to hopefully fix
1155
- def encode_decimal_descriptor(
1156
- %Parameter{type: :decimal, value: value} = param
1157
- ) do
1127
+ def encode_decimal_descriptor(%Parameter{type: :decimal, value: value} = param) do
1158
1128
encode_decimal_descriptor(%{param | value: Decimal.new(value)})
1159
1129
end
1160
1130
 
@@ -1166,7 +1136,7 @@ defmodule Tds.Types do
1166
1136
def encode_float_descriptor(%Parameter{value: value} = param)
1167
1137
when is_float(value) do
1168
1138
param
1169
- |> Map.put(:value, to_decimal(value))
1139
+ |> Map.put(:value, Decimal.from_float(value))
1170
1140
|> encode_float_descriptor
1171
1141
end
1172
1142
 
@@ -1216,7 +1186,7 @@ defmodule Tds.Types do
1216
1186
do: <<@tds_plp_null::little-unsigned-64>>
1217
1187
1218
1188
def encode_data(@tds_data_type_nvarchar, value, _) do
1219
- value = to_little_ucs2(value)
1189
+ value = UCS2.from_string(value)
1220
1190
value_size = byte_size(value)
1221
1191
1222
1192
cond do
 
@@ -1636,8 +1606,7 @@ defmodule Tds.Types do
1636
1606
# 10^scale fs in 1 sec
1637
1607
fs_per_sec = trunc(:math.pow(10, scale))
1638
1608
1639
- fsec =
1640
- hour * 3600 * fs_per_sec + min * 60 * fs_per_sec + sec * fs_per_sec + fsec
1609
+ fsec = hour * 3600 * fs_per_sec + min * 60 * fs_per_sec + sec * fs_per_sec + fsec
1641
1610
1642
1611
bin =
1643
1612
cond do
 
@@ -1803,9 +1772,9 @@ defmodule Tds.Types do
1803
1772
>> = tail
1804
1773
1805
1774
schema_info = %{
1806
- db: ucs2_to_utf(db),
1807
- prefix: ucs2_to_utf(prefix),
1808
- schema: ucs2_to_utf(schema)
1775
+ db: UCS2.to_string(db),
1776
+ prefix: UCS2.to_string(prefix),
1777
+ schema: UCS2.to_string(schema)
1809
1778
}
1810
1779
1811
1780
{schema_info, rest}
changed lib/tds/types/uuid.ex
 
@@ -1,19 +1,18 @@
1
1
defmodule Tds.Types.UUID do
2
- @moduledoc false
2
+ @moduledoc """
3
+ UUID data type
4
+ """
3
5
4
6
@doc """
5
7
Casts to UUID.
6
8
"""
7
- def cast(<< a1, a2, a3, a4, a5, a6, a7, a8, ?-,
8
- b1, b2, b3, b4, ?-,
9
- c1, c2, c3, c4, ?-,
10
- d1, d2, d3, d4, ?-,
11
- e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12 >>) do
12
- << c(a1), c(a2), c(a3), c(a4), c(a5), c(a6), c(a7), c(a8), ?-,
13
- c(b1), c(b2), c(b3), c(b4), ?-,
14
- c(c1), c(c2), c(c3), c(c4), ?-,
15
- c(d1), c(d2), c(d3), c(d4), ?-,
16
- c(e1), c(e2), c(e3), c(e4), c(e5), c(e6), c(e7), c(e8), c(e9), c(e10), c(e11), c(e12) >>
9
+ def cast(
10
+ <<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2, d3,
11
+ d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
12
+ ) do
13
+ <<c(a1), c(a2), c(a3), c(a4), c(a5), c(a6), c(a7), c(a8), ?-, c(b1), c(b2), c(b3), c(b4), ?-,
14
+ c(c1), c(c2), c(c3), c(c4), ?-, c(d1), c(d2), c(d3), c(d4), ?-, c(e1), c(e2), c(e3), c(e4),
15
+ c(e5), c(e6), c(e7), c(e8), c(e9), c(e10), c(e11), c(e12)>>
17
16
catch
18
17
:error -> :error
19
18
else
 
@@ -58,25 +57,20 @@ defmodule Tds.Types.UUID do
58
57
defp c(?d), do: ?d
59
58
defp c(?e), do: ?e
60
59
defp c(?f), do: ?f
61
- defp c(_), do: throw(:error)
60
+ defp c(_), do: throw(:error)
62
61
63
62
@doc """
64
63
Converts a string representing a UUID into a binary.
65
64
"""
66
- def dump(<< a1, a2, a3, a4, a5, a6, a7, a8, ?-,
67
- b1, b2, b3, b4, ?-,
68
- c1, c2, c3, c4, ?-,
69
- d1, d2, d3, d4, ?-,
70
- e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12 >>) do
65
+ def dump(
66
+ <<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2, d3,
67
+ d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
68
+ ) do
71
69
try do
72
- << d(a7)::4, d(a8)::4, d(a5)::4, d(a6)::4,
73
- d(a3)::4, d(a4)::4, d(a1)::4, d(a2)::4,
74
- d(b3)::4, d(b4)::4, d(b1)::4, d(b2)::4,
75
- d(c3)::4, d(c4)::4, d(c1)::4, d(c2)::4,
76
- d(d1)::4, d(d2)::4, d(d3)::4, d(d4)::4,
77
- d(e1)::4, d(e2)::4, d(e3)::4, d(e4)::4,
78
- d(e5)::4, d(e6)::4, d(e7)::4, d(e8)::4,
79
- d(e9)::4, d(e10)::4, d(e11)::4, d(e12)::4 >>
70
+ <<d(a7)::4, d(a8)::4, d(a5)::4, d(a6)::4, d(a3)::4, d(a4)::4, d(a1)::4, d(a2)::4, d(b3)::4,
71
+ d(b4)::4, d(b1)::4, d(b2)::4, d(c3)::4, d(c4)::4, d(c1)::4, d(c2)::4, d(d1)::4, d(d2)::4,
72
+ d(d3)::4, d(d4)::4, d(e1)::4, d(e2)::4, d(e3)::4, d(e4)::4, d(e5)::4, d(e6)::4, d(e7)::4,
73
+ d(e8)::4, d(e9)::4, d(e10)::4, d(e11)::4, d(e12)::4>>
80
74
catch
81
75
:error -> :error
82
76
else
 
@@ -118,7 +112,7 @@ defmodule Tds.Types.UUID do
118
112
defp d(?d), do: 13
119
113
defp d(?e), do: 14
120
114
defp d(?f), do: 15
121
- defp d(_), do: throw(:error)
115
+ defp d(_), do: throw(:error)
122
116
123
117
@doc """
124
118
Converts a binary UUID into a string.
 
@@ -128,8 +122,9 @@ defmodule Tds.Types.UUID do
128
122
end
129
123
130
124
def load(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = string) do
131
- raise ArgumentError, "trying to load string UUID as Tds.Types.UUID: #{inspect string}. " <>
132
- "Maybe you wanted to declare :uuid as your database field?"
125
+ raise ArgumentError,
126
+ "trying to load string UUID as Tds.Types.UUID: #{inspect(string)}. " <>
127
+ "Maybe you wanted to declare :uuid as your database field?"
133
128
end
134
129
135
130
def load(_), do: :error
 
@@ -146,41 +141,27 @@ defmodule Tds.Types.UUID do
146
141
Generates a version 4 (random) UUID in the binary format.
147
142
"""
148
143
def bingenerate do
149
- << a1::4, a2::4, a3::4, a4::4,
150
- a5::4, a6::4, a7::4, a8::4,
151
- b1::4, b2::4, b3::4, b4::4,
152
- _ ::4, c2::4, c3::4, c4::4,
153
- d1::4, d2::4, d3::4, d4::4,
154
- e1::4, e2::4, e3::4, e4::4,
155
- _ ::4, e6::4, e7::4, e8::4,
156
- e9::4, e10::4, e11::4, e12::4 >> = :crypto.strong_rand_bytes(16)
157
- << a7::4, a8::4, a5::4, a6::4,
158
- a3::4, a4::4, a1::4, a2::4,
159
- b3::4, b4::4, b1::4, b2::4,
160
- c3::4, c4::4, 4 ::4, c2::4,
161
- d1::4, d2::4, d3::4, d4::4,
162
- e1::4, e2::4, e3::4, e4::4,
163
- 2 ::4, e6::4, e7::4, e8::4,
164
- e9::4, e10::4, e11::4, e12::4 >>
144
+ <<a1::4, a2::4, a3::4, a4::4, a5::4, a6::4, a7::4, a8::4, b1::4, b2::4, b3::4, b4::4, _::4,
145
+ c2::4, c3::4, c4::4, d1::4, d2::4, d3::4, d4::4, e1::4, e2::4, e3::4, e4::4, _::4, e6::4,
146
+ e7::4, e8::4, e9::4, e10::4, e11::4, e12::4>> = :crypto.strong_rand_bytes(16)
147
+
148
+ <<a7::4, a8::4, a5::4, a6::4, a3::4, a4::4, a1::4, a2::4, b3::4, b4::4, b1::4, b2::4, c3::4,
149
+ c4::4, 4::4, c2::4, d1::4, d2::4, d3::4, d4::4, e1::4, e2::4, e3::4, e4::4, 2::4, e6::4,
150
+ e7::4, e8::4, e9::4, e10::4, e11::4, e12::4>>
165
151
end
166
152
167
153
# Callback invoked by autogenerate fields.
168
154
@doc false
169
155
def autogenerate, do: generate()
170
156
171
- defp encode(<< a1::4, a2::4, a3::4, a4::4,
172
- a5::4, a6::4, a7::4, a8::4,
173
- b1::4, b2::4, b3::4, b4::4,
174
- c1::4, c2::4, c3::4, c4::4,
175
- d1::4, d2::4, d3::4, d4::4,
176
- e1::4, e2::4, e3::4, e4::4,
177
- e5::4, e6::4, e7::4, e8::4,
178
- e9::4, e10::4, e11::4, e12::4 >>) do
179
- << e(a7), e(a8), e(a5), e(a6), e(a3), e(a4), e(a1), e(a2), ?-,
180
- e(b3), e(b4), e(b1), e(b2), ?-,
181
- e(c3), e(c4), e(c1), e(c2), ?-,
182
- e(d1), e(d2), e(d3), e(d4), ?-,
183
- e(e1), e(e2), e(e3), e(e4), e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12) >>
157
+ defp encode(
158
+ <<a1::4, a2::4, a3::4, a4::4, a5::4, a6::4, a7::4, a8::4, b1::4, b2::4, b3::4, b4::4,
159
+ c1::4, c2::4, c3::4, c4::4, d1::4, d2::4, d3::4, d4::4, e1::4, e2::4, e3::4, e4::4,
160
+ e5::4, e6::4, e7::4, e8::4, e9::4, e10::4, e11::4, e12::4>>
161
+ ) do
162
+ <<e(a7), e(a8), e(a5), e(a6), e(a3), e(a4), e(a1), e(a2), ?-, e(b3), e(b4), e(b1), e(b2), ?-,
163
+ e(c3), e(c4), e(c1), e(c2), ?-, e(d1), e(d2), e(d3), e(d4), ?-, e(e1), e(e2), e(e3), e(e4),
164
+ e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12)>>
184
165
catch
185
166
:error -> :error
186
167
else
 
@@ -189,16 +170,16 @@ defmodule Tds.Types.UUID do
189
170
190
171
@compile {:inline, e: 1}
191
172
192
- defp e(0), do: ?0
193
- defp e(1), do: ?1
194
- defp e(2), do: ?2
195
- defp e(3), do: ?3
196
- defp e(4), do: ?4
197
- defp e(5), do: ?5
198
- defp e(6), do: ?6
199
- defp e(7), do: ?7
200
- defp e(8), do: ?8
201
- defp e(9), do: ?9
173
+ defp e(0), do: ?0
174
+ defp e(1), do: ?1
175
+ defp e(2), do: ?2
176
+ defp e(3), do: ?3
177
+ defp e(4), do: ?4
178
+ defp e(5), do: ?5
179
+ defp e(6), do: ?6
180
+ defp e(7), do: ?7
181
+ defp e(8), do: ?8
182
+ defp e(9), do: ?9
202
183
defp e(10), do: ?a
203
184
defp e(11), do: ?b
204
185
defp e(12), do: ?c
added lib/tds/ucs2.ex
 
@@ -0,0 +1,25 @@
1
+ defmodule Tds.UCS2 do
2
+ @moduledoc """
3
+ Converting UTF-8 strings into UCS-2 (UTF16 Little Endian) strings
4
+ used by the MSSQL Database
5
+ """
6
+ alias Tds.Utils
7
+
8
+ @ucs2_charset "utf-16le"
9
+
10
+ @doc """
11
+ Converts a UTF-8 string into UCS-2
12
+ """
13
+ @spec from_string(binary) :: binary
14
+ def from_string(string) when is_binary(string) do
15
+ Utils.encode_chars(string, @ucs2_charset)
16
+ end
17
+
18
+ @doc """
19
+ Converts a UCS-2 string into UTF-8
20
+ """
21
+ @spec to_string(binary) :: binary
22
+ def to_string(string) when is_binary(string) do
23
+ Utils.decode_chars(string, @ucs2_charset)
24
+ end
25
+ end
changed lib/tds/utils.ex
 
@@ -1,65 +1,15 @@
1
1
defmodule Tds.Utils do
2
2
@moduledoc false
3
- require Logger
4
-
5
- def to_hex_list(x) when is_list(x) do
6
- Enum.map(x, &Base.encode16(<<&1>>))
7
- end
8
-
9
- def to_hex_list(x) when is_binary(x) do
10
- x
11
- |> :erlang.binary_to_list()
12
- |> to_hex_list()
13
- end
14
-
15
- def to_hex_string(x) when is_binary(x) do
16
- x
17
- |> to_hex_list()
18
- |> to_hex_string()
19
- end
20
-
21
- def to_hex_string(x) when is_list(x) do
22
- Enum.join(x, " ")
23
- end
24
-
25
- def to_little_ucs2(str) when is_list(str) do
26
- str
27
- |> IO.iodata_to_binary()
28
- |> to_little_ucs2()
29
- end
30
-
31
- def to_little_ucs2(str) do
32
- encode_chars(str, "utf-16le")
33
- end
34
-
35
- def ucs2_to_utf(s) do
36
- decode_chars(s, "utf-16le")
37
- end
38
-
39
3
def encode_chars(string, to_codepage) do
40
4
Application.get_env(:tds, :text_encoder, Tds.Latin1)
41
5
|> apply(:encode, [string, to_codepage])
42
6
end
43
7
44
- def decode_chars(binary, from_codepage) do
8
+ def decode_chars(binary, from_codepage) when is_binary(binary) do
45
9
Application.get_env(:tds, :text_encoder, Tds.Latin1)
46
10
|> apply(:decode, [binary, from_codepage])
47
11
end
48
12
49
- def to_boolean(<<1>>) do
50
- true
51
- end
52
-
53
- def to_boolean(<<0>>) do
54
- false
55
- end
56
-
57
- def error(error, _s) do
58
- {:error, error}
59
- end
60
-
61
- def to_decimal(float), do: Decimal.from_float(float)
62
-
63
13
@doc false
64
14
def use_elixir_calendar_types(value),
65
15
do: Process.put(:use_elixir_calendar_types, value)
changed lib/tds/versions.ex
 
@@ -1,26 +1,23 @@
1
1
defmodule Tds.Version do
2
2
import Tds.Protocol.Grammar
3
3
4
- defstruct version: 0x74000004, str_version: "7.4"
4
+ @default_version :v7_4
5
+ @default_code 0x74000004
5
6
6
7
@versions [
7
- {0x71000001, "7.1"},
8
- {0x72090002, "7.2"},
9
- {0x730A0003, "7.3.A"},
10
- {0x730B0003, "7.3.B"},
11
- {0x74000004, "7.4"}
8
+ {0x71000001, :v7_1},
9
+ {0x72090002, :v7_2},
10
+ {0x730A0003, :v7_3_a},
11
+ {0x730B0003, :v7_3_b},
12
+ {0x74000004, :v7_4}
12
13
]
13
14
14
15
def decode(<<key::little-dword>>) do
15
- @versions
16
- |> List.keyfind(key, 0, "7.4")
16
+ List.keyfind(@versions, key, 0, @default_version)
17
17
end
18
18
19
19
def encode(ver) do
20
- val =
21
- @versions
22
- |> List.keyfind(ver, 1, 0x74000004)
23
-
20
+ val = List.keyfind(@versions, ver, 1, @default_code)
24
21
<<val::little-dword>>
25
22
end
26
23
end
changed mix.exs
 
@@ -2,7 +2,7 @@ defmodule Tds.Mixfile do
2
2
@moduledoc false
3
3
use Mix.Project
4
4
5
- @version "2.2.0"
5
+ @version "2.3.0-rc.0"
6
6
def project do
7
7
[
8
8
app: :tds,
 
@@ -21,12 +21,7 @@ defmodule Tds.Mixfile do
21
21
# Docs
22
22
name: "Tds",
23
23
source_url: "https://github.com/livehelpnow/tds",
24
- docs: [
25
- main: "readme",
26
- extras: ["README.md", "CHANGELOG.md"],
27
- source_ref: "v#{@version}",
28
- source_url: "https://github.com/livehelpnow/tds"
29
- ]
24
+ docs: docs()
30
25
]
31
26
end
32
27
 
@@ -45,7 +40,7 @@ defmodule Tds.Mixfile do
45
40
{:decimal, "~> 1.9 or ~> 2.0"},
46
41
{:jason, "~> 1.0", optional: true},
47
42
{:db_connection, "~> 2.0"},
48
- {:ex_doc, "~> 0.19", only: :dev},
43
+ {:ex_doc, "~> 0.19", only: :docs},
49
44
{:tds_encoding, "~> 1.1", optional: true, only: :test},
50
45
{:tzdata, "~> 1.0", optional: true, only: :test}
51
46
]
 
@@ -57,6 +52,19 @@ defmodule Tds.Mixfile do
57
52
"""
58
53
end
59
54
55
+ defp docs do
56
+ [
57
+ extras: [
58
+ "CHANGELOG.md": [title: "Changelog"],
59
+ "README.md": [title: "Overview"]
60
+ ],
61
+ main: "readme",
62
+ source_ref: "v#{@version}",
63
+ source_url: "https://github.com/livehelpnow/tds",
64
+ formatters: ["html"]
65
+ ]
66
+ end
67
+
60
68
defp package do
61
69
[
62
70
name: "tds",