classdef pcapWriter < handle & comm.internal.ConfigBase & comm_sysmod.internal.pcapCommon
%pcapWriter Create PCAP file writer object
%
%   OBJ = pcapWriter creates a packet capture (PCAP) file writer
%   object, OBJ, that writes protocol packets into a file with the .pcap
%   extension.
%
%   OBJ = pcapWriter(Name, Value) creates a PCAP file writer object,
%   OBJ, with properties specified by one or more name-value pairs. You can
%   specify additional name-value pair arguments in any order as
%   (Name1,Value1,...,NameN,ValueN). When you do not specify a property name
%   and value, the object uses the default value.
%
%   pcapWriter methods:
%   writeGlobalHeader   - Write global header into a file with the .pcap
%                         extension
%   write               - Write packet contents into a file with the .pcap
%                         extension
%
%   The method <a href="matlab:help('pcapWriter.writeGlobalHeader')">writeGlobalHeader</a> must be called for each link type before
%   calling the <a href="matlab:help('pcapWriter.write')">write</a> method.
%
%   pcapWriter Name-Value pairs:
%   'FileName'         - File name specified as a character row
%                        vector or string. The default file name is
%                        'capture'.
%   'ByteOrder'        - Byte order, specified as 'little-endian' or
%                        'big-endian'. The default value is 'little-endian'.
%
%   % Example 1:
%   %  Write WLAN packet to PCAP format file
%
%   % Create a PCAP writer object with file name 'wlanCapture.pcap'
%   wlanLinkType = 105;
%   pcapObj = pcapWriter('FileName', 'wlanCapture');
%
%   % Create a WLAN MAC packet
%   mpdu = 'B4000000FFFFFFFFFFFF00123456789BA79A5B28';
%
%   % Write global header
%   writeGlobalHeader(pcapObj, wlanLinkType);
%
%   % Write MPDU to PCAP format file
%   timestamp = 124800; % Number of microseconds
%   write(pcapObj, mpdu, timestamp);
%
%   % Example 2:
%   %  Write Bluetooth low energy (BLE) link layer packet to PCAP format
%   %  file
%
%   % Create a PCAP object with file name 'bleCapture.pcap'
%   bleLinkType = 251;
%   pcapObj = pcapWriter('FileName', 'bleCapture');
%
%   % BLE link layer packet
%   llpacket = '42BC13E206120E00050014010A001F0040001700170000007D47C0';
%
%   % Write global header
%   writeGlobalHeader(pcapObj, bleLinkType);
%
%   % Write BLE link layer packet to PCAP format file
%   timestamp = 129100; % Number of microseconds
%   write(pcapObj, llpacket, timestamp);
%
%   See also pcapngWriter

% Copyright 2020-2024 The MathWorks, Inc.

