AOJの自動化ツールを作った

概要

Aizu Online Judge (AOJ) を快適に楽しむためのツールを作成し、公開した。

背景

私事だが4月からソフトウェアエンジニアとして某企業で働くことになったので、以前取り組んでいたオンラインジャッジをもう一度やろうかと考えていた。

しかし、以前オンラインジャッジをやっていた際に、単純作業を繰り返していたのが苦だった。 C++を使用していたので、プログラムが書けたら一度コンパイルし、入力データを標準入力から手入力(もしくはテキストファイルに書き出してリダイレクト)し、間違っていたらまたコンパイルからやり直し...といった具合だった。

この一連の作業が煩わしいんだよなぁと思いながら AOJ のサイトを眺めていた折、APIが公開されていることに気が付いた。

この API を使えば割といい感じに自動化できるのでは?と思い、ツールを作成した。 実装は Go で行なった。理由はまだ Go をちゃんと書いたことがなかったということと、クロスコンパイルすればどの OS でも動いてくれる(可能性が高い)からである。

aojtool

拙作 aojtool は先述した面倒な作業をコマンド一発で実行できる。 例えばこの問題のサンプルを実行したい場合、

$ aojtool run ITP1_1_c rectangle.cpp

のようにして実行できる。 提出したい時も

$ aojtool submit TP1_1_c rectangle.cpp

で提出でき、結果の確認も

$ aojtool status

で可能である。

実装

CLI フレームワークとして cobra を用いた。 サブコマンドやオプション等をいい感じに扱ってくれる大変便利なものである。

また API クライアントのアーキテクチャこちらの記事を参考にした。 詳細は当該記事を参照されたし。

type Client struct {
    Endpoint  *url.URL
    UserAgent string

    httpClient *http.Client
    cookieJar  *cookiejar.Jar

    Auth   *AuthService
    Submit *SubmitService
    Status *StatusService
    Test   *TestService
}

Client からサービスオブジェクトを参照して各種処理を実行する。 こうすることで Client のコードが肥大化することを防ぐ。

また、go の net/http/cookiejar を用いることで cookie をいい感じに扱ってくれる。 この cookieシリアライズして書き出したり読み出すことによってログインを保持している。

まとめ

AOJ を快適に楽しむためのツールを作った。 テストが全くなかったり、エラー処理(特に http リクエスト関連)がかなり雑なので、気が向いたら直していきたい。 よかったら go get して使ってみてください。

追記

  • 2019-02-18: 色々直した。テストケースを保存したりしたい。

PNGあぶり出し

Introduction

インターネットに溢れている画像ファイルには、適当なテキストを隠しておくことができる。 PNG画像のバイナリをいじりながら、画像にメッセージを埋め込んでみる。

PNGの仕様

PNG(Portable Network Graphics)は可逆な画像圧縮フォーマットである。 仕様の詳細はこちらPNGは8バイトのシグネチャといくつかのチャンクから構成されている。

チャンクにはいくつかの種類があるが、どのチャンクもチャンクサイズ、チャンクの種類、実際のデータ、チャンクのCRCを持っている。 チャンクの種類は画像の幅や高さを含むIHDRや、画像の終端を示すIENDといったものがある。 CRCはデータ破損などを検出するためのものだが、詳しい説明は割愛する。 f:id:ken_tunc:20180922010117p:plain

チャンクを覗く

実際にPythonを使ってPNG画像からチャンクを見てみよう。 画像はいつものを使わせていただく。 まずチャンクを表すクラスを定義する。 Python3.7で追加されたData Classesが便利なので、これを使う。 Data Classes__init__()などを自動的に生成してくれるので大変使い勝手が良い。 変数の型も宣言すると補完が効いたりして気持ちがいいので、今回は積極的に使っていく。

from dataclasses import dataclass

@dataclass
class Chunk:
    length: int
    type: str
    data: bytes

次に、与えられたファイルのチャンクのジェネレータを返す関数を定義する。 f.read(size)メソッドでsizeバイトを読み込むことができる。 また、PNGはビッグエンディアンであることにも注意する。

def read_chunks(fname: str):
    with open(fname, 'rb') as f:
        # シグネチャを出力
        print(f'signature: {f.read(8)}')

        while True:
            length = int.from_bytes(f.read(4), 'big')
            type = f.read(4).decode()
            data = f.read(length)
            yield Chunk(length, type, data)

            # 画像の終端ならば終了
            if type == 'IEND':
                break

            # CRCの4バイトをスキップ
            f.seek(4, 1)


