#!/bin/env python
"""

cldmpReplace.py -- Replace old strings with new ones in SVN dump files

This script performs string replacement within a Subversion (SVN) dump
file.  SVN dump files, which use the file extension "cldmp", have a specific
format with certain constraints.  For example there are imbedded byte totals
that must be adjusted as content is changed.

This script will only modify the contents of the "value" fields of the
dump file (i.e. the portion between a line of the form "V <byte count>" and
the line "PROPS-END").  Any occurances of the string to be replaced in other
locations in the dump file outside of the value field will be ignored.

Usage:

    cldmpReplace.py [options] <input file> <old string> <new string>


Options:

-h: Print this help message.

-b: Make a backup copy of the input file.

-v: Verbose mode

-x: Overwrite input file.  If you select this option, it is recommended
    that you also use the backup option -b.  Without the -x option, an
    output file will be created that has the same name as the input file
    but with a tilde (~) at the end.


Examples:

    cldmpReplace.py -h

    cldmpReplace.py -v file.cldmp oldstring newstring

    cldmpReplace.py -b -x -v file.cldmp oldstring newstring    
    
            
"""

import getopt
import os
import shutil
import string
import sys
import time

lastLineOfPropsSection = "PROPS-END\n"

DEBUG = True
BACKUP = False # Gets overriden by command line option
VERBOSE = False
OVERWRITE_INPUT_FILE = False
occurancesIgnored = 0
occurancesReplaced = 0



def main():
    t1 = now()
    global BACKUP, VERBOSE, OVERWRITE_INPUT_FILE
    # Parse the command line
    try:
        opts, args = getopt.getopt(sys.argv[1:], 'hbvx')
    except getopt.error, msg:
        usage('getopt error: ' + str(msg))
        sys.exit(1)

    for o, a in opts:
        if o == '-h':
            usage()
            return
        if o == '-b':
            BACKUP = True
        if o == '-v':
            VERBOSE = True
        if o == '-x':
            OVERWRITE_INPUT_FILE = True

    if len(args) != 3:
        usage()
        sys.exit(1)

    fileIn = args[0]
    stringOld = args[1]
    stringNew = args[2]
    fileTemp = fileIn + "~"

    if BACKUP:
        fileBackup = fileIn + "." + getTimeStamp()
        shutil.copyfile(fileIn, fileBackup)

    fin = open(fileIn, 'rb')
    ftemp = open(fileTemp, 'wb')
    lineNum = 0
    try:
        linesQueue = []
        reachedEndOfSection = False
        for line in fin:
            lineNum += 1
            if line == lastLineOfPropsSection:
                reachedEndOfSection = True
            linesQueue.append(line)
            if reachedEndOfSection:
                linesOutput = processSection(linesQueue, lineNum, stringOld, stringNew)
                ftemp.writelines(linesOutput)
                linesQueue = []
                reachedEndOfSection = False
            else:
                continue
        if len(linesQueue) > 0:
            ftemp.writelines(linesQueue)
    finally:
        fin.close()
        ftemp.close()
    t2 = now()
    duration = t2 - t1
    duration_hms = hms(duration)
    if duration > 0:
        linesPerSecond = lineNum/duration
    else:
        linesPerSecond = ""
    
    print "    Lines processed: %s" % lineNum
    print "           Duration: %s" % duration_hms
    if linesPerSecond:
        print "   Lines per second: %06.1f" % linesPerSecond
    print " Occurances ignored: %s" % occurancesIgnored
    print "Occurances replaced: %s" % occurancesReplaced

    if OVERWRITE_INPUT_FILE:
        os.unlink(fileIn)
        os.rename(fileTemp, fileIn)
    
def processSection(lines, lineNumOverall, stringOld, stringNew):
    global occurancesIgnored, occurancesReplaced
