    Dim ProgramID$/"CP/M to SDOS file conversion utility program V1.0a"/
!
!   Program to copy files from CP/M standard single density-disk...
!           into SDOS files and vice-versa
!   Runs under SDOS.
!
!   Supports the commands listed in HELP comment below.
!
!   Note: When copying to SDOS, CP/M files are preserved as is,
!   INCLUDING the embedded LineFeeds; SDOS will ignore the linefeeds.
!   This allows any arbitrary (binary) file to be copied to SDOS.
!   Note that copied text files will have CP/M's ^Z at end.
!   When copying from SDOS to CP/M, linefeeds are inserted after
!   each CarriageReturn, and ^Z (CP/M EOF) is written at end of file.
!   Thus, one can only copy text files to CP/M.
!
!   v1.0   First stab
!   v1.0a  Fixed "0 record count in all directory entries"
!          Changed "COPY <cpmfid>=<sdosfid>" to "PIP ..."
!
Dim CPMDisk/1/,CPMDrive$(50),CCUnlockDisk$/:e,4,0,:10/
Dim CPMBuffer$(128),CPMDirectoryEntry$(32),CPMFileName$(50)
Dim CPMDiskMap$(256)
Dim TargetCPMDirectoryName$(11)
Dim CCSetMapAlgorithm$/:e,8,0,:12/,CPMStandardMapalgorithm$/:00,:06/
Dim SDOSFile/2/,SDOSFileName$(50),SDOSFileBuffer$(256)

CPMUtility: ! Start of CP/M utility main code
    If col(0)>1
    Then
        ! Smart user, get drive spec from same line
        Input "" CPMDrive$
        Print ProgramID$
    Else
        Print ProgramID$
        Input "Name of disk device containing CP/M diskette: " CPMDrive$
    Fi

    Open #CPMDisk,CPMDrive$ \ ! Assume standard CP/M single-density disk
    CPMReservedTracks=2 \ ! Number of tracks reserved for CP/M itself
    CPMDirectoryEntries=64 \ ! Number of entries in CP/M Directory
    CPMSectorSize=128 \ ! Size of CP/M data sector in bytes
    CPMNumberOfSectorsPerTrack=26 \ ! For IBM diskette
    CPMClusterSizeInSectors=8 \ ! Number of sectors to make 1024 bytes
    Syscall #CPMDisk,CCSetMapAlgorithm$,CPMStandardMapAlgorithm$
    Syscall #CPMDisk,CCUnlockDisk$ \ ! Unlock disk so we can write on it
!^L
CPMUtilityNextCommand: ! Process next command
    Input "CP/M Utility>" CPMBuffer$
    Let CPMBuffer$=LowerCase$(CPMBuffer$)
    If CPMBuffer$="exit" Then exit
    ElseIf CPMBuffer$="dir" Then CPMDirectory
    ElseIf CPMBuffer$="init" then CPMInitialize
    ElseIf Find(CPMBuffer$,"copy ")=1 Then SDOSCopy
    ElseIf Find(CPMBuffer$,"pip ")=1 Then CPMCopy
    ElseIf Find(CPMBuffer$,"era ")=1 Then EraseCPMFile
ShowHelpMessage:
    Print "?? Invalid CP/M Utility command."
    !
    ! User needs some HELP.
    !
    Print "Only the following commands are valid here:"
    PRINT "   DIR"  \ ! To see list of files on CP/M diskette

    PRINT "   COPY <cp/m file spec> TO <target sdosfilespec>"
    Print "   PIP <cp/m target file spec> = <sdosfilespec>"
    Print "   ERA  <cp/m file spec>" \ ! To make a CP/M file go away
    Print "   EXIT" \ ! To get out of CP/M utility
    Print "   INIT" \ ! To initialize file structure on CP/M diskette
    Goto CPMUtilityNextCommand
