This is for reference and has been html formatted, download from the links on the main page

#!/bin/env /usr/bin/python
#  change above line to local site needs
# 
# *** procrc.py - Copyright (C) 2015 Frank Spaniak All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# procrc.py - display the CRCs of source files of a compiled progress program
#     based on procrc.c from Grant P. Maizels.  credits go to his work.
#
# Python port and maintainer:
#     Frank Spaniak (python.procrc at gmail dot com, http://procrc.spaniak.org) 
#
# with appreciation for the help from:
#     Progress Tools (http://progress-tools.x10.mx)
# 
# Version history
#    1.0     fjs    Initial release
#
#    
#
# TODO:  Python 3 port
#

import sys
import os
import datetime
import struct
import array

import optparse
import textwrap
from optparse import OptionParser

#if sys.version_info < (3,0):
#    from __builtin__ import True
    

# Copyright and credits do not remove  
copyright = "***procrc.py - Copyright (C) 2015 Frank Spaniak, see --help"

program_credits=""" Display file and crc information for Progress dotr file(s) 
procrc.py -  Copyright (C) 2015 Frank Spaniak All rights reserved 
    (python.procrc@gmail.com, http://procrc.spaniak.org/) 

Original Work and credit to Grant P. Maizels
With thanks to Progress Tools (http://progress-tools.x10.mx) 
"""

READ_FROM_FILES    = True
READ_TO_FILES      = False
errors_encountered = None


testing_mode    = False # 
debug_it        = False #

file_list       = []
do_export       = False
do_sort         = False
do_tab          = False
do_headers_only = False
do_compare      = False
comp_result     = {}  # dictionary to hold compare function results

from_dir        = None
to_dir          = None

rcode_version       = 0
header1_size        = 0
text_segment_size   = 0                
pointer             = 0

byteswapped         = False
# -----  right out of procrc.c
O_MAGIC             = 0
O_TS                = 4
# signature size
O_HDR1              = 8   # v9+ but zero previously - so there should be no compatibility problems 
O_HDR11             = 56  # v11

O_RCVER             = int(0xE)      # 14
O_SEGT_SZ           = int(0x1E)     # 30
HDR_SZ              = int(0x44)     # 68

#
# constants
# SEGMENT LIST 
I_SEG_AD            = 0             
DEBUG_SEG_AD_V9     = int(0x18)
DEBUG_SEG_AD_V10    = int(0x24)
DEBUG_SEG_AD_V11    = int(0xC)      # Progress Tools   Thanks!

# crc offset 
O_PROG_CRC_V9       = int(0x46)  # 70
O_PROG_CRC_V10      = int(0x6E)  # 110
O_PROG_CRC_V11      = int(0xA4)  # 164

# DEBUG HEADER 
O_SRC_CNT_V9        = 2
O_SRC_CNT_V10       = int(0x14)
O_SRC_CNT_V11       = int(0x14)

O_SRC_LST           = 4

# DEBUG SRC LIST 
O_DEBSRC_NEXT       =  0
O_DEBSRC_CRC_V9     =  2
O_DEBSRC_CRC_V10    =  4
O_DEBSRC_CRC_V11    =  4

#O_DEBSRC_PPSZ_V9    =  4
#O_DEBSRC_PPSZ_V10   =  int(0xA)
#O_DEBSRC_PPSZ_V11   =  int(0xA)

O_DEBSRC_FILE_V9    =  9
O_DEBSRC_FILE_V10   =  int(0xB)
O_DEBSRC_FILE_V11   =  int(0xB)