##    # Return original lines if the string to be replaced is not found.
##    s = string.join(lines)
##    if s.find(stringOld) == -1:
##        return lines

    # Extract Properties sub-section
    propsStartMarker = "Content-length: "
    propsStartMarkerIndex = -1
    for i in range(len(lines)):
        line = lines[i]
        if line[:len(propsStartMarker)] == propsStartMarker:
            propsStartMarkerIndex = i
            break
    if propsStartMarkerIndex == -1:
        raise 'Error in section ending with line %s: Text "%s" not found.' \
              % (lineNumOverall, propsStartMarker)
    propsStartIndex = propsStartMarkerIndex + 1
    linesProps = lines[propsStartIndex:-1]
    #print linesProps
    propsByteCountOld = len(string.join(linesProps))
    #print propsByteCountOld

    # If the string to be replaced occurs in the lines preceeding the property
    # keys and values, and if VERBOSITY is on, then alert the user that these
    # lines are being ignored.
    lineNum = lineNumOverall - len(lines) + 1
    for line in lines[:propsStartIndex]:
        try:
            stringOldIndex = line.index(stringOld)
            if VERBOSE:
                print "IGNORED: line %s: Text to be replaced is outside of "\
                      "property value." % (lineNum)
            occurancesIgnored += 1
            try:
                while 1:
                    remainingLine = line[stringOldIndex+len(stringOld):]
                    stringOldIndex = remainingLine.index(stringOld)
                    occurancesIgnored += 1
                    line = remainingLine
            except IndexError:
                pass
            lineNum += 1
        except ValueError:
            lineNum += 1
            continue

    # Process property keys and values
    linesProps = processPropLines(linesProps, lineNum, stringOld, stringNew)
    propsByteCountNew = len(string.join(linesProps))
    contentLengthDelta = propsByteCountNew - propsByteCountOld
    if contentLengthDelta != 0:
        # Update value of "Content-length:" line
        contentLengthOld = long(lines[propsStartMarkerIndex].split()[1])
        contentLengthNew = contentLengthOld + contentLengthDelta
        lines[propsStartMarkerIndex] = propsStartMarker + " %s\n" % contentLengthNew
        # Update value of "Prop-content-length:" line
        propContentLengthMarker = "Prop-content-length: "
        for i in range(len(lines)):
            line = lines[i]
            if line[:len(propContentLengthMarker)] == propContentLengthMarker:
                propContentLengthOld = long(line.split()[1])
                propContentLengthIndex = i
                break
        propContentLengthNew = propContentLengthOld + contentLengthDelta
        lines[propContentLengthIndex] = propContentLengthMarker + " %s\n" % propContentLengthNew
    
    lines = lines[:propsStartIndex] + linesProps
    lines = lines + [lastLineOfPropsSection]
    return lines

def processPropLines(linesProps, lineNum, stringOld, stringNew):
    global occurancesIgnored, occurancesReplaced
    inKeyLines = False
    inValueLines = False
    valueByteCount = -1
    valueByteCountDelta = 0
    for i in range(len(linesProps)):
        line = linesProps[i]
        lineNum += 1
        if line[:2] == "K ":
            inKeyLines = True
            inValueLines = False
            if valueByteCountDelta != 0:
                valueByteCountNew = valueByteCount + valueByteCountDelta
                linesProps[valueByteCountLineIndex] = "V " + "%s\n" % valueByteCountNew
        elif line[:2] == "V ":
            if inValueLines == False:
                inValueLines = True
                inKeyLines = False
                valueByteCount = long(line.split()[1])
                valueByteCountDelta = 0
                valueByteCountLineIndex = i
            else:
                # A value line starts with "V " by coincidence
                pass
        elif inKeyLines:
            try:
                stringOldIndex = line.index(stringOld)
                if VERBOSE:
                    print "IGNORED: line %s: Text to be replaced is within "\
                          "property key \"%s\"." % (lineNum, line[:-1])
                occurancesIgnored += 1
                try:
                    while 1:
                        remainingLine = line[stringOldIndex+len(stringOld):]
                        stringOldIndex = remainingLine.index(stringOld)
                        occurancesIgnored += 1
                        line = remainingLine
                except IndexError:
                    pass
            except ValueError:
                pass
        else:
            inValueLines = True
            # Count number of replacements
            try:
                stringOldIndex = line.index(stringOld)
                if VERBOSE:
                    print "REPLACED: line %s" % lineNum
                occurancesReplaced += 1
                try:
                    while 1:
                        remainingLine = line[stringOldIndex+len(stringOld):]
                        stringOldIndex = remainingLine.index(stringOld)
                        occurancesReplaced += 1
                        line = remainingLine
                except IndexError:
                    pass
            except ValueError:
                pass
            # Make replacements
            line = linesProps[i]
            lineLengthPreReplacement = len(line)
            line = line.replace(stringOld, stringNew)
            lineLengthPostReplacement = len(line)
            lineLengthDelta = lineLengthPostReplacement - lineLengthPreReplacement
            valueByteCountDelta = valueByteCountDelta + lineLengthDelta
            linesProps[i] = line
    if valueByteCountDelta != 0:
        valueByteCountNew = valueByteCount + valueByteCountDelta
        linesProps[valueByteCountLineIndex] = "V " + "%s\n" % valueByteCountNew        
    return linesProps



def getTimeStamp():
    timeStamp = time.strftime("%Y%m%d%H%M%S", time.localtime())
    return timeStamp

def now():
    return time.time()
    
def iso8601(t):
    time_tuple=time.localtime(t)
    iso8601_time=time.strftime("%Y-%m-%d %H:%M:%S", time_tuple)
    return iso8601_time

def hms(t):
    hours = int(t / 3600)
    if hours >= 1:
        t = t - (hours * 3600)
    minutes = int(t / 60)
    if minutes >= 1:
        t = t - (minutes * 60)
    seconds = t
    return "%d:%02d:%06.3f" % (hours, minutes, seconds)


def usage(msg=""):
    if msg:
        print msg
    print __doc__
    return
        
if __name__ == "__main__":
    main()