!^L
!
!   CP/M Directory Entry Format:
!
!   !------------------!
!   !     Flag         !  :e5 --> Deleted, 0--> Real, other values --> ?
!   !------------------!
!   !                  !  All characters in uppercase.
!   !    Filename      !  11 bytes, implicit "." before last 3
!   !                  !  Bits set on filename chars are used as protection.
!   !------------------!
!   !   Extent Number  !  1 byte; 0 --> 1st extent, 1 --> 2nd extent, etc.
!   !------------------!
!   !     unknown      !  2 bytes; normally zero
!   !------------------!
!   ! #SectorsinExtent !  1 byte; range 0..:80 for CP/M standard disk
!   !------------------!
!   !                  !
!   !   Cluster List   !  16 bytes, containing list of cluster numbers...
!   !                  !  owned by file.  Cluster #0 --> Non-existent.
!   !------------------!
!
!   Each file has a directory entry for each block of 16 clusters owned.
!   Note there are only 256 clusters max!?
!   Disk map is built when disk is "mounted".
!
CPMDirectory: ! List out directory of CP/M diskette
    Print "   Files on CP/M diskette in "; CPMDrive$
    ActualCPMFileCount=0
    Position #CPMDisk...
&            @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks
    For DirectoryEntry=1 to CPMDirectoryEntries
        Read #CPMDisk,CPMDirectoryEntry$
        If CPMDirectoryEntry$[1]=:E5 Then Cycle DirectoryEntry
        If CPMDirectoryEntry$[1]=0 and CPMDirectoryEntry$[13]=0
        Then
            ActualCPMFileCount=ActualCPMFileCount+1
            Print CPMDirectoryEntry$[2,8];".";CPMDirectoryEntry$[10,3];" ";
            Print Hex$(CPMDirectoryEntry$[13]);
            Print Hex$(CPMDirectoryEntry$[14]);
            Print Hex$(CPMDirectoryEntry$[15]);
            Print Hex$(CPMDirectoryEntry$[16])
        Fi
    Next DirectoryEntry
    Print ActualCPMFileCount;"files."
    Goto CPMUtilityNextCommand
!^L
EraseCPMFile: ! Delete a CP/M file from diskette
    CPMFileName$=Right$(CPMBuffer$,5) \ ! Drop "era " prefix
    Gosub ExtractCPMFileName
    Position #CPMDisk...
&            @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks
    Let FileFound=false
    For DirectoryEntry=1 to CPMDirectoryEntries
        Read #CPMDisk,CPMDirectoryEntry$
        If CPMDirectoryEntry$[1]=:E5 Then Cycle DirectoryEntry
        If CPMDirectoryEntry$[1]=0 ...
&          and CPMDirectoryEntry$[2,11]=TargetCPMDirectoryName$
        Then
            FileFound=True \ ! Remember we found the file
            CPMDirectoryEntry$[1]=:E5 \ ! Mark directory slot as free
            Write #CPMDisk...
&              @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks...
&              +(DirectoryEntry-1)*32,CPMDirectoryEntry$
        Fi
    Next DirectoryEntry
    If FileFound=0
    Then Print "No such CP/M file."
    Goto CPMUtilityNextCommand

CPMInitialize:  ! Initialize diskette with CP/M file system
    Input "This destroys contents of diskette, are you sure ? " CPMBuffer$
    If Find(UpperCase$(CPMBuffer$),"Y")<>1
    Then Print "CP/M Disk Initialization aborted."\Goto CPMUtilityNextCommand

    ! Zap all the directory entries.
    Len(CPMDirectoryEntry$)=32 \ ! Set up deleted directory entry
    For i=1 to maxlen(CPMDirectoryEntry$)
        CPMDirectoryEntry$[i]=:E5 \ ! "Deleted" directory entry
    Next i
    Len(CPMDirectoryEntry$)=MaxLen(CPMDirectoryEntry$)

    Position #CPMDisk...
&            @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks
    For DirectoryEntry=1 to CPMDirectoryEntries
        Write #CPMDisk,CPMDirectoryEntry$
    Next DirectoryEntry
    Print "CP/M Disk Initialization completed."
    Goto CPMUtilityNextCommand
!^L
SDOSCopy: ! Copy CP/M file to SDOS file
    i=Find(CPMBuffer$," to ")
    If i>0
    Then
        SDOSFileName$=Right$(CPMBuffer$,i+4)
        CPMFileName$=CPMBuffer$[6,i-6] \ ! Drop SDOS file name, "copy " prefix
        Gosub ExtractCPMFileName
        Goto CopyCPMToSDOSFile
    Else ShowHelpMessage

CPMCopy: ! Copy SDOS file to CP/M file
    i=Find(CPMBuffer$,"=")
    If i>0
    Then
        SDOSFileName$=Right$(CPMBuffer$,i+1)
        CPMFileName$=CPMBuffer$[5,i-5] \ ! Drop SDOS file name, "pip " prefix
        Gosub ExtractCPMFileName
        Goto CopySDOSToCPMFile
    Else ShowHelpMessage

