| // Copyright 2026 Google LLC |
| // |
| // 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. |
| |
| package cosboot |
| |
| import ( |
| "bytes" |
| "fmt" |
| "log" |
| "os/exec" |
| "regexp" |
| "strconv" |
| "strings" |
| |
| "cos.googlesource.com/cos/tools.git/src/pkg/tools/partutil" |
| ) |
| |
| var ( |
| defaultARegex = regexp.MustCompile(`defaultA=(.*)`) |
| defaultBRegex = regexp.MustCompile(`defaultB=(.*)`) |
| menuEntryRegex = regexp.MustCompile(`(?m)^menuentry\s+.*\{[\s\S]*?^\}`) |
| ) |
| |
| func readGPTOnCOS() (partutil.SFDiskTable, error) { |
| var stdout bytes.Buffer |
| var stderr bytes.Buffer |
| cmd := exec.Command("rootdev", "-s", "-d") |
| cmd.Stdout = &stdout |
| cmd.Stderr = &stderr |
| if err := cmd.Run(); err != nil { |
| log.Printf("Command %q failed with stderr: %v", cmd.String(), string(stderr.Bytes())) |
| return partutil.SFDiskTable{}, fmt.Errorf("cannot determine boot disk: %v", err) |
| } |
| disk := strings.TrimSpace(string(stdout.Bytes())) |
| return partutil.ReadPartitionTableJSON(disk) |
| } |
| |
| func isBootA(disk string) (bool, error) { |
| var gpt partutil.SFDiskTable |
| if disk == "" { |
| var err error |
| gpt, err = readGPTOnCOS() |
| if err != nil { |
| return false, err |
| } |
| } else { |
| var err error |
| gpt, err = partutil.ReadPartitionTableJSON(disk) |
| if err != nil { |
| return false, err |
| } |
| } |
| part2Attrs := gpt.PartitionTable.Partitions[1].Attrs |
| part2Prio, err := partutil.GPTPriority(part2Attrs) |
| if err != nil { |
| return false, fmt.Errorf("could not get partition 2 GPTPriority: %v", err) |
| } |
| part4Attrs := gpt.PartitionTable.Partitions[3].Attrs |
| part4Prio, err := partutil.GPTPriority(part4Attrs) |
| if err != nil { |
| return false, fmt.Errorf("could not get partition 4 GPTPriority: %v", err) |
| } |
| return part2Prio >= part4Prio, nil |
| } |
| |
| func NextGRUBCmdLine(disk string) (string, error) { |
| grubConfig, err := ReadEFIFile(disk, partutil.GRUBCfgEFIPath) |
| if err != nil { |
| return "", err |
| } |
| grubConfigStr := string(grubConfig) |
| matches := defaultARegex.FindStringSubmatch(grubConfigStr) |
| if len(matches) < 2 { |
| return "", fmt.Errorf("could not find defaultA in grub config: %v", grubConfigStr) |
| } |
| defaultA, err := strconv.Atoi(matches[1]) |
| if err != nil { |
| return "", err |
| } |
| matches = defaultBRegex.FindStringSubmatch(grubConfigStr) |
| if len(matches) < 2 { |
| return "", fmt.Errorf("could not find defaultB in grub config: %v", grubConfigStr) |
| } |
| defaultB, err := strconv.Atoi(matches[1]) |
| if err != nil { |
| return "", err |
| } |
| useBootA, err := isBootA(disk) |
| if err != nil { |
| return "", fmt.Errorf("Failed to determine if next boot is boot A on disk %q: %v", disk, err) |
| } |
| menuEntries := menuEntryRegex.FindAllString(grubConfigStr, -1) |
| var menuEntry string |
| if useBootA { |
| menuEntry = strings.TrimSpace(menuEntries[defaultA]) |
| } else { |
| menuEntry = strings.TrimSpace(menuEntries[defaultB]) |
| } |
| lines := strings.Split(menuEntry, "\n") |
| if len(lines) < 3 { |
| return "", fmt.Errorf("Menuentry is malformed: %v", menuEntry) |
| } |
| cmdline := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(lines[1]), "linux")) |
| return cmdline, nil |
| } |