isoutils.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package isoutils
  15. import (
  16. "bytes"
  17. "fmt"
  18. "io"
  19. "regexp"
  20. "strings"
  21. "sync"
  22. "github.com/kdomanski/iso9660"
  23. "github.com/mogaika/udf"
  24. "gopkg.in/ini.v1"
  25. "yunion.io/x/log"
  26. "yunion.io/x/pkg/util/imagetools"
  27. )
  28. // ========== 2. 新增结构化返回结果(包含发行版、版本号、架构) ==========
  29. type ISOInfo struct {
  30. Distro string // 发行版(如 CentOS、Ubuntu Server)
  31. Version string // 版本号(如 7.9、22.04 LTS、2022)
  32. Arch string // 架构(如 x86_64、riscv64、arm64)
  33. Language string // 语言(如 en-US、zh-CN)
  34. }
  35. // ISO格式类型
  36. type ISOFormat string
  37. const (
  38. ISOFormatUnknown ISOFormat = "unknown"
  39. ISOFormatUDF ISOFormat = "udf"
  40. ISOFormatISO9660 ISOFormat = "iso9660"
  41. )
  42. // ========== 3. 优化ISOFileReader:增加缓存、日志、架构识别 ==========
  43. type ISOFileReader struct {
  44. format ISOFormat
  45. img *udf.Udf
  46. iso9660Img *iso9660.Image // ISO9660格式的读取器
  47. reader io.Reader
  48. cache sync.Map // 缓存已读取的文件内容:key=文件路径,value=文件内容
  49. }
  50. // isIsoFile 检测ISO格式(UDF或ISO9660)
  51. func isIsoFile(readerAt io.ReaderAt) (bool, error) {
  52. // 读取0x8000地址的内容(ISO9660的Primary Volume Descriptor位置)
  53. buf := make([]byte, 6)
  54. n, err := readerAt.ReadAt(buf, 0x8000)
  55. if err != nil && err != io.EOF {
  56. return false, fmt.Errorf("读取ISO格式标识失败: %v", err)
  57. }
  58. if n < 6 {
  59. return false, fmt.Errorf("读取数据不足")
  60. }
  61. // ISO9660格式:偏移0x8001-0x8005应该是"CD001"
  62. if bytes.Equal(buf[1:6], []byte("CD001")) {
  63. return true, nil
  64. }
  65. return false, nil
  66. }
  67. // NewISOFileReader 初始化ISO读取器(新增格式检测和日志配置)
  68. func NewISOFileReader(reader io.Reader) (*ISOFileReader, error) {
  69. readerAt, ok := reader.(io.ReaderAt)
  70. if !ok {
  71. return nil, fmt.Errorf("reader is not io.ReaderAt")
  72. }
  73. // 检测ISO格式
  74. isIso, err := isIsoFile(readerAt)
  75. if err != nil {
  76. return nil, err
  77. }
  78. if !isIso {
  79. return nil, fmt.Errorf("ISO镜像格式不正确")
  80. }
  81. ret := &ISOFileReader{
  82. format: ISOFormatISO9660,
  83. reader: reader,
  84. cache: sync.Map{},
  85. }
  86. if isUdfFile(readerAt) {
  87. ret.format = ISOFormatUDF
  88. ret.img = udf.NewUdfFromReader(readerAt)
  89. return ret, nil
  90. }
  91. isoImg, err := iso9660.OpenImage(readerAt)
  92. if err != nil {
  93. return nil, fmt.Errorf("打开ISO9660镜像失败: %v", err)
  94. }
  95. ret.iso9660Img = isoImg
  96. return ret, nil
  97. }
  98. // ISO9660FileInfo ISO9660文件信息
  99. type ISO9660FileInfo struct {
  100. Name string // 文件名
  101. IsDir bool // 是否为目录
  102. Size int64 // 文件大小(字节)
  103. Location int64 // 文件在ISO中的位置(字节偏移,使用库时可能为0)
  104. }
  105. func (r *ISOFileReader) list(path string) ([]ISO9660FileInfo, error) {
  106. if r.format == ISOFormatISO9660 {
  107. return r.listISO9660Dir(path)
  108. }
  109. return r.listUdfDir(path)
  110. }
  111. // FileExists 检查ISO内指定路径的文件是否存在(支持UDF和ISO9660)
  112. func (r *ISOFileReader) FileExists(path string) bool {
  113. if r.format == ISOFormatISO9660 {
  114. _, err := r.findISO9660File(path)
  115. return err == nil
  116. }
  117. _, err := r.GetFile(path)
  118. return err == nil
  119. }
  120. // ReadFileContent 读取ISO内指定文件的内容(新增缓存、日志,支持UDF和ISO9660)
  121. func (r *ISOFileReader) ReadFileContent(path string) (string, error) {
  122. // 优先从缓存读取
  123. if cacheVal, ok := r.cache.Load(path); ok {
  124. log.Debugf("从缓存读取文件内容: %s", path)
  125. return cacheVal.(string), nil
  126. }
  127. var content string
  128. var err error
  129. // 根据格式选择相应的读取方法
  130. if r.format == ISOFormatISO9660 {
  131. content, err = r.readISO9660FileContent(path)
  132. } else if r.format == ISOFormatUDF {
  133. content, err = r.readUdfFileContent(path)
  134. } else {
  135. return "", fmt.Errorf("未知的ISO格式: %s", r.format)
  136. }
  137. if err != nil {
  138. return "", err
  139. }
  140. // 写入缓存
  141. r.cache.Store(path, content)
  142. log.Debugf("读取文件%s内容(长度: %d)并缓存", path, len(content))
  143. return content, nil
  144. }
  145. // ========== 6. 核心识别函数:整合版本号、架构、日志、缓存 ==========
  146. func DetectOSFromISO(r io.Reader) (*ISOInfo, error) {
  147. defer func() {
  148. if r := recover(); r != nil {
  149. log.Errorf("DetectOSFromISO panic error: %v", r)
  150. }
  151. }()
  152. result := &ISOInfo{}
  153. // 初始化读取器
  154. reader, err := NewISOFileReader(r)
  155. if err != nil {
  156. return nil, err
  157. }
  158. // ========== 识别Windows系列 ==========
  159. if reader.FileExists("sources/install.wim") {
  160. return DetectWindowsEdition(reader)
  161. }
  162. files, err := reader.list("/")
  163. if err != nil {
  164. return nil, err
  165. }
  166. for _, file := range files {
  167. fileName := file.Name
  168. if fileName == ".treeinfo" {
  169. content, _ := reader.ReadFileContent(fileName)
  170. result = getOsInfoByIniFile(content)
  171. break
  172. }
  173. }
  174. if reader.FileExists(".disk/info") {
  175. content, _ := reader.ReadFileContent(".disk/info")
  176. info := imagetools.NormalizeImageInfo(content, "", "", "", "")
  177. result = &ISOInfo{
  178. Distro: info.OsDistro,
  179. Version: info.OsVersion,
  180. Arch: info.OsArch,
  181. Language: info.OsLang,
  182. }
  183. }
  184. if len(result.Distro) == 0 || result.Distro == imagetools.OS_DIST_OTHER_LINUX {
  185. realeaseFile := ""
  186. if reader.FileExists("dists") {
  187. files, err := reader.list("dists")
  188. if err != nil {
  189. return nil, err
  190. }
  191. for _, file := range files {
  192. log.Debugf("file: %s", file.Name)
  193. if !file.IsDir {
  194. continue
  195. }
  196. subFiles, err := reader.list(fmt.Sprintf("dists/%s", file.Name))
  197. if err != nil {
  198. return nil, err
  199. }
  200. for _, subFile := range subFiles {
  201. if subFile.Name == "Release" {
  202. realeaseFile = fmt.Sprintf("dists/%s/%s", file.Name, subFile.Name)
  203. break
  204. }
  205. }
  206. if realeaseFile != "" {
  207. break
  208. }
  209. }
  210. }
  211. if realeaseFile != "" {
  212. content, _ := reader.ReadFileContent(realeaseFile)
  213. result = getOsInfoByReleaseFile(content)
  214. } else if reader.FileExists("boot/grub2/grub.cfg") {
  215. content, _ := reader.ReadFileContent("boot/grub2/grub.cfg")
  216. result = getOsInfoByGrub(content)
  217. } else if reader.FileExists("EFI/BOOT/grub.cfg") {
  218. content, _ := reader.ReadFileContent("EFI/BOOT/grub.cfg")
  219. result = getOsInfoByGrub(content)
  220. } else if reader.FileExists("isolinux/isolinux.cfg") {
  221. content, _ := reader.ReadFileContent("isolinux/isolinux.cfg")
  222. result = getOsInfoByIsoLinux(content)
  223. }
  224. }
  225. return result, nil
  226. }
  227. func getOsInfoByReleaseFile(content string) *ISOInfo {
  228. result := &ISOInfo{}
  229. for _, line := range strings.Split(content, "\n") {
  230. if strings.HasPrefix(line, "Origin:") {
  231. result.Distro = strings.TrimSpace(strings.TrimPrefix(line, "Origin:"))
  232. }
  233. if strings.HasPrefix(line, "Label:") && len(result.Distro) == 0 {
  234. result.Distro = strings.TrimSpace(strings.TrimPrefix(line, "Label:"))
  235. }
  236. if strings.HasPrefix(line, "Version:") {
  237. result.Version = strings.TrimSpace(strings.TrimPrefix(line, "Version:"))
  238. }
  239. if strings.HasPrefix(line, "Architectures:") {
  240. result.Arch = strings.TrimSpace(strings.TrimPrefix(line, "Architectures:"))
  241. result.Arch = detectArchitecture(strings.ToLower(result.Arch), result.Arch)
  242. }
  243. }
  244. return result
  245. }
  246. func getOsInfoByIniFile(content string) *ISOInfo {
  247. cfg, err := ini.Load([]byte(content))
  248. if err != nil {
  249. // 兼容手动解析(应对部分非标准 INI 格式的 .treeinfo)
  250. return parseTreeInfoFallback(content)
  251. }
  252. release := cfg.Section("release")
  253. ret := &ISOInfo{}
  254. ret.Distro = release.Key("name").String()
  255. ret.Version = release.Key("version").String()
  256. general := cfg.Section("general")
  257. ret.Arch = general.Key("arch").String()
  258. if len(ret.Version) == 0 {
  259. ret.Version = general.Key("version").String()
  260. }
  261. return ret
  262. }
  263. func parseTreeInfoFallback(content string) *ISOInfo {
  264. result := &ISOInfo{}
  265. info := strings.Split(content, "\n")
  266. for _, line := range info {
  267. if strings.HasPrefix(line, "arch =") {
  268. result.Arch = strings.TrimPrefix(line, "arch = ")
  269. }
  270. if strings.HasPrefix(line, "version =") {
  271. result.Version = strings.TrimPrefix(line, "version = ")
  272. }
  273. if strings.HasPrefix(line, "name =") {
  274. result.Distro = strings.TrimPrefix(line, "name = ")
  275. }
  276. }
  277. return result
  278. }
  279. func getOsInfoByIsoLinux(content string) *ISOInfo {
  280. result := &ISOInfo{}
  281. lowerContent := strings.ToLower(content)
  282. // 5.1 识别发行版(关键词匹配)
  283. result.Distro = detectDistro(lowerContent)
  284. // 5.2 识别版本号(正则提取)
  285. result.Version = detectVersion(content)
  286. // 5.3 识别 CPU 架构(关键词+正则)
  287. result.Arch = detectArchitecture(lowerContent, content)
  288. return result
  289. }
  290. func getOsInfoByGrub(content string) *ISOInfo {
  291. result := &ISOInfo{}
  292. lowerContent := strings.ToLower(content)
  293. result.Distro = detectDistro(lowerContent)
  294. result.Version = detectGrubVersion(content)
  295. result.Arch = detectArchitecture(lowerContent, content)
  296. return result
  297. }
  298. func detectDistro(lowerContent string) string {
  299. info := imagetools.NormalizeImageInfo(lowerContent, "", "", "", "")
  300. return info.OsDistro
  301. }
  302. // detectVersion 从配置内容中提取版本号
  303. func detectVersion(content string) string {
  304. // 匹配版本号的正则(支持 x x.y、x.y.z、x.y-LTS 等格式)
  305. versionRegex := regexp.MustCompile(`(\d+(\.\d+(\.\d+)?)?(-[A-Za-z0-9]+)?)`)
  306. // 优先从启动标题(label/menu label)中提取
  307. labelLines := regexp.MustCompile(`(?i)menu label .+Install.+`).FindAllStringSubmatch(content, -1)
  308. for _, line := range labelLines {
  309. log.Debugf("line: %s", line)
  310. if len(line) >= 1 {
  311. version := versionRegex.FindString(line[0])
  312. if version != "" {
  313. return version
  314. }
  315. }
  316. }
  317. // 从整个内容中提取第一个匹配的版本号
  318. return versionRegex.FindString(content)
  319. }
  320. // detectArchitecture 识别 CPU 架构
  321. func detectArchitecture(lowerContent, rawContent string) string {
  322. // 架构关键词映射
  323. archKeywords := map[string][]string{
  324. "x86_64": {"x86_64", "amd64"},
  325. "aarch64": {"aarch64", "arm64"},
  326. "i386": {"i386", "i686"},
  327. "armhfp": {"armhfp", "armv7"},
  328. "ppc64le": {"ppc64le"},
  329. "s390x": {"s390x"},
  330. }
  331. for arch, keywords := range archKeywords {
  332. for _, kw := range keywords {
  333. if strings.Contains(lowerContent, kw) {
  334. return arch
  335. }
  336. }
  337. }
  338. // 从内核文件名(vmlinuz/initrd)中提取
  339. kernelRegex := regexp.MustCompile(`vmlinuz-([a-zA-Z0-9_]+)`)
  340. match := kernelRegex.FindStringSubmatch(rawContent)
  341. if len(match) >= 2 {
  342. return match[1]
  343. }
  344. return ""
  345. }
  346. // detectGrubVersion 从 grub.cfg 提取版本号
  347. func detectGrubVersion(content string) string {
  348. // 匹配版本号的正则(支持 x x.y、x.y.z、x.y-LTS、x.y.z-xxx 等格式)
  349. versionRegex := regexp.MustCompile(`(\d+(\.\d+(\.\d+)?)?(-[A-Za-z0-9]+)?)`)
  350. // 优先从 GRUB 菜单标题(menuentry)中提取(准确性更高)
  351. menuEntryRegex := regexp.MustCompile(`(?i)menuentry\s+["'](.+?)["']`)
  352. menuEntries := menuEntryRegex.FindAllStringSubmatch(content, -1)
  353. for _, entry := range menuEntries {
  354. log.Debugf("entry: %s", entry)
  355. if len(entry) >= 1 {
  356. version := versionRegex.FindString(entry[0])
  357. if version != "" {
  358. return version
  359. }
  360. }
  361. }
  362. // 从内核文件名/参数中提取
  363. kernelLines := regexp.MustCompile(`linux\s+.+`).FindAllString(content, -1)
  364. for _, line := range kernelLines {
  365. version := versionRegex.FindString(line)
  366. if version != "" {
  367. return version
  368. }
  369. }
  370. // 最后从整个内容中提取第一个匹配的版本号
  371. return versionRegex.FindString(content)
  372. }