for chunk in read_chunks('Lenna_text.png'):
    print(f'chunk {chunk.type} ({chunk.length} bytes)')

これを実行すると、次のような出力が得られる。

signature: b'\x89PNG\r\n\x1a\n'
chunk IHDR (13 bytes)
chunk sRGB (1 bytes)
chunk IDAT (473761 bytes)
chunk IEND (0 bytes)

PNGシグネチャ\x89PNG\r\n\x1a\nであり、IHDRというチャンクには13バイトのデータがあることがわかる。 IHDRチャンクには画像の幅や高さ(4バイト)、ビット深度やカラータイプ(1バイト)などが含まれているので、これを出力してみよう。 1度に複数のバイト列を解釈するにはstructモジュールが便利だ。

import struct

for chunk in read_chunks('Lenna_text.png'):
    print(f'chunk {chunk.type} ({chunk.length} bytes)')

    if chunk.type == 'IHDR':
        width, height, bit_depth, color_type = \
            struct.unpack_from('>IIBB', chunk.data)
        print(f'width: {width}, height: {height}, bit depth: {bit_depth}, '
              f'color type: {color_type}')

struct.unpack_from()は与えられたフォーマットに沿ってアンパックしてくれる。 >はビッグエンディアンIBはそれぞれunsigned int(4バイト)、unsigned char(1バイト)を表す。 これを実行すると次のように出力される。 

signature: b'\x89PNG\r\n\x1a\n'
chunk IHDR (13 bytes)
width: 512, height: 512, bit depth: 8, color type: 2
chunk sRGB (1 bytes)
chunk IDAT (473761 bytes)
chunk IEND (0 bytes)

画像の高さと幅が512pxであることがわかる。 その他細かい仕様は各自調べてみて欲しい。

秘密のテキスト

次はPNG画像にテキスト情報を埋め込む。 PNGには補助チャンクとしてtEXtチャンクがあり、テキストを保持することができる。 tEXtチャンクはキーワードとテキストをnull文字で結合したものを格納できる。 まず、先ほどのChunkクラスを継承したTextChunkクラスを作る。

@dataclass
class TextChunk(Chunk):

    def __init__(self, keyword: str, text: str):
        data_str = keyword + '\0' + text
        self.data = data_str.encode()
        self.length = len(self.data)
        self.type = 'tEXt'

このクラスの__init__()は自動生成されるものではなく、新たに定義されたものが呼び出される。 次にChunkをファイルに書き出す際にバイト列に変換する必要があるため、to_bytes()メソッドを定義する。

from binascii import crc32

@dataclass
class Chunk:
    # 省略

    def to_bytes(self) -> bytes:
        # それぞれをバイト列に変換
        length = self.length.to_bytes(4, 'big')
        type = self.type.encode()
        data = self.data
        crc = crc32(type + data).to_bytes(4, 'big')

        return length + type + data + crc

CRCはチャンクの種類とデータを用いて計算される。 今回は組込みのbinasciiモジュールを使って済ませてしまった。 最後に、これらのクラスを用いて、新しいPNGファイルを書き出す。 tEXtチャンクはIHDRチャンクとIENDチャンクの間にあればどこでもよい。

keyword = 'comment'
text = 'nanrakano message'
text_chunk = TextChunk(keyword, text)

# 元画像のチャンクを取得
chunks = list(read_chunks('Lenna.png'))

with open('Lenna_text.png', 'wb') as f:
    # シグネチャを出力
    f.write(b'\x89PNG\r\n\x1a\n')
    # IHDRチャンクを出力
    f.write(chunks[0].to_bytes())
    # tEXtチャンクを出力
    f.write(text_chunk.to_bytes())
    # 残りのチャンクを出力
    for chunk in chunks[1:]:
        f.write(chunk.to_bytes())

このようにして下の画像にテキスト(サンプルコードとは異なる)を埋め込んだので、ダウンロードしてメッセージを読んでもらえると嬉しい。 f:id:ken_tunc:20180926151901p:plain

tEXtチャンクのテキストを読むために少しだけコードを追加する。

for chunk in read_chunks('Lenna.png'):
    print(f'chunk {chunk.type} ({chunk.length} bytes)')

    if chunk.type == 'IHDR':
        # 省略

    elif chunk.type == 'tEXt':
        keyword, text = chunk.data.decode().split('\0')
        print(f'{keyword}: {text}')