package app import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" ) // oneOSAssetVehicle contains only asset catalog fields from the OneOS vehicle // page. Authorization only consumes fully enumerated caller-scoped snapshots // when ONEOS_VEHICLE_QUERY_SCOPE is explicitly enabled. type oneOSAssetVehicle struct { ID string `json:"id"` VIN string `json:"vin"` PlateNumber string `json:"plateNumber"` VehicleType string `json:"vehicleType"` Brand string `json:"brand"` BrandName string `json:"brandName"` Model string `json:"model"` ModelName string `json:"modelName"` ProvinceName string `json:"provinceName"` CityName string `json:"cityName"` CustomerName string `json:"customerName"` ProjectName string `json:"projectName"` BusinessDepartment string `json:"businessDepartment"` BusinessResponsiblePerson string `json:"businessResponsiblePerson"` OperationStatus string `json:"operationStatus"` VehicleSource string `json:"vehicleSource"` } func (v *oneOSAssetVehicle) UnmarshalJSON(raw []byte) error { type fields oneOSAssetVehicle var payload struct { fields ID json.RawMessage `json:"id"` } if err := json.Unmarshal(raw, &payload); err != nil { return err } *v = oneOSAssetVehicle(payload.fields) id, err := oneOSIDString(payload.ID) if err != nil { return err } v.ID = id return nil } type oneOSAssetPage struct { Records []oneOSAssetVehicle `json:"records"` Total int64 `json:"total"` Current int64 `json:"current"` Size int64 `json:"size"` } var errOneOSAssetQuery = errors.New("invalid OneOS asset query") var oneOSAssetPageFilters = map[string]bool{ "assetCategory": true, "vin": true, "plateNumber": true, "plateNumberList": true, "province": true, "city": true, "provinces": true, "vehicleType": true, "brand": true, "model": true, "customerName": true, "businessDept": true, "registeredOwnership": true, "contractCode": true, "operationOrgId": true, "vehicleSource": true, "rentalCompany": true, "operationStatus": true, "vehicleStatus": true, "onlineStatus": true, "licenseStatus": true, "packingLotId": true, "provinceName": true, "cityName": true, "commercialInsuranceStatus": true, "compulsoryInsuranceStatus": true, "projectName": true, "insuranceStatus": true, } const oneOSVehicleAssetCategory = "allOperating" // VehicleAssetPage faithfully reads one bounded page of the OneOS vehicle // catalog. It neither scans all pages nor turns records into permission grants. func (c *oneOSProfileClient) VehicleAssetPage(ctx context.Context, token, clientID string, filters url.Values) (oneOSAssetPage, error) { return c.vehicleAssetPage(ctx, token, clientID, filters, 50) } // Larger pages are reserved for the server-side permission snapshot. func (c *oneOSProfileClient) vehicleAssetPage(ctx context.Context, token, clientID string, filters url.Values, maxPageSize int64) (oneOSAssetPage, error) { if c == nil || !validOneOSLoginToken(token) || clientID == "" || clientID != c.passwordClientID { return oneOSAssetPage{}, errors.New("invalid OneOS credential") } query := url.Values{} pageNum, pageSize := int64(1), int64(10) if filters != nil { for key, values := range filters { if key == "pageNum" || key == "pageSize" { if len(values) != 1 || strings.TrimSpace(values[0]) == "" { return oneOSAssetPage{}, errOneOSAssetQuery } continue } if !oneOSAssetPageFilters[key] || len(values) == 0 || len(values) > 20 { return oneOSAssetPage{}, errOneOSAssetQuery } for _, value := range values { if strings.TrimSpace(value) == "" || len(value) > 128 || strings.ContainsAny(value, "\r\n") { return oneOSAssetPage{}, errOneOSAssetQuery } query.Add(key, value) } } if raw := filters.Get("pageNum"); raw != "" { parsed, err := strconv.ParseInt(raw, 10, 64) if err != nil || parsed < 1 || parsed > 1000000 || len(filters["pageNum"]) != 1 { return oneOSAssetPage{}, errOneOSAssetQuery } pageNum = parsed } if raw := filters.Get("pageSize"); raw != "" { parsed, err := strconv.ParseInt(raw, 10, 64) if err != nil || parsed < 1 || parsed > maxPageSize || len(filters["pageSize"]) != 1 { return oneOSAssetPage{}, errOneOSAssetQuery } pageSize = parsed } } if len(query["assetCategory"]) > 1 { return oneOSAssetPage{}, errOneOSAssetQuery } // Fixed business boundary; caller filters cannot select a different category. query.Set("assetCategory", oneOSVehicleAssetCategory) query.Set("pageNum", strconv.FormatInt(pageNum, 10)) query.Set("pageSize", strconv.FormatInt(pageSize, 10)) ctx, cancel := context.WithTimeout(ctx, c.requestTimeout) defer cancel() request, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/asset/vehicle-info/page?"+query.Encode(), nil) if err != nil { return oneOSAssetPage{}, errors.New("invalid OneOS asset request") } request.Header.Set("Authorization", "Bearer "+token) request.Header.Set("ClientID", clientID) request.Header.Set("Accept", "application/json") response, err := c.client.Do(request) if err != nil { return oneOSAssetPage{}, errors.New("OneOS asset page unavailable") } defer response.Body.Close() if response.StatusCode != http.StatusOK { return oneOSAssetPage{}, fmt.Errorf("OneOS asset page rejected: HTTP %d", response.StatusCode) } raw, err := io.ReadAll(io.LimitReader(response.Body, (1<<20)+1)) if err != nil || len(raw) > 1<<20 { return oneOSAssetPage{}, errors.New("OneOS asset page too large") } var envelope oneOSProfileEnvelope[oneOSAssetPage] if err := json.Unmarshal(raw, &envelope); err != nil || envelope.Code != 200 { return oneOSAssetPage{}, errors.New("invalid OneOS asset page response") } page := envelope.Data if page.Total < 0 || page.Current != pageNum || page.Size != pageSize || int64(len(page.Records)) > pageSize { return oneOSAssetPage{}, errors.New("invalid OneOS asset page metadata") } return page, nil }