ExtractCPMFileName: ! Extract CP/M file name from CPMBuffer$
    Len(TargetCPMDirectoryName$)=11 \ ! So i can blank pad later
    i=Find(CPMFileName$,".")
    If i=0
    Then
        TargetCPMDirectoryName$[1,11]=CPMFileName$
    Else
        TargetCPMDirectoryName$[1,8]=CPMFileName$[1,i-1]
        TargetCPMDirectoryName$[9,3]=Right$(CPMFileName$,i+1)
    Fi
    Let TargetCPMDirectoryName$=UpperCase$(TargetCPMDirectoryName$)
    Return
!^L
CopyCPMToSDOSFile: ! Copy CP/M file to SDOS File
    Position #CPMDisk...
&            @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks
    For DirectoryEntry=1 to CPMDirectoryEntries
        Read #CPMDisk,CPMDirectoryEntry$
        If CPMDirectoryEntry$[1]=:E5 Then Cycle DirectoryEntry
        If CPMDirectoryEntry$[1]=0 and CPMDirectoryEntry$[13]=0
        Then
            If CPMDirectoryEntry$[2,11]=TargetCPMDirectoryName$
            Then CPMCopyFoundFileToSDOSFile
        Fi
    Next DirectoryEntry
    Print "No such CP/M file."
    Goto CPMUtilityNextCommand

CPMCopyFoundFileToSDOSFile:
    Create #SDOSFile,SDOSFileName$ \ ! Make a new SDOS file of appropo name
CPMCopyFoundFileToSDOSFileHandleDirectoryEntry:
    CPMRecordCount=CPMDirectoryEntry$[16] \ ! Number of sectors to copy
    If CPMRecordCount=0
    Then
        ! Beleive it or not, this is what CP/M does: I tried it.
        Print "EOF (Zero record count in directory entry encountered)"
        Close #SDOSFile
        Goto CPMUtilityNextCommand
    Fi
    ! Read all the data from each cluster specified by CPMDirectoryEntry$
    For ClusterIndex=1 to 16
        If CPMRecordCount=0
        Then
            ! We read all records that are in this extent.
            ! Print "EOF (Record count in directory entry < :80)"
            Close #SDOSFile
            Goto CPMUtilityNextCommand
        Fi
        Let ClusterNumber=CPMDirectoryEntry$[16+ClusterIndex]
        ! Print "Processing CPM Cluster Number ";ClusterNumber
        If ClusterNumber=0
        Then
            Print "EOF (Zero cluster number encountered)"
            Close #SDOSFile
            Goto CPMUtilityNextCommand
        Fi
        Position #CPMDisk...
&           @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks+...
&               ClusterNumber*CPMClusterSizeInSectors*CPMSectorSize
        For SectorNumber=1 to CPMClusterSizeInSectors
            If CPMRecordCount=0
            Then
                ! We read all records that are in this extent.
                ! Print "EOF (Record count in directory entry < :80)"
                Close #SDOSFile
                Goto CPMUtilityNextCommand
            Fi
            Read #CPMDisk,CPMBuffer$
            ! *** The following code works but it not used
            ! *** because we wish to copy arbitrary binary CP/M files,
            ! *** not just text files.
            ! Check for CP/M end of text file, i.e., ^Z (followed by Nulls)
            ! If find(CPMBuffer$,chr$(:1a))>0
            ! Then
            !     ! End of CP/M text file found. Stop the copy here.
            !     ! Print "EOF (^Z encountered)."
            !     Let i=find(CPMBuffer$,chr$(:1a))
            !     Write #SDOSFile,CPMBuffer$[1,i-1] \ ! Write last chunk
            !     Close #SDOSFile
            !     Goto CPMUtilityNextCommand
            ! Fi
            Write #SDOSFile,CPMBuffer$
            CPMRecordCount=CPMRecordCount-1 \ ! "Count" record that we read
        Next SectorNumber
    Next ClusterIndex

    ! Find Next directory entry for this file
    Let CPMExtentNumber=CPMDirectoryEntry$[13]+1
    Position #CPMDisk...