# to fix optparse formatting so newlines work better
class IndentedHelpFormatterWithNL(optparse.IndentedHelpFormatter):
    def format_description(self, description):
        if not description: return ""
        desc_width = self.width - self.current_indent
        indent = " "*self.current_indent
        # the above is still the same
        bits = description.split('\n')
        formatted_bits = [
            textwrap.fill(bit,
                desc_width,
                initial_indent=indent,
                subsequent_indent=indent) for bit in bits]
        result = "\n".join(formatted_bits) + "\n"
        return result

    def format_option(self, option):
    # The help for each option consists of two parts:
    #   * the opt strings and metavars
    #   eg. ("-x", or "-fFILENAME, --file=FILENAME")
    #   * the user-supplied help string
    #   eg. ("turn on expert mode", "read data from FILENAME")
    #
    # If possible, we write both of these on the same line:
    #   -x    turn on expert mode
    #
    # But if the opt string list is too long, we put the help
    # string on a second line, indented to the same column it would
    # start in if it fit on the first line.
    #   -fFILENAME, --file=FILENAME
    #       read data from FILENAME
        result = []
        opts = self.option_strings[option]
        opt_width = self.help_position - self.current_indent - 2
        if len(opts) > opt_width:
            opts = "%*s%s\n" % (self.current_indent, "", opts)
            indent_first = self.help_position
        else: # start help on same line as opts
            opts = "%*s%-*s  " % (self.current_indent, "", opt_width, opts)
            indent_first = 0
        result.append(opts)
        if option.help:
            help_text = self.expand_default(option)
    # Everything is the same up through here
            help_lines = []
            for para in help_text.split("\n"):
                help_lines.extend(textwrap.wrap(para, self.help_width))
    # Everything is the same after here
            result.append("%*s%s\n" % (
                indent_first, "", help_lines[0]))
            result.extend(["%*s%s\n" % (self.help_position, "", line)
            for line in help_lines[1:]])
        elif opts[-1] != "\n":
            result.append("\n")
        return "".join(result)

#
# debugging and helpers
#
def print_bytes(addr, byteswapped):
    laddr = addr
    #if debug_it: print("   %s print bytes at at address: %s (0x%lx) %s" % (m, laddr, laddr, binascii.hexlify(memoryview(bytearray(p[laddr:laddr+4]))) ))
    a = [0]*16
    if byteswapped:
        for i, x in reversed(list(enumerate(p[laddr:laddr+16]))):
            a[i] = x
    else:
        for i, x in enumerate(p[laddr:laddr+16]):
            a[i] = x     
                   
    z = " ".join("{:02x}".format(x) for x in a)    
    b = " ".join("{:04x}".format(laddr))
    if debug_it: print "   print bytes at (",laddr,") " ,b," :", z

def print_bytes_big(m, addr, byteswapped):
    laddr = addr
    xlen = 16*16
    for v in range(laddr, laddr + xlen, 16):
        print_bytes(m, v, byteswapped)

# for progress export function
def doq(instring):
    return instring.replace('\"', '\"\"')
 
def enq(instring):
    return '\"' + instring + '\"'

def edq(instring):
    return enq(doq(instring))

# used by sort option
def sort_key(item):
    return item[0]

# bit flipping routines
def get_double(addr, byteswapped):
    laddr = pointer + addr        
    b = [0]*8  # init array to zero   
    if byteswapped:
        for i, x in reversed(list(enumerate(p[laddr:laddr+8]))):
            b[i] = x
    else:
        for i, x in enumerate(p[laddr:laddr + 8]):
            b[i] = x    
    double_val = struct.unpack("<q", "".join(map(chr, bytearray(b))))
    return double_val

def get_long(addr, byteswapped):
    laddr = pointer + addr
    if byteswapped: 
        return ((p[laddr+3] * 256 * 65536) +
        (p[laddr+2] * 65536) +
        (p[laddr+1] * 256) +
        p[laddr])
    else:
        return ((p[laddr] * 256 * 65536) +
        (p[laddr+1] * 65536) +
        (p[laddr+2] * 256) +
        p[laddr+3])

def get_short(addr, byteswapped):
    laddr = pointer + addr
    if byteswapped:
        return ((p[laddr+1] * 256) + p[laddr])
    else:
        return ((p[laddr] * 256) + p[laddr+1])

def get_byte(addr, byteswapped):
    laddr = pointer + addr
    return p[laddr]

def get_byte_a(addr):  # absolute no offset
    return p[addr]

def get_string_a(addr, s_length):
    x = "".join( chr( val ) for val in p[addr: addr + s_length] )
    return x

def null_filter(still, int_char):
    if not still:
        return False
    if int_char == 0:
        still = False
        return False
    return True

def get_string_terminated(addr):
    keep_going = True
    max_len = 18
    x = []
    for val in p[addr: addr + max_len]:
        keep_going = null_filter(keep_going, val)
        if keep_going:
            x.append(chr(val))
        else:
            break
    return "".join(x)

# the pointer is added in, in the subordinate routine
def get_var_size(addr, the_size, byteswapped):
    if the_size == 1:
        return get_byte(addr, byteswapped)
    elif the_size == 2:
        return get_short(addr, byteswapped)
    elif the_size == 4:
        return get_long(addr, byteswapped)
    elif the_size == 8:
        return get_double(addr, byteswapped)    