%#codegen

    properties (GetAccess = public, SetAccess = private)
        %FileName File name
        %   Specify file name as a character row vector or
        %   string. The default file name is 'capture'.
        FileName = 'capture'

        %ByteOrder Byte order
        %   Specify the byte order as 'little-endian' or
        %   'big-endian'. The default value is 'little-endian'.
        ByteOrder = 'little-endian'
    end

    properties (Hidden)
        %DisableValidation Disable the validation for input arguments of
        %write method
        %  Specify this property as a logical scalar. When true, validation
        %  is not performed on the input arguments and the packet is
        %  expected to be octets in decimal format.
        DisableValidation (1, 1) logical = false
    end

    properties (GetAccess = public, SetAccess = private, Hidden)
        %GlobalHeaderPresent Header flag
        %   Set this property to true to indicate that the global header is
        %   present. The default value is false.
        GlobalHeaderPresent (1, 1) logical = false
    end

    properties (Access = private)
        %FID File identifier
        FID = -1

        %SnapLength Snap length
        %   SnapLength indicates limit on the number of bytes for each
        %   packet shown in the capture file. The value zero indicates no
        %   limit for maximum number of octets captured.
        SnapLength = 0
    end

    properties (Hidden, Constant)
        ByteOrder_Values = {'little-endian', 'big-endian'}
    end

    methods (Access = private)
        function setFileName(obj, value)
            validateattributes(value, {'char' ,'string'}, {'scalartext', 'nonempty'}, ...
                mfilename, 'FileName');

            coder.internal.errorIf(any(ismember(char(value), '/\*:?"<>|"%')),...
                'shared_comm_sysmod:pcapWriter:SpecialCharactersNotAccepted', ...
                '\ / : * ? " < > |');

            obj.FileName = char(value);
        end

        function setByteOrder(obj, value)
            propName = 'ByteOrder';
            value = validateEnumProperties(obj, propName, value);
            obj.(propName) = '';
            obj.(propName) = value;
        end

        function globalHeaderBytes = createGlobalHeader(obj, linkType)
            %createGlobalHeader Create a PCAP global header

            % Magic number indicates the Endianness (4 bytes)
            magicNumber = obj.updateEndianness([212; 195; 178; 161]);

            % PCAP file format major & minor version (2.4) numbers
            versionMajor = obj.updateEndianness([2; 0]); %2 bytes
            versionMinor = obj.updateEndianness([4; 0]); %2 bytes

            % GMT, set as zero (4 bytes)
            timezone = [0; 0; 0; 0];

            % Accuracy of timestamps, set as zero (4 bytes)
            sigfigs = [0; 0; 0; 0];

            % Maximum number of octets captured for each packet (4 bytes)
            snapLen = obj.getByteArray(obj.SnapLength, 4);

            % Data link type / protocol number  (4 bytes)
            network = obj.getByteArray(linkType, 4);

            % Global header block
            globalHeaderBytes = [magicNumber; ...
                                 versionMajor; ...
                                 versionMinor; ...
                                 timezone; ...
                                 sigfigs; ...
                                 snapLen; ...
                                 network];
        end

        function openFile(obj, fileName)
            %openFile Open a file

            if isempty(coder.target)
                % Open the file using UTF-8 encoding (fopen default
                % encoding). Due to the limitations related to code
                % generation, encoding is not specified explicitly.
                [obj.FID, errMsg] = fopen(fileName, 'w');
                coder.internal.errorIf(obj.FID == -1, ...
                    'shared_comm_sysmod:pcapWriter:FileOpenError', errMsg);
            else
                % Open the file using UTF-8 encoding (fopen default
                % encoding). Due to the limitations related to code
                % generation, encoding is not specified explicitly.
                obj.FID = fopen(fileName, 'w');
                coder.internal.errorIf(obj.FID == -1, ...
                    'shared_comm_sysmod:pcapWriter:FileOpenErrorInCodegen');
            end
        end

        function closeFile(obj)
            %closeFile Close the file

            if obj.FID > 0
                fclose(obj.FID);
                obj.FID = -1;
            end
        end

        function delete(obj)
            %delete Function acts as a destructor

            obj.closeFile();
        end
    end

    methods
        function obj = pcapWriter(varargin)
            % Name-value pair check
            coder.internal.errorIf(mod(nargin,2) == 1, 'MATLAB:system:invalidPVPairs');

            obj@comm.internal.ConfigBase();

            if isempty(coder.target) % Simulation path
                % Apply name-value pairs
                for i = 1:2:nargin
                    if ~obj.DisableValidation
                    value = validatestring(varargin{i}, {'FileName', ...
                        'ByteOrder'}, mfilename);
                    end
                    switch(value)
                        case 'FileName'
                            setFileName(obj, varargin{i+1});
                        otherwise % ByteOrder
                            setByteOrder(obj, varargin{i+1});
                    end
                end
            else %Codegen path
                nvPairs = struct('FileName', uint32(0), 'ByteOrder', uint32(0));

                % Select parsing options
                popts = struct('PartialMatching', true,...
                'CaseSensitivity', false);

                % Parse inputs
                pStruct = coder.internal.parseParameterInputs(nvPairs, ...
                    popts, varargin{:});

                % Get values for the N-V pair or set defaults for the
                % optional arguments
                setByteOrder(obj, coder.internal.getParameterValue( ...
                    pStruct.ByteOrder, coder.const('little-endian'), ...
                    varargin{:}));

                setFileName(obj, coder.internal.getParameterValue( ...
                    pStruct.FileName, 'capture', varargin{:}));
            end

            % To check whether file with the same name already exist or
            % not. If the file already exists, it prompts the user to
            % change the filename.
            fileName = [char(obj.FileName), '.',  'pcap'];
            coder.internal.errorIf(isfile(fileName), ...
                'shared_comm_sysmod:pcapWriter:UnableToCreateNewFile', obj.FileName);

            obj.ByteOrder = char(obj.ByteOrder);
            if strcmp(obj.ByteOrder, 'big-endian')
                obj.IsLittleEndian = false;
            end
        end

        function writeGlobalHeader(obj, linkType)
            %writeGlobalHeader Write global header into a file with the
            %.pcap extension
            %   writeGlobalHeader(OBJ, LINKTYPE) writes global header into
            %   a file with the .pcap extension
            %
            %   LINKTYPE indicates a unique identifier for a protocol,
            %   specified as a scalar integer value in the range [0, 65535].

            narginchk(2, 2);

            validateattributes(linkType, {'numeric'}, {'scalar', 'integer', ...
                'nonnegative', '<=', 65535}, mfilename, 'linkType');

            linkType = double(linkType);

            coder.internal.errorIf(obj.GlobalHeaderPresent == true, ...
                'shared_comm_sysmod:pcapWriter:MultipleHeadersNotAccepted');

            % Name of the file
            fileName = [char(obj.FileName), '.', 'pcap'];

            % Open file
            openFile(obj, fileName);

            % Create global header
            globalHeaderBytes = obj.createGlobalHeader(linkType);

            % Write global header to PCAP format file
            fwrite(obj.FID, globalHeaderBytes);

            % Set the header flag
            obj.GlobalHeaderPresent = true;
        end

        function write(obj, packet, timestamp, varargin)
            %write Write packet contents into a file with the .pcap
            %extension
            %
            %   write(OBJ, PACKET, TIMESTAMP) writes packet contents into a
            %   file with the .pcap extension
            %
            %   PACKET is specified as one of these types:
            %    - A binary vector representing bits
            %    - A character vector representing octets in hexadecimal
            %      format
            %    - A string scalar representing octets in hexadecimal
            %      format
            %    - A numeric vector, where each element is in the range
            %      [0, 255], representing octets in decimal format
            %    - An n-by-2 character array, where each row represents
            %      an octet in hexadecimal format
            %
            %   TIMESTAMP is specified as a scalar integer greater than or
            %   equal to 0. Timestamp is the packet arrival time in
            %   microseconds since 1/1/1970.
            %
            %   write(OBJ, PACKET, TIMESTAMP, Name, Value) specifies
            %   additional name-value pair arguments described below. When
            %   a name-value pair is not specified, its default value is
            %   used.
            %
            %   'PacketFormat'    Format of the packet, specified as
            %                     'bits' or 'octets'. The default value is
            %                     'octets'. If it is specified as 'octets',
            %                     the packet can be a numeric vector
            %                     representing octets in decimal format or
            %                     alternatively, it can be a character
            %                     array or string scalar representing
            %                     octets in hexadecimal format. Otherwise,
            %                     Packet is a binary vector.

            narginchk(3, 5);

            % Check whether the validation is enabled
            if ~obj.DisableValidation
                % Name-value pair check
                coder.internal.errorIf(mod(nargin-3,2) == true, ...
                    'MATLAB:system:invalidPVPairs');
            end

            if isempty(coder.target) % Simulation path
                % Initialize
                packetFormat = 'octets';

                % Apply name-value pairs
                for i = 1:2:nargin-3
                    value = validatestring(varargin{i}, {'PacketFormat'}, ...
                        mfilename);
                    if strcmpi(value, 'PacketFormat')
                        packetFormat = varargin{i+1};
                    end
                end
            else %Codegen path
                nvPairs = struct('PacketFormat', uint32(0));

                % Select parsing options
                popts = struct('PartialMatching', true,...
                    'CaseSensitivity',false);

                % Parse inputs
                pStruct = coder.internal.parseParameterInputs( ...
                    nvPairs,popts,varargin{:});

                % Get values for the N-V pair or set defaults for the optional arguments
                packetFormat = coder.internal.getParameterValue( ...
                    pStruct.PacketFormat, coder.const('octets'), ...
                    varargin{:});
            end

            % Check whether the validation is enabled
            if ~obj.DisableValidation
                packetFormat = validatestring(packetFormat, {'bits', 'octets'}, ...
                    mfilename, 'PacketFormat');

                validateattributes(timestamp, {'numeric'}, {'scalar', 'integer', ...
                    'nonnegative'}, mfilename, 'timestamp')

                if ~obj.GlobalHeaderPresent
                    coder.internal.errorIf(~obj.GlobalHeaderPresent == true, ...
                    'shared_comm_sysmod:pcapWriter:GlobalHeaderRequired');
                end

                % Validate packet and return octets in decimal format
                packetData = obj.validatePayloadFormat(packet, packetFormat);
            else
                packetData = double(packet);
            end

            % Packet length (4 bytes)
            packetLen = length(packetData);
            packetLenBytes = obj.getByteArray(packetLen, 4);

            % Timestamp in seconds (4 bytes)
            tsSec = obj.getByteArray(fix(timestamp/1000000), 4);
            % Timestamp in microseconds (4 bytes)
            tsUsec = obj.getByteArray(fix(rem(timestamp, 1000000)), 4);

             % Write packet to PCAP format file
            fwrite(obj.FID, [tsSec; tsUsec; packetLenBytes; packetLenBytes;...
                  packetData]);
        end
    end
end