&            @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks
    For DirectoryEntry=1 to CPMDirectoryEntries
        Read #CPMDisk,CPMDirectoryEntry$
        If CPMDirectoryEntry$[1]=:E5 Then Cycle DirectoryEntry
        If CPMDirectoryEntry$[1]=0 and CPMDirectoryEntry$[13]=CPMExtentNumber
        Then
            If CPMDirectoryEntry$[2,11]=TargetCPMDirectoryName$
            Then CPMCopyFoundFileToSDOSFileHandleDirectoryEntry
        Fi
    Next DirectoryEntry
    Print "EOF (No more directory entries)"
    Close #SDOSFile
    Goto CPMUtilityNextCommand
!^L
CopySDOSToCPMFile: ! Copy SDOS file to CP/M File
    ! Read thru CP/M directory and build disk allocation map
    ! Also check to see if new file name already exists
    For i=1 to maxlen(CPMDiskMap$) do CPMDiskMap$(i)=0 \ ! mark clusters free
    CPMDiskMap$(1)=1 \ ! Cluster "zero" is always allocated
    Position #CPMDisk...
&            @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks
    For DirectoryEntry=1 to CPMDirectoryEntries
        Read #CPMDisk,CPMDirectoryEntry$
        If CPMDirectoryEntry$[1]=:E5 Then Cycle DirectoryEntry
        If CPMDirectoryEntry$[1]=0
        Then
            ! Found valid directory entry
            If CPMDirectoryEntry$[2,11]=TargetCPMDirectoryName$
            Then
                Print "?? ";CPMFileName$;" already exists, copy aborted."
                Goto CPMUtilityNextCommand
            Fi
            For ClusterIndex=1 to 16
                ! Mark each cluster owned by file as busy
                CPMDiskMap$[1+CPMDirectoryEntry$(16+ClusterIndex)]=1
            Next ClusterIndex
        Fi
    Next DirectoryEntry

!   Print "CP/M disk allocation map:"
!   For i=0 to 255 step 16
!       Print hex$(i)[4,2];"/ ";
!       for j=0 to 15
!           print hex$(CPMDiskMap$[i+j+1])[4,2];" ";
!       next j
!       Print
!   next i
!   Print

    ! If we get here, the file is really a new file!
    If Error When Open #SDOSFile,SDOSFileName$
    Then
        Print "Copy aborted: ";
        If Err=1011 Then Print "No such SDOS file."
        ElseIf Err=1023 Then Print "Bad SDOS file name."
        ElseIf Err=1056 Then Print "No such SDOS device."
        Else Error
        Goto CPMUtilityNextCommand
    Fi
    Let SDOSFileBuffer$="" \ ! Make sure buffer is empty when we start
    Let CPMExtentNumber=0 \ ! Extent of 1st CPMDirectoryEntry
CopySDOSFileToCPMNeedNewDirectoryEntry: ! Find space for next extent
    Position #CPMDisk...
&            @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks
    For DirectoryEntry=1 to CPMDirectoryEntries
        Read #CPMDisk,CPMDirectoryEntry$
        If CPMDirectoryEntry$[1]=:E5
        Then CopySDOSFileToCPMCreateNewDirectoryEntry
    Next DirectoryEntry
    Print "?? Directory is full, can't copy file"
    If CPMExtentNumber>0 Then EraseCPMFile Else Goto CPMUtilityNextCommand

CopySDOSFileToCPMCreateNewDirectoryEntry: ! Create new directory entry
    CPMRecordCount=0 \ ! Number of sectors ("records") in this extent
    CPMDirectoryEntry$[1]=0 \ ! Mark directory entry as valid
    CPMDirectoryEntry$[2,11]=TargetCPMDirectoryName$
    CPMDirectoryEntry$[13]=CPMExtentNumber
    CPMDirectoryEntry$[14]=0 \ ! ?? I don't know what this byte is
    CPMDirectoryEntry$[15]=0 \ ! ?? I don't know what this byte is
    CPMDirectoryEntry$[16]=CPMRecordCount
    For i=17 to 32 Do CPMDirectoryEntry$[i]=0 \ ! Mark "unallocated cluster"

    ! Now copy data to fill clusters for this directory entry
    For ClusterIndex=1 to 16
        ! Find available cluster number
        For ClusterNumber=0 to 255 until CPMDiskMap$[1+ClusterNumber]=0 ...
