%% @author Justin Sheehy %% @author Andy Gross %% @copyright 2007-2009 Basho Technologies, 2018-2022 Marc Worrell. %% Based on mochiweb_request.erl, which is Copyright 2007 Mochi Media, Inc. %% @doc Response functions, generate the response inclusive body and headers. %% The body can be sourced from multiple sources. These sources include files, %% binary files and functions. %% The response body function handles range requests. %% @reference mochiweb_request.erl %% @end %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. -module(cowmachine_response). -author('Marc Worrell '). -export([ send_response/1, server_header/0 ]). % Debugging -export([ send_stream_body/2 ]). -define(FILE_CHUNK_LENGTH, 16#80000). % 512KB -define(DEFAULT_IDLE_TIMEOUT, 60000). %% @doc Returns server header. -spec server_header() -> Result when Result :: binary(). server_header() -> case application:get_env(cowmachine, server_header) of {ok, Server} -> z_convert:to_binary(Server); undefined -> case application:get_key(cowmachine, vsn) of {ok, Version} -> <<"CowMachine/", (z_convert:to_binary(Version))/binary>>; undefined -> <<"CowMachine">> end end. %% @doc Send responce. -spec send_response(Context) -> Result when Context :: cowmachine_req:context(), Result :: {ok, Req, Env} | {stop, Req}, Req :: cowboy_req:req(), Env :: cowboy_middleware:env(). send_response(Context) -> ControllerOptions = cowmachine_req:controller_options(Context), HttpStatusCode = case proplists:get_value(http_status_code, ControllerOptions) of Code when is_integer(Code) -> Code; _ -> cowmachine_req:response_code(Context) end, Context1 = send_response_range(HttpStatusCode, Context), {ok, cowmachine_req:req(Context1), cowmachine_req:env(Context1)}. %% -------------------------------------------------------------------------------------------------- %% Internal functions %% -------------------------------------------------------------------------------------------------- -spec send_response_range(Code, Context) -> Result when Code :: integer(), Context :: cowmachine_req:context(), Result :: cowmachine_req:context(). send_response_range(200, Context) -> {Range, Context1} = get_range(Context), case Range of undefined -> send_response_code(200, all, Context1); Ranges -> case get_resp_body_size(cowmachine_req:resp_body(Context1)) of {ok, Size, Body1} -> Context2 = cowmachine_req:set_resp_body(Body1, Context1), case range_parts(Ranges, Size) of [] -> % no valid ranges % could be 416, for now we'll just return 200 and the whole body send_response_code(200, all, Context2); PartList -> % error_logger:info_msg("PARTS ~p (~p)", [PartList, wrq:path(ReqData)]), ContentType = cowmachine_req:get_resp_header(<<"content-type">>, Context2), {RangeHeaders, Boundary} = make_range_headers(PartList, Size, ContentType), Context3 = cowmachine_req:set_resp_headers(RangeHeaders, Context2), send_response_code(206, {PartList, Size, Boundary, ContentType}, Context3) end; {error, nosize} -> send_response_code(200, all, Context1) end end; send_response_range(Code, Context) -> send_response_code(Code, all, Context). -spec send_response_code(Code, Parts, Context) -> Result when Code :: integer(), Parts :: cowmachine_req:parts(), Context :: cowmachine_req:context(), Result :: cowmachine_req:context(). send_response_code(Code, Parts, Context) -> send_response_bodyfun(cowmachine_req:resp_body(Context), Code, Parts, Context). -spec send_response_bodyfun(RespBody, Code, Parts, Context) -> Result when RespBody :: cowmachine_req:resp_body(), Code :: integer(), Parts :: cowmachine_req:parts(), Context :: cowmachine_req:context(), Result :: cowmachine_req:context(). send_response_bodyfun({device, IO}, Code, Parts, Context) -> Length = iodevice_size(IO), send_response_bodyfun({device, Length, IO}, Code, Parts, Context); send_response_bodyfun({device, Length, IO}, Code, all, Context) -> Writer = fun(FunContext) -> send_device_body(FunContext, Length, IO), _ = file:close(IO), FunContext end, start_response_stream(Code, Length, Writer, all, Context); send_response_bodyfun({device, _Length, IO}, Code, Parts, Context) -> Writer = fun(FunContext, WParts) -> FunContext1 = send_device_body_parts(FunContext, WParts, IO), _ = file:close(IO), FunContext1 end, start_response_stream(Code, undefined, Writer, Parts, Context); % File send_response_bodyfun({file, Filename}, Code, Parts, Context) -> Length = filelib:file_size(Filename), send_response_bodyfun({file, Length, Filename}, Code, Parts, Context); send_response_bodyfun({file, Length, Filename}, Code, all, Context) -> Writer = fun(FunContext) -> send_file_body(FunContext, Length, Filename, fin) end, start_response_stream(Code, Length, Writer, all, Context); send_response_bodyfun({file, _Length, Filename}, Code, Parts, Context) -> Writer = fun(FunContext, WParts) -> send_file_body_parts(FunContext, WParts, Filename) end, start_response_stream(Code, undefined, Writer, Parts, Context); % Stream functions with continuation send_response_bodyfun({stream, {_, _} = InitialStream}, Code, Parts, Context) -> start_response_stream(Code, undefined, InitialStream, Parts, Context); send_response_bodyfun({stream, Size, {_, _} = InitialStream}, Code, Parts, Context) -> start_response_stream(Code, Size, InitialStream, Parts, Context); send_response_bodyfun({stream, StreamFun}, Code, Parts, Context) when is_function(StreamFun) -> start_response_stream(Code, undefined, {<<>>, StreamFun}, Parts, Context); send_response_bodyfun({stream, Size, StreamFun}, Code, Parts, Context) when is_function(StreamFun) -> start_response_stream(Code, Size, {<<>>, StreamFun}, Parts, Context); % send_response_bodyfun({stream, {_Data, Fun} = Initial, Code, Parts, Context) when is_function(Fun, 1) -> % Writer = fun(FunContext) -> send_stream_body(FunContext, Fun(Parts)) end, % start_response_stream(Code, undefined, Writer, Context); % % Stream without parts - remove parts response header % send_response_bodyfun({stream, Fun}, Code, Parts, Context) when is_function(Fun, 0) -> % Writer = fun(FunContext) -> send_stream_body(FunContext, Fun()) end, % start_response_stream(Code, undefined, Writer, Context); % send_response_bodyfun({stream, Size, Fun}, Code, all, Context) when is_function(Fun, 0) -> % Writer = fun(FunContext) -> send_stream_body(FunContext, Fun()) end, % start_response_stream(Code, undefined, Writer, Context); % % Stream with parts (managed by the streaming function) % send_response_bodyfun({stream, Fun}, Code, Parts, Context) when is_function(Fun, 1) -> % Writer = fun(FunContext) -> send_stream_body(FunContext, Fun(Parts)) end, % start_response_stream(Code, undefined, Writer, Context); % send_response_bodyfun({stream, Size, Fun}, Code, Parts, Context) when is_function(Fun, 1) -> % Writer = fun(FunContext) -> send_stream_body(FunContext, Fun(Parts) end, % start_response_stream(Code, undefined, Writer, Context); % Writer send_response_bodyfun({writer, WriterFun}, Code, all, Context) -> Writer = fun(FunContext) -> send_writer_body(FunContext, WriterFun) end, start_response_stream(Code, undefined, Writer, all, Context); % Data send_response_bodyfun(undefined, Code, Parts, Context) -> send_response_bodyfun(<<>>, Code, Parts, Context); send_response_bodyfun(Body, Code, all, Context) -> Length = iolist_size(Body), Headers = response_headers(Code, Length, Context), Req = cowmachine_req:req(Context), Req1 = cowboy_req:reply(Code, Headers, Body, Req), cowmachine_req:set_req(Req1, Context); send_response_bodyfun(Body, Code, Parts, Context) -> set_idle_timeout(infinity, Context), Headers = response_headers(Context), Req = cowmachine_req:req(Context), Req1 = cowboy_req:stream_reply(Code, Headers, Req), Context1 = cowmachine_req:set_req(Req1, Context), Context2 = send_parts(Context1, Parts, iolist_to_binary(Body)), set_idle_timeout(?DEFAULT_IDLE_TIMEOUT, Context2), Context2. -spec start_response_stream(Code, Length, InitialStream, Parts, Context) -> Result when Code :: integer(), Length :: non_neg_integer() | undefined, InitialStream :: function() | tuple(), Parts :: cowmachine_req:parts(), Context :: cowmachine_req:context(), Result :: cowmachine_req:context(). start_response_stream(Code, Length, InitialStream, Parts, Context) -> set_idle_timeout(infinity, Context), {Code1, Context1, Parts1} = case is_streaming_range(InitialStream) of false when Parts =/= all -> % Drop range response header C1 = cowmachine_req:remove_resp_header(<<"accept-ranges">>, Context), C2 = cowmachine_req:remove_resp_header(<<"content-range">>, C1), C3 = cowmachine_req:remove_resp_header(<<"content-length">>, C2), {200, C3, all}; false -> {Code, Context, all}; true -> {Code, Context, Parts} end, Headers = response_headers(Code1, Length, Context1), Req = cowmachine_req:req(Context1), Req1 = cowboy_req:stream_reply(Code, Headers, Req), Context2 = cowmachine_req:set_req(Req1, Context1), FirstHunk = case InitialStream of {InitialData, InitialFun} -> {InitialData, stream_initial_fun(InitialFun, Parts1)}; InitialFun -> stream_initial_fun(InitialFun, Parts1) end, Context3 = send_stream_body(FirstHunk, Context2), set_idle_timeout(?DEFAULT_IDLE_TIMEOUT, Context3), Context3. -spec stream_initial_fun(Fun, Parts) -> Result when Fun :: function(), Parts :: cowmachine_req:parts(), Result :: done | function(). stream_initial_fun(F, Parts) when is_function(F, 2) -> fun(Ctx) -> F(Ctx, Parts) end; stream_initial_fun(F, _Parts) when is_function(F) -> F; stream_initial_fun(done, _Parts) -> done. %% @doc Set the idle timeout for the connection. This will reset the idle timeout %% timer for HTTP/1.x connections. During streaming we do not want a timeout. %% The default is 60 seconds. -spec set_idle_timeout(Timeout, Context) -> ok when Timeout :: pos_integer() | infinity, Context :: cowmachine_req:context(). set_idle_timeout(Timeout, Context) -> Req = cowmachine_req:req(Context), cowboy_req:cast({set_options, #{ idle_timeout => Timeout }}, Req). %% @doc Check if we support ranges on the data stream (body or function) -spec is_streaming_range(Stream) -> Result when Stream :: Fun | {binary(), Fun} | {any(), Fun} | {any, done}, Fun :: function(), Result :: boolean(). is_streaming_range(Fun) when is_function(Fun, 2) -> true; is_streaming_range(Fun) when is_function(Fun) -> false; is_streaming_range({<<>>, Fun}) when is_function(Fun, 2) -> % Only support ranges if the initial binary is a range, until we % add functionality to stream the data from the binary in accordance % with the requested ranges (and update the range given to the range function) true; is_streaming_range({_, Fun}) when is_function(Fun) -> false; is_streaming_range({_, done}) -> false. %% @todo Add the cookies! -spec response_headers(Code, Length, Context) -> Result when Code :: integer(), Length :: non_neg_integer() | undefined, Context :: cowmachine_req:context(), Result :: map(). response_headers(Code, Length, Context) -> Hdrs = cowmachine_req:get_resp_headers(Context), Hdrs1 = case Code of 304 -> Hdrs; _ when is_integer(Length) -> Hdrs#{<<"content-length">> => integer_to_binary(Length)}; _ -> Hdrs end, Hdrs1#{ <<"server">> => server_header(), <<"date">> => cowboy_clock:rfc1123() }. -spec response_headers(Context) -> Result when Context :: cowmachine_req:context(), Result :: map(). response_headers(Context) -> Hdrs = cowmachine_req:get_resp_headers(Context), Hdrs#{ <<"server">> => server_header(), <<"date">> => cowboy_clock:rfc1123() }. % With continuation %% @doc Send stream body. -spec send_stream_body(FunContext, Context) -> Result when FunContext :: {InitialData, InitialFun} | InitialFun, InitialData :: binary() | {file, Filename} | {file, Size, Filename} | done | WriterFun, Filename :: file:filename_all(), WriterFun :: function(), Size :: non_neg_integer(), InitialFun :: function(), Context :: cowmachine_req:context(), Result :: cowmachine_req:context(). send_stream_body({<<>>, done}, Context) -> % @TODO: in cowboy this give a wrong termination with two 0 size chunks send_chunk(Context, <<>>, fin); send_stream_body({<<>>, Next}, Context) -> next(Next, Context); send_stream_body({Data, Next}, Context) when is_binary(Data); is_list(Data) -> send_chunk(Context, Data, fin(Next)), next(Next, Context); send_stream_body({{file, Filename}, Next}, Context) -> Length = filelib:file_size(Filename), send_stream_body({{file, Length, Filename}, Next}, Context); send_stream_body({{file, 0, _Filename}, Next}, Context) -> send_stream_body({<<>>, Next}, Context); send_stream_body({{file, Size, Filename}, Next}, Context) -> Context1 = send_file_body(Context, Size, Filename, fin(Next)), next(Next, Context1); % Without continuation send_stream_body(done, Context) -> send_chunk(Context, <<>>, fin); send_stream_body(WriterFun, Context) when is_function(WriterFun, 1) -> WriterFun(Context); send_stream_body(WriterFun, Context) when is_function(WriterFun, 0) -> _ = WriterFun(), Context. -spec next(Fun, Context) -> Result when Fun :: fun((any()) -> any()), Context :: cowmachine_req:context(), Result :: Context. next(Fun, Context) when is_function(Fun, 1) -> send_stream_body(Fun(Context), Context); next(Fun, Context) when is_function(Fun, 0) -> send_stream_body(Fun(), Context); next(done, Context) -> Context. -spec fin(Next) -> Result when Next :: done | any(), Result :: fin | nofin. fin(done) -> fin; fin(_) -> nofin. %%@equiv send_file_body_loop(Context, 0, Length, IO, fin) -spec send_device_body(Context, Length, IO) -> Result when Context :: cowmachine_req:context(), Length :: non_neg_integer(), IO :: file:io_device(), Result :: cowmachine_req:context(). send_device_body(Context, Length, IO) -> send_file_body_loop(Context, 0, Length, IO, fin). -spec send_file_body(Context, Length, File, FinNoFin) -> Result when Context :: cowmachine_req:context(), Length :: non_neg_integer(), File :: Filename | iodata(), Filename :: file:name_all(), FinNoFin :: fin | nofin, Result :: cowmachine_req:context(). send_file_body(Context, Length, Filename, FinNoFin) -> {ok, FD} = file:open(Filename, [read,raw,binary]), try send_file_body_loop(Context, 0, Length, FD, FinNoFin) after file:close(FD) end. -spec send_device_body_parts(Context, Parts, IO) -> Result when Context :: cowmachine_req:context(), Parts :: cowmachine_req:parts(), IO :: file:io_device(), Result :: cowmachine_req:context(). send_device_body_parts(Context, {[{From,Length}], _Size, _Boundary, _ContentType}, IO) -> {ok, _} = file:position(IO, From), send_file_body_loop(Context, 0, Length, IO, fin); send_device_body_parts(Context, {Parts, Size, Boundary, ContentType}, IO) -> lists:foreach( fun({From,Length}) -> {ok, _} = file:position(IO, From), send_chunk(Context, part_preamble(Boundary, ContentType, From, Length, Size), nofin), send_file_body_loop(Context, 0, Length, IO, nofin), send_chunk(Context, <<"\r\n">>, nofin) end, Parts), send_chunk(Context, end_boundary(Boundary), fin). -spec send_file_body_parts(Context, Parts, Filename) -> Result when Context :: cowmachine_req:context(), Parts :: cowmachine_req:parts(), Filename :: file:filename_all(), Result :: ok | {error, term()}. send_file_body_parts(Context, Parts, Filename) -> {ok, FD} = file:open(Filename, [raw,binary]), try send_device_body_parts(Context, Parts, FD) after file:close(FD) end. -spec send_parts(Context, Parts, Bin) -> Result when Context :: cowmachine_req:context(), Parts :: cowmachine_req:parts(), Bin :: binary(), Result :: cowmachine_req:context(). send_parts(Context, {[{From,Length}], _Size, _Boundary, _ContentType}, Bin) -> send_chunk(Context, binary:part(Bin,From,Length), fin); send_parts(Context, {Parts, Size, Boundary, ContentType}, Bin) -> lists:foreach( fun({From,Length}) -> Part = [ part_preamble(Boundary, ContentType, From, Length, Size), binary:part(Bin,From,Length), <<"\r\n">> ], send_chunk(Context, Part, nofin) end, Parts), send_chunk(Context, end_boundary(Boundary), fin). -spec send_file_body_loop(Context, Offset, Size, Device, FinNoFin) -> Result when Context :: cowmachine_req:context(), Offset :: integer(), Size :: non_neg_integer(), Device :: file:io_device(), FinNoFin :: fin | nofin, Result :: cowmachine_req:context(). send_file_body_loop(Context, Offset, Size, _Device, FinNoFin) when Offset =:= Size -> send_chunk(Context, <<>>, FinNoFin); send_file_body_loop(Context, Offset, Size, Device, FinNoFin) when Size - Offset =< ?FILE_CHUNK_LENGTH -> {ok, Data} = file:read(Device, Size - Offset), send_chunk(Context, Data, FinNoFin); send_file_body_loop(Context, Offset, Size, Device, FinNoFin) -> {ok, Data} = file:read(Device, ?FILE_CHUNK_LENGTH), send_chunk(Context, Data, nofin), send_file_body_loop(Context, Offset+iolist_size(Data), Size, Device, FinNoFin). -spec send_writer_body(Context, BodyFun) -> Result when Context :: cowmachine_req:context(), BodyFun :: function(), Result :: any(). send_writer_body(Context, BodyFun) -> BodyFun(fun(Data, false, ContextW) -> send_chunk(ContextW, Data, nofin); (Data, true, ContextW) -> send_chunk(ContextW, Data, fin) end, Context). -spec send_chunk(Context, Data, IsFin) -> Result when Context :: cowmachine_req:context(), Data :: iolist() | binary(), IsFin :: fin | nofin, Result :: Context. send_chunk(Context, <<>>, nofin) -> Context; send_chunk(Context, [], nofin) -> Context; send_chunk(Context, Data, FinNoFin) -> Data1 = iolist_to_binary(Data), Req = cowmachine_req:req(Context), ok = cowboy_req:stream_body(Data1, FinNoFin, Req), Context. -spec get_range(Context) -> Result when Context :: cowmachine_req:context(), Result :: {Range, cowmachine_req:context()}, Range :: undefined | [ {integer()|none, integer()|none} ]. get_range(Context) -> Env = cowmachine_req:env(Context), Range = case maps:get(cowmachine_range_ok, Env) of false -> undefined; true -> Req = cowmachine_req:req(Context), case cowboy_req:header(<<"range">>, Req) of undefined -> undefined; RawRange -> parse_range_request(RawRange) end end, Env1 = Env#{ cowmachine_range => Range }, {Range, cowmachine_req:set_env(Env1, Context)}. -spec parse_range_request(RangeRequest) -> Result when RangeRequest :: undefined | binary(), Result :: undefined | [{integer()|none,integer()|none}]. parse_range_request(<<"bytes=", RangeString/binary>>) -> try Ranges = binary:split(binary:replace(RangeString, <<" ">>, <<>>, [global]), <<",">>, [global]), lists:map( fun (<<"-", V/binary>>) -> {none, binary_to_integer(V)}; (R) -> case binary:split(R, <<"-">>) of [S1, <<>>] -> {binary_to_integer(S1), none}; [<<>>, S2] -> {none, binary_to_integer(S2)}; [S1, S2] -> {binary_to_integer(S1), binary_to_integer(S2)}; [S1] -> {binary_to_integer(S1), none} end end, Ranges) catch _:_ -> % Invalid range header, ignore silently undefined end; parse_range_request(_) -> undefined. % Map request ranges to byte ranges, taking the total body length into account. -spec range_parts(Ranges, Size) -> Result when Ranges :: [{integer() | none, integer() | none}], Size :: integer(), Result :: [{integer(),integer()}]. range_parts(Ranges, Size) -> Ranges1 = [ range_skip_length(Spec, Size) || Spec <- Ranges ], [ R || R <- Ranges1, R =/= invalid_range ]. -spec range_skip_length(Spec, Size) -> Result when Spec :: {integer() | none, integer() | none}, Size :: integer(), Result :: invalid_range | {integer(),integer()}. range_skip_length({none, R}, Size) when R =< Size, R >= 0 -> {Size - R, R}; range_skip_length({none, _OutOfRange}, Size) -> {0, Size}; range_skip_length({R, none}, Size) when R >= 0, R < Size -> {R, Size - R}; range_skip_length({_OutOfRange, none}, _Size) -> invalid_range; range_skip_length({Start, End}, Size) when 0 =< Start, Start =< End, End < Size -> {Start, End - Start + 1}; range_skip_length({_OutOfRange, _End}, _Size) -> invalid_range. -spec get_resp_body_size(RespBody) -> Result when RespBody :: cowmachine_req:resp_body(), Result :: {ok, integer(), cowmachine_req:resp_body()} | {error, nosize}. get_resp_body_size({device, Size, _} = Body) -> {ok, Size, Body}; get_resp_body_size({device, IO}) -> Length = iodevice_size(IO), {ok, Length, {device, Length, IO}}; get_resp_body_size({file, Size, _} = Body) -> {ok, Size, Body}; get_resp_body_size({file, Filename}) -> Length = filelib:file_size(Filename), {ok, Length, {file, Length, Filename}}; get_resp_body_size(B) when is_binary(B) -> {ok, size(B), B}; get_resp_body_size(L) when is_list(L) -> B = iolist_to_binary(L), {ok, size(B), B}; get_resp_body_size(_) -> {error, nosize}. -spec make_range_headers(Parts, Size, ContentType) -> Result when Parts :: list(), Size :: non_neg_integer(), ContentType :: binary() | undefined, Result :: {list(), none} | {list(), binary()}. make_range_headers([{Start, Length}], Size, _ContentType) -> HeaderList = [{<<"accept-ranges">>, <<"bytes">>}, {<<"content-range">>, iolist_to_binary([ "bytes ", make_io(Start), "-", make_io(Start+Length-1), "/", make_io(Size)])}, {<<"content-length">>, integer_to_binary(Length)}], {HeaderList, none}; make_range_headers(Parts, Size, ContentType) when is_list(Parts) -> Boundary = boundary(), Lengths = [ iolist_size(part_preamble(Boundary, ContentType, Start, Length, Size)) + Length + 2 || {Start,Length} <- Parts ], TotalLength = lists:sum(Lengths) + iolist_size(end_boundary(Boundary)), HeaderList = [{<<"accept-ranges">>, <<"bytes">>}, {<<"content-type">>, <<"multipart/byteranges; boundary=", Boundary/binary>>}, {<<"content-length">>, integer_to_binary(TotalLength)}], {HeaderList, Boundary}. -spec part_preamble(Boundary, CType, Start, Length, Size) -> Result when Boundary :: binary(), CType :: binary(), Start :: non_neg_integer(), Length :: non_neg_integer(), Size :: non_neg_integer(), Result :: [binary()]. part_preamble(Boundary, CType, Start, Length, Size) -> [boundary(Boundary), <<"content-type: ">>, CType, <<"\r\ncontent-range: bytes ">>, integer_to_binary(Start), <<"-">>, integer_to_binary(Start+Length-1), <<"/">>, integer_to_binary(Size), <<"\r\n\r\n">>]. -spec boundary() -> Result when Result :: binary(). boundary() -> A = rand:uniform(100000000), B = rand:uniform(100000000), <<(integer_to_binary(A))/binary, $_, (integer_to_binary(B))/binary>>. -spec boundary(B) -> Result when B :: binary(), Result :: binary(). boundary(B) -> <<"--", B/binary, "\r\n">>. -spec end_boundary(B) -> Result when B :: binary(), Result :: binary(). end_boundary(B) -> <<"--", B/binary, "--\r\n">>. -spec make_io(Integer) -> Result when Integer :: integer(), Result :: list(). make_io(Integer) when is_integer(Integer) -> integer_to_list(Integer). % make_io(Atom) when is_atom(Atom) -> % atom_to_list(Atom); % make_io(Io) when is_list(Io); is_binary(Io) -> % Io. -spec iodevice_size(IoDevice) -> Result when IoDevice :: file:io_device(), Size :: non_neg_integer(), Result :: Size. iodevice_size(IoDevice) -> {ok, Size} = file:position(IoDevice, eof), {ok, 0} = file:position(IoDevice, bof), Size.