# Main Program 
def get_procrc(file_to_open, primary_list):
    global p, byteswapped, pointer
    
    rcode_version       = 0
    header1_size        = 0
    text_segment_size   = 0                
    pointer             = 0 
                    
    # see if file has progress r-code signature
    if get_long(O_MAGIC, byteswapped) != 0x56ced309:
        if get_long(O_MAGIC, byteswapped) == 0x09d3ce56:
            byteswapped = True
        else:
            if not do_export and not do_compare:
                print("Not a progress R-code file\n")
            return
       
    rcode_version = get_short(O_RCVER, byteswapped)  # works for all
    
    #is_64bits = False
    #if (rcode_version & 0x4000) != 0:
    #    is_64bits = True 

    if (rcode_version & 0x3FFF) >= 1100:
        if debug_it: print("   version 11 code detected")
        version         = 11
        o_src_cnt       = O_SRC_CNT_V11          # 28
        o_debsrc_crc    = O_DEBSRC_CRC_V11       # 4 
        o_debsrc_file   = O_DEBSRC_FILE_V11      # 11
        o_prog_crc      = O_PROG_CRC_V11         # 110
        sz_debsrc_next  = 4
        sz_src_lst      = 4
        
    elif (rcode_version & 0x3FFF) >= 1000:
        if debug_it: print("   version 10 code detected")
        version         = 10
        o_src_cnt       = O_SRC_CNT_V10
        o_debsrc_crc    = O_DEBSRC_CRC_V10
        o_debsrc_file   = O_DEBSRC_FILE_V10
        o_prog_crc      = O_PROG_CRC_V10
        sz_debsrc_next  = 4
        sz_src_lst      = 4
    else:
        if debug_it: print("   version 9 code detected")
        version         = 9
        o_src_cnt       = O_SRC_CNT_V9
        o_debsrc_crc    = O_DEBSRC_CRC_V9
        o_debsrc_file   = O_DEBSRC_FILE_V9
        o_prog_crc      = O_PROG_CRC_V9
        sz_debsrc_next  = 2
        sz_src_lst      = 2
    
    if (version == 11):
        debug_seg_ad = DEBUG_SEG_AD_V11
    else:
        if (rcode_version & 0x3FFF) >= 909:
            debug_seg_ad = DEBUG_SEG_AD_V10 
        else: 
            debug_seg_ad = DEBUG_SEG_AD_V9
    
    comp_timestamp = get_long(O_TS, byteswapped)
    
    if version == 11:
        header1_size = get_long(O_HDR11, byteswapped)    
    else:
        header1_size = get_long(O_HDR1, byteswapped)
 
    text_segment_size = get_long(O_SEGT_SZ, byteswapped)

    # timestamp is in localtime
    #
    char_date = datetime.datetime.fromtimestamp(comp_timestamp).strftime("%a %b %d %H:%M:%S %Y")
      
    pointer = pointer + HDR_SZ + header1_size
   
    # there is a bug here with what appears to be really old v9 dotr's that causes this to grok
    try:
        i_offset = get_long(I_SEG_AD, byteswapped)  # usually works out to zero
    
    except:
        if not do_export and not do_compare:
            print("Unable to get debugging segment offset in %s \n" % (file_to_open))
        return
    
    opointer = pointer    
    debug_offset = get_long(debug_seg_ad, byteswapped)
    
    #if debug_offset == 0:
    #    if not (do_export and do_compare):
    #        print("No debug segment in %s \n" % (file_to_open))
    #    return
    

    # p points to i segment
    pointer = opointer + text_segment_size + i_offset

    prog_crc = get_short(o_prog_crc, byteswapped)  # main code r-code crc
    
    pointer = opointer + text_segment_size + debug_offset
    src_count = get_short(o_src_cnt, byteswapped)  # number of include files       
    src_list = get_var_size(O_SRC_LST, sz_src_lst, byteswapped)
        
    struct_addr = src_list
   
    includes = []  # include list of tuples
          
    for _ in range(0, src_count):                
        include_crc = get_short(struct_addr + o_debsrc_crc, byteswapped) # good!

        full_path_ptr = pointer + struct_addr + o_debsrc_file        
        file_path = get_string_a(full_path_ptr, get_byte_a(full_path_ptr - 1))

        file_name_ptr = pointer + struct_addr + o_debsrc_file + len(file_path)
        file_name = get_string_terminated(file_name_ptr)

        # now build the address of the next file reference
        struct_addr = get_var_size(struct_addr + O_DEBSRC_NEXT, sz_debsrc_next, byteswapped)
        includes.append((file_name, file_path, include_crc))
    
    if do_sort: # reorder output list
        includes = sorted(includes, key=sort_key)  #Sort on the module name
        
    if do_compare:
        xname = os.path.basename(file_to_open)
        
        xptr = 0  # 0 = left column 1 = right
        if not primary_list:
            xptr = 1
        if not xname in comp_result:
            comp_result[xname] = [0,0, {}] # Create entry for the output report
        comp_result[xname][xptr] = prog_crc  
        
        # now get the detail for this one and put into the same dictionary entry
        for inc in includes:
            iname = inc[0]
            if not iname in comp_result[xname][2]:
                comp_result[xname][2][iname] = [0,0]
            comp_result[xname][2][iname][xptr] = inc[2]      
    else: 
        if not do_export:
            if not do_tab:
                # old familiar printout
                print("R-Code file: %s" % file_to_open)
                print("Size: %s" % len(p))
                print("Compiled on: %i (0x%lx) %s" % (comp_timestamp, comp_timestamp, char_date))
                print("R-Code version: %s" % str(rcode_version))
                print("R-Code CRC: %s" % str(prog_crc))
        else:  
            # progress export format dotr information
            x = ''
            x += edq("%s" % (file_to_open)) # original filename to check
            x += " %s" % (len(p))           # filesize
            x += " %s" % (datetime.datetime.fromtimestamp(comp_timestamp).isoformat())
            x += " %s" % str(rcode_version)
            x += " %s" % str(prog_crc)
            print x
    
        if not do_headers_only:
            for inc in includes:
                if do_export:
                    # progress export format include references
                    x = ''
                    x += edq("%s" % (file_to_open))     # original filename to check
                    x += " " + edq("%s" % (inc[0]))     # include basename
                    x += " " + edq("%s%s" % (inc[1],inc[0])) # reference path + name
                    x += " " + "%s" % (inc[2])                   # include crc
                    print(x)           
                else:
                    if do_tab:
                        print("Source CRC:\t%s\t%s%s\t%s" % (inc[0], inc[1],inc[0], inc[2]))
                    else:
                        print("Source CRC: %s %s%s %s" % (inc[0], inc[1],inc[0], inc[2]))
                
    if do_export and not do_headers_only:
        print(".")

