Using Ada.Sequential_IO to create simple hexdump utility


There is every now and then need for viewing the file contents as "raw" bytes shown as hex numbers. Usually, operating systems have tool 'hexdump' to do this. But it is not hard to create one from scratch.

Here is one example how to create a simple hexdump utility in Ada.

Example output of hexdump:

 0: 77 69 74 68 20 41 64 61 2E 54
10: 65 78 74 5F 49 4F 3B 0A 77 69
20: 74 68 20 41 64 61 2E 53 65 71
30: 75 65 6E 74 69 61 6C 5F 49 4F
40: 3B 0A 77 69 74 68 20 41 64 61

The utility needs to read a file one byte at time. Ada offers multiple different IO packages which can be used for that purpose.

In this case, we will use Ada.Sequential_IO, which reads files sequentially one element at time, as the name says.

The element can be, for example, Unsigned_8 - meaning that you read the file one byte at time.

package IO is new Ada.Sequential_IO (Interfaces.Unsigned_8);

The reading happens via Read procedure call, which takes File and Item parameters

My_File : IO.File_Type;
Data    : Interfaces.Unsigned_8;

-- ...

Read (File => My_File, Item => Data);

You can output the 'Data' variable as-is, but in some cases it can be non-printable character (like 0), so it is better to print all characters as hex numbers.

Ada provides some builtin ways for this, but to get a certain formatting, it is better to do the conversion with a custom function

function Hex (Byte : Interfaces.Unsigned_8) return String is
   Hex_Chars : constant array (Interfaces.Unsigned_8 range 0 .. 15)
     of Character := ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                      'A', 'B', 'C', 'D', 'E', 'F');
   Half_Byte_1 : constant Interfaces.Unsigned_8 := Byte mod 16;
   Half_Byte_2 : constant Interfaces.Unsigned_8 := Byte / 16;
begin
   return Hex_Chars (Half_Byte_2) & Hex_Chars (Half_Byte_1);
end Hex;

Now we have all the basic pieces done, the rest of the program is mostly formatting and error checking:

--
-- Copyright (c) 2016 Tero Koskinen <tero.koskinen@iki.fi>
--
-- Permission to use, copy, modify, and distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--

with Ada.Text_IO;
with Ada.Sequential_IO;
with Ada.Command_Line;
with Ada.IO_Exceptions;
with Interfaces;

use Ada;

procedure Hexdump is
   use type Interfaces.Unsigned_32;
   use type Interfaces.Unsigned_8;

   -- Read file sequentially as Unsigned_8 type elements.
   package IO is new Ada.Sequential_IO (Interfaces.Unsigned_8);

   -- For printing the offset
   package Long_Int_IO is new Ada.Text_IO.Modular_IO (Interfaces.Unsigned_32);

   -- Our custom hex number formatting function
   function Hex (Byte : Interfaces.Unsigned_8) return String is
     Hex_Chars : constant array (Interfaces.Unsigned_8 range 0 .. 15)
       of Character := ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                        'A', 'B', 'C', 'D', 'E', 'F');
     Half_Byte_1 : constant Interfaces.Unsigned_8 := Byte mod 16;
     Half_Byte_2 : constant Interfaces.Unsigned_8 := Byte / 16;
   begin
     return Hex_Chars (Half_Byte_2) & Hex_Chars (Half_Byte_1);
   end Hex;

   My_File      : IO.File_Type;
   Place        : Interfaces.Unsigned_32 := 0;
   Screen_Width : constant := 10;
   Data         : Interfaces.Unsigned_8;
begin
   if Command_Line.Argument_Count < 1 then
     Text_IO.Put_Line("syntax: hexdump <file>");
     return;
   end if;

   begin
     IO.Open
        (File => My_File,
         Mode => IO.In_File,
         Name => Command_Line.Argument (1));
   exception
     -- GNAT uses Name_Error and Janus/Ada Use_Error for missing files.
     when Ada.IO_Exceptions.Name_Error | Ada.IO_Exceptions.Use_Error =>
        Text_IO.Put_Line
          ("File '" & Command_Line.Argument (1) & "' not found");
        return;
   end;

   while not IO.End_Of_File (My_File) loop
     if Place mod Screen_Width = 0 then
        Long_Int_IO.Put (Place, 9);
        Text_IO.Put (":");
     end if;

     IO.Read (File => My_File, Item => Data);
     Text_IO.Put (" " & Hex (Data));

     if Place mod Screen_Width = Screen_Width -  1 then
        Text_IO.New_Line;
     end if;
     Place := Place + 1;
   end loop;

   IO.Close (My_File);
end Hexdump;

You can get the file from here.

Edit 2017-08-08: Changed Sequential_IO element from Character to Unsigned_8 as it simplified the code quite a lot and removes unnecessary type conversions.