classdef HelperPCAPWriter < handle & HelperPCAPUtils
%HelperPCAPWriter Create a PCAP file writer object which supports writing
%protocol packets in PCAP format
%
%   PCAPWRITER = HelperPCAPWriter(LINKTYPE) creates a packet capture (PCAP)
%   file writer handle object, PCAPWRITER, that writes protocol packets
%   into a PCAP format file.
%
%   LINKTYPE         - Unique identifier for a protocol
%                      For example: Linktype of WLAN protocol is 127
%                                   Linktype of BLE protocol is 251
%
%   PCAPWRITER = HelperPCAPWriter(LINKTYPE, Name1, Value1, ..., NameN,
%   ValueN) specifies additional name-value pair arguments described below.
%   When a name-value pair is not specified, its default value is used.
%
%   FileName        - Name of the file. It is specified as a character
%                     vector or a string. The default file name is
%                     'capture'.
%
%   ByteOrder       - Specifies the sequential order in which bytes
%                     are arranged into larger numerical values. It can be
%                     one of the 'little-endian' |'big-endian'. The default
%                     value is 'little-endian'.
%
%   HelperPCAPWriter methods:
%
%   write                           - Write packet data along with PCAP
%                                     packet header to file
%   writeGlobalHeader               - Write global header into PCAP format
%                                     file
%
%   % Example 1:
%   %  Write WLAN packets into a PCAP format file
%
%   % Create a PCAP object with file name as sample.pcap
%   wlanLinkType = 105;
%   pcapObj = HelperPCAPWriter('FileName', 'sample');
%
%   % The methods MUST be called in this sequence:
%   %	1.writeGlobalHeader
%   %	2.write
%
%   % Write global header
%   writeGlobalHeader(pcapObj, wlanLinkType);
%
%   % Create a WLAN MAC packet of type QoS Data
%   macConfig = wlanMACFrameConfig('FrameType', 'QoS Data');
%   payload = '00576000103afffe80';
%   mpdu = wlanMACFrame(payload, macConfig);
%
%   % Write MAC protocol data unit (MPDU)
%   timestamp = 124800;
%   write(pcapObj, timestamp, mpdu);
%
%
%   % Example 2:
%   %  Write Blutooth low energy (BLE) packets into a PCAP format file
%
%   % Create a PCAP object with file name as sample.pcap
%   bleLinkType = 251;
%   pcapObj = HelperPCAPWriter('FileName', 'sample');
%
%   % The methods MUST be called in this sequence:
%   %	1.writeGlobalHeader
%   %	2.write
%
%   % Write global header
%   writeGlobalHeader(pcapObj, bleLinkType);
%
%   % Create a BLE link layer packet
%   llpacket = '42BC13E206120E00050014010A001F0040001700170000007D47C0';
%
%   % Write llpacket
%   timestamp = 129100;
%   write(pcapObj, timestamp, llpacket);
%
%   Reference - https://www.tcpdump.org/linktypes.html

% Copyright 2019 The MathWorks, Inc.