def read_dotr(filename):
    try: 
        f = open(filename, "rb")  # read binary
        raw_data = f.read()       # read as byte strings
        f.close()
        return array.array('i', map(ord,raw_data)) # convert to array int
    except:
        raise 
       
def get_file_list(dir_or_file):
    """return the list of files to be processed """
    try:
        if os.path.isdir(dir_or_file):
            temp_list = os.listdir(dir_or_file)
            return [os.path.join(dir_or_file, i) for i in temp_list if i.endswith('.r')]
        else:  # single filename
            return [dir_or_file]
    except:
        raise

def process_file(filename, read_type):
    global p, errors_encountered

    if os.path.isfile(filename):
        try:            
            p  = None  # reset for each iteration                  
            try:  # read in the binary dotr file
                p = read_dotr(filename) 
            except:
                if read_type:
                    raise
                else:
                    errors_encountered = True
        except:
            # if error on export mode silently fail
            if read_type:
                if not do_export:
                    print("ERROR: Unable to read %s \n" %(filename))
                if len(file_list) == 1:
                    sys.exit(1)
            errors_encountered = True

        get_procrc(filename, read_type)  # true indicate that this is the primary list
        
    else:
        errors_encountered = True
 
 
if __name__ == '__main__':

    file_list = []    # file list to do
    clist = []        # compare to list
    comp_result = {}  # compare results dictionary

    parser = OptionParser(
    usage="%prog [--version --sort --tab --dir [path] --export [--only] --compare [--message] --help ] rcode.r [rcode2.r ...]",
    description=program_credits,
    version="%prog 1.0",
    formatter=IndentedHelpFormatterWithNL() )

    if sys.version_info<(2,4,0):
        print "You need python 2.4 or later to run this script\n"
        parser.print_help()
        sys.exit(1)

    parser.add_option("-s", "--sort",
        action="store_true", dest="do_sort",
        help="Sort include files by module name")
    
    parser.add_option("-t", "--tab",
        action="store_true", dest="do_tab",
        help="Tab delimit include filenames")
    
    parser.add_option("-d", "--dir", type='string',
        action="append", default=[], dest="scan_dir",
        help="Directory Name to scan. Repeat as needed")
        
    parser.add_option('-e', '--export', action='store_true', 
        dest='do_export', 
        help='Output in export format')

    parser.add_option('-o', '--only', action='store_true', 
        dest='do_headers_only', 
        help='Only the header information is exported')

    parser.add_option('-c', '--compare', type='string',
        action="append", default=[], dest="cdir",
        help='Compare to file/dir, repeat as needed')
   
    parser.add_option('-u', '--message', type='string',
        dest="user_message",
        help='User message to put on compare report')    
    
    # args may also contain the filenames to process
    options, args = parser.parse_args()
    
    if options.do_sort != None:
        do_sort = True

    if options.do_tab != None:
        do_tab = True
        
    if options.do_export != None:
        do_export = True        # progress export format
        
    if options.do_headers_only != None:
        if not do_export:
            print "ERROR: Export (-e) must be specified to use headers only\n"
            parser.print_help()
            sys.exit(1)
        do_headers_only = True        # headers only in progress export format

    # testing 
    if testing_mode:
        #do_export = True
        #args = [ '/apps/workspace/procrc/src/dotrs/crctest9.r', '/apps/workspace/procrc/src/dotrs/crctest10.r', '/apps/workspace/procrc/src/dotrs/crctest11.r']  
        #options.scan_dir = [ '/apps/test/dir' ]
        #options.user_message = 'first test'
        pass
        
    # read the dir names list to compare from  
    if len(options.scan_dir) != 0:
        for xdir in options.scan_dir:
            try:
                file_list.extend(get_file_list(xdir))        
            except:
                print "file/directory %s was not found" % (xdir)
        if len(file_list) > 0: from_dir = options.scan_dir[0]

    # read any filenames on the command line
    if len(args) != 0:
        for xfile in args:
            file_list.append(xfile)
          
    # read the directory/file names to compare to
    if len(options.cdir) != 0:
        do_compare = True        
        for xdir in options.cdir:            
            try:  #  build out the list to compare to
                clist.extend(get_file_list(xdir))    
            except:
                print "Compare file/directory %s was not found" % (xdir)
                do_compare = False
        if len(clist) > 0: to_dir = options.cdir[0]

    # if doing compare turn off anything to do with exporting
    if do_compare:
        do_export = False
        do_headers_only = False

    if not do_export and not do_compare:
        print(copyright)
    
    # if nothing to do then print and exit
    if len(file_list) == 0:
        if do_export:
            print(".")
            print(".")            
        else:       
            print("Nothing to do!")
        sys.exit(1)
    
    # read and process the 'from' files   
    for filename in file_list:
        process_file(filename, READ_FROM_FILES)

        if len(args) > 1 or len(file_list) > 1:
            if not do_export and not do_compare:
                print(" ") # put a blank line between them
                
    # read and process the compare to list 
    #
    if len(clist) > 0:
        # read in the compare to list
        for filename in clist:
            process_file(filename, READ_TO_FILES)
            
        # now we have both sides
        # a zero in the results list, indicates that the file was not found

        files_checked = len(comp_result)
        diffs_found = 0
        
        # clean out the comparison structure of matching entries 
        # leaving only the differences to report
        for xkey in comp_result.keys():
            if comp_result[xkey][0] == comp_result[xkey][1]:
                # eliminate all those that are the same program level crc
                try:
                    del comp_result[xkey]
                except KeyError:
                    continue
            else:
                include_dict = comp_result[xkey][2]
                for ykey in include_dict:
                    if include_dict[ykey][0] == include_dict[ykey][1]:
                        include_dict[ykey] = [ 0, 0 ] # a hack
                        diffs_found += 1
        
        # display the results            
        print(copyright) 
  
        # now report the results of the comparison
        if options.user_message != None: 
            print "%-80s" % (options.user_message)            
        if from_dir != None: 
            print "Dir   %-s" % (from_dir)
        if options.cdir != None: 
            print "CDir  %-s" % (to_dir)
                
        if len(comp_result) != 0:
            print "%-25s  %-10s  %-10s" % ('FileName', 'CRC Dir', 'CRC Cdir')              
            for key in sorted(comp_result):
                print "%-25s  %-10s  %-10s" % (key, str(comp_result[key][0]), str(comp_result[key][1]))
                includes = comp_result[key][2]
                for include in sorted(includes):
                    # to work around dictionary size change issue
                    if includes[include][0] == 0 and includes[include][1] == 0: # skip if both are zero 
                        continue
                    print "   %-22s     %-10s  %-10s" % (include, str(includes[include][0]), str(includes[include][1]))    
                print " "    
        print "%d unique files checked, %d differences found." % (files_checked, diffs_found)


    if do_export:
        print(".")