&           Do ! this cluster is busy, inspect the next
        If ClusterNumber=256
        Then
            Print "?? CP/M diskette is full, copy aborted."
            If CPMExtentNumber=0 and ClusterIndex=1
            Then CPMUtilityNextCommand Else EraseCPMFile
        Fi
        Let CPMDiskMap$[1+ClusterNumber]=1 \ ! Mark cluster as allocated
        Let CPMDirectoryEntry$[16+ClusterIndex]=ClusterNumber
        ! Print "Filling CPM Cluster Number ";ClusterNumber
        Position #CPMDisk...
&           @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks+...
&               ClusterNumber*CPMClusterSizeInSectors*CPMSectorSize
        For SectorNumber=1 to CPMClusterSizeInSectors
            Gosub GetCPMBufferFull
            ! Check for SDOS end of file
            If len(CPMBuffer$)<128
            Then
                ! End of SDOS text file found. Stop the copy here.
                ! Print "SDOS file EOF encountered."
                CPMRecordCount=CPMRecordCount+1
                Write #CPMDisk,CPMBuffer$,Chr$(:1a) \ ! Write last chunk
                Close #SDOSFile
                ! Write last directory entry back to diskette
                CPMDirectoryEntry$[16]=CPMRecordCount \ ! Set final count
                Write #CPMDisk...
&               @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks...
&                    +(DirectoryEntry-1)*32,CPMDirectoryEntry$
                Goto CPMUtilityNextCommand
            Fi
            CPMRecordCount=CPMRecordCount+1
            Write #CPMDisk,CPMBuffer$
        Next SectorNumber
    Next ClusterIndex
    ! This extent has been filled. Update the directory.
    CPMDirectoryEntry$[16]=CPMRecordCount \ ! Set final record count
    Write #CPMDisk...
&         @CPMSectorSize*CPMNumberOfSectorsPerTrack*CPMReservedTracks...
&              +(DirectoryEntry-1)*32,CPMDirectoryEntry$
    Let CPMExtentNumber=CPMExtentNumber+1 \ ! we need another extent.
    Goto CopySDOSFileToCPMNeedNewDirectoryEntry

GetCPMBufferFull: ! Set up 128 byte block of bytes for CP/M buffer
    Let CPMBuffer$=""
    Repeat
        If Len(SDOSFileBuffer$)=0
        Then
            Read #SDOSFile,SDOSFileBuffer$
            If Len(SDOSFileBuffer$)=0 Then Return \ ! Must be EOF
        Fi
        Let j=Find(SDOSFileBuffer$,chr$(:0D))
        If j=0
        Then
            ! No <CR> remains in SDOSFileBuffer$
            ! Attach all of SDOSFileBuffer$ to CPMBuffer$
            If Len(CPMBuffer$)+Len(SDOSFileBuffer$)<=128
            Then
                Let i=Len(CPMBuffer$)
                Let Len(CPMBuffer$)=i+Len(SDOSFileBuffer$)
                Let CPMBuffer$[i+1,Len(SDOSFileBuffer$)]=SDOSFileBuffer$
                Let SDOSFileBuffer$=""
            Else
                Let i=Len(CPMBuffer$)
                Let Len(CPMBuffer$)=128
                Let CPMBuffer$[i+1,128-i]=SDOSFileBuffer$
                Let SDOSFileBuffer$=Right$(SDOSFileBuffer$,128-i+1)
            Fi
        Else
            ! A <CR> at position j exists in SDOSFileBuffer$
            If Len(CPMBuffer$)+j<=128
            Then
                ! Copy text up to and including <CR> onto CPMBuffer$
                ! Replace <CR> by <LF>, remove all but <LF> from SDOSFileBuffer$
                Let i=Len(CPMBuffer$)
                Let Len(CPMBuffer$)=i+j
                Let CPMBuffer$[i+1,j]=SDOSFileBuffer$
                Let SDOSFileBuffer$=Right$(SDOSFileBuffer$,j)
                Let SDOSFileBuffer$[1]=:0A \ ! Insert the line feed
            Else
                ! oops, not enough room to place <CR> in CPMBuffer$ yet
                Let i=Len(CPMBuffer$)
                Let Len(CPMBuffer$)=128
                Let CPMBuffer$[i+1,128-i]=SDOSFileBuffer$
                Let SDOSFileBuffer$=Right$(SDOSFileBuffer$,128-i+1)
            Fi
        Fi
    Unless Len(CPMBuffer$)=128 End
    Return

END