%#codegen

    properties (Access = private)
        %FID File identifier
        %   Scalar MATLAB integer-valued double, called a file identifier
        FID = -1

        %IsLittleEndian Byte order flag
        %   Set this property to true to indicate that the byte order is
        %   'little-endian'. The default value is true.
        IsLittleEndian (1, 1) logical = true
    end

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

    properties (GetAccess = public, SetAccess = private)
        %FileName Name of the file
        %   It is specified as a character vector or a string. The default
        %   file name is the value of the timestamp.
        FileName = 'capture'

        %ByteOrder Order of bytes
        %   Specifies the sequential order in which bytes are arranged into
        %   larger numerical values
        ByteOrder = 'little-endian'
    end

    methods (Access = private)
        function globalHeaderBytes = createGlobalHeader(obj, linkType)
            %createGlobalHeader Create global header and return it as a
            %byte array

            % PCAP file format and byte order
            magicNumber = obj.formByteArray([161; 178; 195; 212]);
            % Major & minor version numbers to represent version 2.4
            versionMajor = obj.formByteArray([0; 2]);
            versionMinor = obj.formByteArray([0; 4]);
            % GMT to local correction
            timezone = [0; 0; 0; 0];
            % Accuracy of timestamps
            sigfigs = obj.formByteArray([0; 0; 255; 255]);
            % Maximum number of octets captured for each packet
            snapLen = obj.formByteArray([255; 255; 0; 0]);
            % Data link type (protocol number)
            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
            %
            % FILENAME  - Name of the file

            % Open and clear the existing content in the file
            obj.FID = fopen(fileName, 'w');

            % Check if file ID is negative
            if obj.FID < 0
                error('Unable to open file %s because value of its file ID is less than zero.', fileName);
            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 = HelperPCAPWriter(varargin)
            %HelperPCAPWriter Create a PCAP packet writer configuration
            %object.

            % Name-value pair check
            if mod(nargin,2)
                error('Incorrect number of input arguments. Number of input arguments must be even.');
            end

            % Apply name-value pairs
            for i = 1:2:length(varargin)
                if strcmp('FileName', varargin{i}) || strcmp('ByteOrder', varargin{i})
                    obj.(varargin{i}) = varargin{i+1};
                end
            end

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

        function set.FileName(obj, value)
            validateattributes(value, {'char' ,'string'}, {'row', 'nonempty'}, ...
                    mfilename, 'FileName');
            obj.FileName = value;
        end

        function set.ByteOrder(obj, value)
            validatestring(value, {'little-endian', 'big-endian'}, ...
                    mfilename, 'ByteOrder');
            obj.ByteOrder = value;
        end

        function writeGlobalHeader(obj, linkType)
            %writeGlobalHeader Write global header into a PCAP file
            %   writeGlobalHeader(OBJ, LINKTYPE) writes global header into
            %   a PCAP format file
            %
            %   LINKTYPE      - Protocol number

            if obj.IsHeader
                error('Unable to write multiple global headers in PCAP file. PCAP File must contain only 1 header.');
            end

            % Form file name
            fileName = [char(obj.FileName), '.',  'pcap'];
            % Open file for writing packets
            openFile(obj, fileName);
            % Create and write the global header into the PCAP file
            globalHeaderBytes = obj.createGlobalHeader(linkType);
            fwrite(obj.FID, globalHeaderBytes);
            % Set the header flag
            obj.IsHeader = true;
        end

        function write(obj, timestamp, packet)
            %write Write packet into PCAP format file
            %
            %   write(OBJ, TIMESTAMP, PACKET) writes packets into PCAP
            %   format file
            %
            %   PACKET      - Packet is specified as one of the following
            %                 types:
            %                   - 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 of [0 - 255] inclusive,
            %                     representing octets in decimal format.
            %                   - An n-by-2 character array, where each row
            %                     represents an octet in hexadecimal
            %                     format.
            %   TIMESTAMP   - Packet arrival time in microseconds. It is
            %                 specified as a double value. The default
            %                 value is zero.

            % Validate packet and return octets in decimal format
            packetData = obj.validatePayloadFormat(packet);

            if obj.FID < 0
                error('Unable to write packet. To write a packet in PCAP file, add global header.');
            end

            % Maximum length of a packet in the PCAP file (65535 octets)
            maxPacketLen = 65535;
            packetLen = length(packet);
            % Calculate inclusion length
            inclLen = min(packetLen, maxPacketLen);
            inclLenBytes = obj.getByteArray(inclLen, 4);
            lenBytes = obj.getByteArray(packetLen, 4);

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

            % Write packet header into PCAP file
            fwrite(obj.FID, [tsSec; tsUsec; inclLenBytes; lenBytes]);
            % Write packet data into PCAP file
            fwrite(obj.FID, packetData(1:inclLen, 1));
        end
    end
end
