diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..078fee4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: go + +notifications: + webhooks: + on_success: change + on_failure: always + on_start: never + email: + - tiago4orion@gmail.com + - tiagokatcipis@gmail.com diff --git a/cmd/disk/cmds/mbr.go b/cmd/disk/cmds/mbr.go new file mode 100644 index 0000000..672af19 --- /dev/null +++ b/cmd/disk/cmds/mbr.go @@ -0,0 +1,89 @@ +package cmds + +import ( + "flag" + "fmt" + + "github.com/barsoomia/disk/mbr" +) + +var ( + flags *flag.FlagSet + help, create, update *bool + addPart, delPart, startSect *int + bootcode, lastSect *string +) + +func init() { + flags = flag.NewFlagSet("mbr", flag.ContinueOnError) + help = flags.Bool("help", false, "Show this help") + create = flags.Bool("create", false, "Create new MBR") + update = flags.Bool("update", false, "Update MBR") + addPart = flag.Int("add-part", 0, "Add partition") + delPart = flag.Int("del-part", 0, "Delete partition") + startSect = flag.Int("start-sect", 0, "start sector") + lastSect = flag.String("last-sect", "", "last sector (modififers +K, +M, +G works)") + bootcode = flags.String("bootcode", "", "Bootsector binary code") +} + +func addPartition(disk string, partn int, startsect int, lastsect string) error { + mbrdata, err := mbr.FromFile(disk) + if err != nil { + return err + } + + p := mbr.NewEmptyPartition() + mbrdata.SetPart(partn, p) + return nil +} + +func MBR(args []string) error { + flags.Parse(args[1:]) + + if *help { + flags.PrintDefaults() + return nil + } + + disks := flags.Args() + if len(disks) != 1 { + return fmt.Errorf("Require one device file") + } + + if *create { + if *update { + return fmt.Errorf("-create conflicts with -update") + } + + return mbr.Create(disks[0], *bootcode) + } + + if *update { + if *addPart <= 0 || *delPart == 0 { + return fmt.Errorf("-update requires flag -add-part or --del-part") + } + + if *addPart != 0 { + partn := *addPart + if *startSect == -1 { + return fmt.Errorf("-add-part requires -start-sect") + } + + if *lastSect == "" { + return fmt.Errorf("-add-part requires -last-sect") + } + return addPartition(disks[0], partn, *startSect, *lastSect) + } + + return fmt.Errorf("-del-part not implemented") + } + + for _, disk := range disks { + err := mbr.Info(disk) + if err != nil { + return err + } + } + + return nil +} \ No newline at end of file diff --git a/cmd/disk/main.go b/cmd/disk/main.go new file mode 100644 index 0000000..2a0045d --- /dev/null +++ b/cmd/disk/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "os" + + "github.com/barsoomia/disk/cmd/disk/cmds" +) + +var ( + perr = func(format string, args ...interface{}) (int, error) { + return fmt.Fprintf(os.Stderr, format, args...) + } +) + +func usage() { + perr("fdisk [options] device/file\n") + perr("Subcommands:\n") + perr("\tmbr\n") + perr("\n") + perr("Use: fdisk -h for more info\n") +} + +func main() { + var err error + if len(os.Args) <= 1 { + usage() + os.Exit(1) + } + + switch os.Args[1] { + case "mbr": + err = cmds.MBR(os.Args[1:]) + default: + perr("Invalid subcommand: %s\n", os.Args[1]) + os.Exit(1) + } + + if err != nil { + perr("error: %s\n", err) + os.Exit(1) + } +} diff --git a/mbr/mbr.go b/mbr/mbr.go new file mode 100644 index 0000000..227da8e --- /dev/null +++ b/mbr/mbr.go @@ -0,0 +1,185 @@ +package mbr + +import ( + "fmt" + "io" + "math" + "os" +) + +type ( + mbr [MBRSize]byte + + // cylinder-head-sector + chs struct { + head, sector uint8 + cylinder uint16 + } +) + +const ( + MBRSize = 0x200 + // Classical MBR structure + CPart1 = 0x1be // 16 bytes each + CPart2 = 0x1ce + CPart3 = 0x1de + CPart4 = 0x1ee + + PEntrySZ = 16 + + Magic1Off = 0x1fe + Magic2Off = 0x1ff + + // MBR magic numbers + Magic1 = 0x55 + Magic2 = 0xaa + + // bootstrap code + BCOffEnd = 0x01bd + BCSize = BCOffEnd +) + +func NewCHS(cylinder uint16, head uint8, sector uint8) chs { + return chs{ + cylinder: cylinder, + head: head, + sector: sector, + } +} + +func Info(fname string) error { + file, err := os.Open(fname) + if err != nil { + return err + } + + var mbr [MBRSize]byte + _, err = file.Read(mbr[:]) + if err != nil && err != io.EOF { + return err + } + + if mbr[Magic1Off] != Magic1 && + mbr[Magic2Off] != Magic2 { + return fmt.Errorf("no MBR found\n") + } + + var part *partition + var empty bool + + if part, err, empty = NewPartition(mbr[CPart1 : CPart1+PEntrySZ]); err != nil { + return err + } else if !empty { + fmt.Printf("%s\n", part) + } + + if part, err, empty = NewPartition(mbr[CPart2 : CPart2+PEntrySZ]); err != nil { + return err + } else if !empty { + fmt.Printf("%s\n", part) + } + + if part, err, empty = NewPartition(mbr[CPart3 : CPart3+PEntrySZ]); err != nil { + return err + } else if !empty { + fmt.Printf("%s\n", part) + } + + if part, err, empty = NewPartition(mbr[CPart4 : CPart4+PEntrySZ]); err != nil { + return err + } else if !empty { + fmt.Printf("%s\n", part) + } + + return nil +} + +func NewMBR() mbr { + mbr := mbr{} + mbr[Magic1Off] = Magic1 + mbr[Magic2Off] = Magic2 + return mbr +} + +func FromFile(disk string) (*mbr, error) { + mbr := mbr{} + devfile, err := os.OpenFile(disk, os.O_RDWR, 0) + if err != nil { + return nil, err + } + + n, err := devfile.Read(mbr[:]) + if err != nil && err != io.EOF { + return nil, err + } + + if n < MBRSize { + return nil, fmt.Errorf("MBR requires at least 512 bytes") + } + + return &mbr, nil +} + +func (m mbr) SetBootcode(bcode []byte) error { + if len(bcode) > BCSize { + return fmt.Errorf("bootcode must have less than %d bytes", BCSize) + } + + if copied := copy(m[0:BCOffEnd], bcode[:]); copied != len(bcode) { + return fmt.Errorf("Failed to copy bootcode to mbr") + } + + return nil +} + +func (m mbr) SetPart(index int, part *partition) { + copy(m[CPart1+index*16:16], part.Bytes()) +} + +func Create(devfname, bootfname string) error { + mbr := NewMBR() + devfile, err := os.OpenFile(devfname, os.O_RDWR, 0) + if err != nil { + return err + } + + if bootfname != "" { + bfile, err := os.Open(bootfname) + if err != nil { + return err + } + + var bootcode [BCSize + 1]byte + n, err := bfile.Read(bootcode[:]) + if err == io.EOF || n > BCSize { + return fmt.Errorf("bootcode must have less than %d bytes. Got %d", BCSize, n) + } + + err = mbr.SetBootcode(bootcode[0:n]) + if err != nil { + return err + } + } + + _, err = devfile.Write(mbr[:]) + return err +} + +func CHS2LBA(c uint16, h uint8, s uint8) uint32 { + return (uint32(c)*16+uint32(h))*63 + uint32((s - 1)) +} + +func LBA2C(lba uint32) uint16 { + return uint16(math.Mod(float64(lba), 16*63)) +} + +func LBA2H(lba uint32) uint8 { + lbaf := float64(lba) + spt := float64(63) + hpt := float64(16) + return uint8(math.Mod(math.Mod(lbaf, spt), hpt)) +} + +func LBA2S(lba uint32) uint8 { + return uint8(math.Mod(float64(lba), float64(63))) + 1 +} diff --git a/mbr/mbr_test.go b/mbr/mbr_test.go new file mode 100644 index 0000000..04c36f0 --- /dev/null +++ b/mbr/mbr_test.go @@ -0,0 +1,50 @@ +package mbr + +import "testing" + +type testTbl struct { + chs chs + lba uint32 +} + +func TestCHS2LBA(t *testing.T) { + var tests []testTbl + + for i := 1; i < 64; i++ { + tests = append(tests, struct { + chs chs + lba uint32 + }{ + chs: NewCHS(0, 0, uint8(i)), + lba: uint32(i - 1), + }) + } + + tests = append(tests, testTbl{ + chs: NewCHS(0, 1, 1), + lba: 63, + }) + + tests = append(tests, testTbl{ + chs: NewCHS(0, 15, 1), + lba: 945, + }) + + tests = append(tests, testTbl{ + chs: NewCHS(15, 15, 63), + lba: 16127, + }) + + tests = append(tests, testTbl{ + chs: NewCHS(16319, 15, 63), + lba: 16450559, + }) + + for _, test := range tests { + if val := CHS2LBA(test.chs.cylinder, test.chs.head, test.chs.sector); val != test.lba { + t.Errorf("Expected %d but got %d", test.lba, val) + return + } + } + +} diff --git a/mbr/partition.go b/mbr/partition.go new file mode 100644 index 0000000..e8b08b9 --- /dev/null +++ b/mbr/partition.go @@ -0,0 +1,152 @@ +package mbr + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +type ( + status byte + typ byte + + partition struct { + number uint8 + status status + begin chs + typ typ + end chs + lba int32 + nsectors int32 + } +) + +func (chs chs) String() string { + return fmt.Sprintf("%d/%d/%d", chs.cylinder, chs.head, chs.sector) +} + +func (t typ) String() string { + switch t { + case 0x83: + return fmt.Sprintf("%x (Linux)", int(t)) + } + + return "unknown" +} + +func (st status) String() string { + err := "" + if st&0x80 != 0 && st&0x80 != 0x80 { + err += " (wrong)" + } + + switch st >> 7 { + case 1: + return "active" + err + } + + return "inactive" + err +} + +func (p partition) String() string { + format := `Partition #%d +Status: %s +FS type: %s +First C/H/S: %s +Last C/H/S: %s +LBA: %d +Number of sectors: %d +` + return fmt.Sprintf(format, p.number, p.status, p.typ, p.begin, p.end, p.lba, p.nsectors) +} + +func (p partition) Bytes() []byte { + var data [16]byte + + data[0] = byte(p.status) + data[1] = p.begin.head + data[2] = byte((p.begin.cylinder >> 6) | uint16(p.begin.sector&0x3f)) + return data[:] +} + +func NewPartition(entry []byte) (*partition, error, bool) { + part := &partition{} + + if len(entry) != 16 { + return nil, fmt.Errorf("Invalid partition entry: %v", entry), false + } + + if isPartEmpty(entry) { + return nil, nil, true + } + + part.status = status(entry[0]) + part.begin.head = uint8(entry[1]) + part.begin.sector = getsector(entry[2]) + + if part.begin.sector == 0 { + return nil, fmt.Errorf("First sector must be >= 1. Found %d", + part.begin.sector), false + } + + part.begin.cylinder = getcylinder(entry[2], entry[3]) + part.typ = typ(entry[4]) + part.end.head = uint8(entry[5]) + part.end.sector = getsector(entry[6]) + part.end.cylinder = getcylinder(entry[6], entry[7]) + + buf := bytes.NewBuffer(entry[8:12]) + err := binary.Read(buf, binary.LittleEndian, &part.lba) + + if err != nil { + return nil, err, false + } + + buf = bytes.NewBuffer(entry[12:16]) + err = binary.Read(buf, binary.LittleEndian, &part.nsectors) + + return part, err, false +} + +func NewEmptyPartition() *partition { + return &partition{} +} + +func (p *partition) IsEqual(other *partition) bool { + if p == other { + return true + } + + if p.status != other.status || + p.begin.head != other.begin.head || + p.begin.sector != other.begin.sector || + p.begin.cylinder != other.begin.cylinder || + p.typ != other.typ || + p.end.head != other.end.head || + p.end.sector != other.end.sector || + p.end.cylinder != other.end.cylinder || + p.lba != other.lba || + p.nsectors != other.nsectors { + return false + } + + return true +} + +func isPartEmpty(buf []byte) bool { + for i := 0; i < 16; i++ { + if buf[i] != 0 { + return false + } + } + + return true +} + +func getsector(b byte) uint8 { + return b & 0x3f // sector in bits 5-0 +} + +func getcylinder(b1, b2 byte) uint16 { + return uint16(b1&0xc0)<<2 | uint16(b2) +} diff --git a/mbr/partition_test.go b/mbr/partition_test.go new file mode 100644 index 0000000..107a5c3 --- /dev/null +++ b/mbr/partition_test.go @@ -0,0 +1,111 @@ +package mbr + +import "testing" + +func testParse(entry [16]byte, expected *partition, t *testing.T) { + part, err, empty := NewPartition(entry[:]) + + if err != nil { + t.Error(err) + return + } + + if empty { + t.Errorf("Partition is empty") + return + } + + if !part.IsEqual(expected) { + t.Errorf("Partitions differ: %v != %v", part, expected) + return + } +} + +func TestPartitionFailParse(t *testing.T) { + // empty partition entry + _, err, _ := NewPartition([]byte{}) + + if err == nil { + t.Errorf("Partition entry must have at least 16 bytes") + return + } + + _, err, _ = NewPartition([]byte{ + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, + }) // 15 bytes + + if err == nil { + t.Errorf("Partition entry must have at least 16 bytes") + return + } + + _, err, empty := NewPartition([]byte{ + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + }) + + if err != nil { + t.Error(err) + return + } + + if !empty { + t.Errorf("Partition entry is empty") + return + } + + _, err, empty = NewPartition([]byte{ + 0x80, // active partition + 0, // wrong, sector must be >= 1 + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + }) + + if err == nil { + t.Errorf("Must fail, first sector must be >= 1") + return + } + + if empty { + t.Error("is not empty") + return + } +} + +func TestPartitionOK(t *testing.T) { + for _, test := range []struct { + entry [16]byte + expected partition + }{ + { + entry: [16]byte{ + 0, 0, 1, 0, + 0x83, + 255, 0x3f | 0xc0, 0xff, 0, 0, + 0, 0}, + expected: partition{ + status: 0, + begin: chs{ + head: 0, + sector: 1, + cylinder: 0, + }, + typ: 0x83, + end: chs{ + head: 255, + sector: 63, + cylinder: 1023, + }, + lba: 0, + nsectors: 0, + }, + }, + } { + testParse(test.entry, &test.expected, t) + } +} diff --git a/mkfile b/mkfile new file mode 100644 index 0000000..b903477 --- /dev/null +++ b/mkfile @@ -0,0 +1,3 @@ +all: + go build ./cmd/disk +